微服务之间打通用户上下文
- 打通上下文步骤
- 需求:
- 1、gateway网关登录拦截器:【LoginFilter】
- 解释:
- 代码
- 2、SpringMVC全局处理:【GlobalConfig】
- 解释:
- 代码:
- 3、自定义登录拦截器:【LoginInterceptor】
- 解释:
- 代码:
- 4、创建登录上下文对象:【LoginContextHolder】
- 解释:
- 代码:
- 5、简单对外的Util工具类
- 解释:
- 代码:
- 6、测试
打通上下文步骤
需求:
上下文打通,获取用户登录的唯一标识loginId:就是当前端页面发送请求时,如果我们要获取当前登录用户的loginId,那么需要在 HttpServletRequest 里面拿,很不方便现在我们通过自定义拦截器,来实现随时随地获取当前登录用户的loginId,这个就是微服务之间的上下文打通了。
1、gateway网关登录拦截器:【LoginFilter】
解释:
网关登录拦截器:用于自定义header请求头作用:从 Sa-Token 框架中获取当前登录用户的 loginId,【把它放进请求头中】,传给后端其他服务用。loginId 是 用户标识
header 是 服务间传递信息的载体
这个过程其实是 用户上下文的传递如果不做拦截,下游的每个服务都需要拿到token后,通过 Sa-Token 的工具类 StpUtil.getLoginId() 再解析一次。
如果没做统一拦截,每个服务、每个接口都要重复解析 token比如:------------------------------------------------
想象你是个前台小姐姐(前端),你手上拿着一张门票(token),
你给安保(网关)看了一下,安保说:“你是 VIP 用户123,欢迎~”
现在这个安保要放你进去,就顺手在你胸牌上贴了个条:“loginId: 123”
后面每个部门(子服务)看到你,只需要看你胸口的标签就知道你是谁了,不用每次都回头问安保“诶,这人是谁?
代码
/*** 网关的登录拦截器:* 作用:解析token,拿到LoginId(当前微信用户登录的唯一标识),用来做用户上下文打通* GlobalFilter:Spring Cloud Gateway 提供的全局过滤器接口,所有请求都会经过此类** @author lujinhong* @since 2025-04-14*/@Component
@Slf4j
public class LoginFilter implements GlobalFilter {/*** 这是过滤器的主要方法,所有经过网关的请求都会先执行这里的逻辑* 1、把sa-token中的用户登录的唯一标识loginId放到请求头中*/@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 获取当前前端传来的请求头信息ServerHttpRequest request = exchange.getRequest();// 获取一个可变的请求构造器,用于后续修改请求头ServerHttpRequest.Builder mutate = request.mutate();String url = request.getURI().getPath();log.info("LoginFilter url:{}", url);if (url.equals("/user/doLogin")) {// 如果当前请求是登录,直接放行,不拦截,只有登录后,用户才会有loginIdreturn chain.filter(exchange);}SaTokenInfo tokenInfo = StpUtil.getTokenInfo();log.info("LoginFilter.filter.url:{}", new Gson().toJson(tokenInfo));// 获取当前用户的登录标识String loginId = (String) tokenInfo.getLoginId();if (StringUtils.isEmpty(loginId)) {throw new RuntimeException("未获取到用户信息-->loginId");}// 往header放一个 loginIdmutate.header("loginId", loginId);// 构造一个新的 ServerWebExchange,并调用 chain.filter() 继续执行下一个过滤器或最终的业务逻辑Mono<Void> filter = chain.filter(exchange.mutate().request(mutate.build()).build());return filter;}
}
2、SpringMVC全局处理:【GlobalConfig】
解释:
就是在项目启动的时候,添加一个我自己定义的拦截器到拦截器链里面
把自定义的拦截器 LoginInterceptor 注册进 Spring MVC 的拦截链里,让它可以拦截所有进来的请求。每个请求都需要经过 LoginInterceptor 拦截器处理一遍LoginInterceptor 用来拦截登录的header头信息,把loginId 存到ThreadLocal中
代码:
//@Configuration 用于标注一个类为配置类
@Configuration
public class GlobalConfig extends WebMvcConfigurationSupport {/*** 重写这个方法的目的就是换一个自定义的拦截器,用来拦截header的东西* 问题:请求时,打断点看,没有执行到这个方法,直接执行到 LoginInterceptor 的方法* 回答:这个方法是项目启动的时候就初始化执行一次了,把自定义的 LoginInterceptor 注册到拦截器链中了。* 这个操作是在我们发起请求之前,所以打断点不会执行到这里,项目重新启动就可以看到执行这个方法*/protected void addInterceptors(InterceptorRegistry registry) {// /** 拦截所有请求,每个请求都需要进入到 LoginInterceptor 这个自定义拦截器中registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**");}}
3、自定义登录拦截器:【LoginInterceptor】
解释:
上面的网关拦截器,从 satoken中获取到loginId,然后存到放 header 头中。现在这个拦截器作用就是:
1、通过实现 HandlerInterceptor 接口,重写 preHandle 前置拦截方法,在请求达到 controller 之前进行拦截到 header ,从里面获取到 loginId。
2、然后通过 LoginContextHolder(自己定义的登录上下文对象),把 loginId 存放到 ThreadLocal 中。这个拦截器的作用就是做一些请求的前置处理。添加到 ThreadLocal 中的作用:把loginId 放到 ThreadLocal 里面,保证各线程的 loginId 互相不被其他线程干扰影响
代码:
/*** 自定义一个登录拦截器,用来拦截登录的header头信息** @author lujinhong* @since 2025-04-14*/
@Component
public class LoginInterceptor implements HandlerInterceptor {/*** 前置拦截器* 执行时机:在请求达到 controller 之前进行拦截处理,一般用于: 登录验证、权限校验、拦截*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取请求头的 loginIdString loginId = request.getHeader(SubjectConstants.LOGIN_ID);// 把 loginId 存放到上下文里面,然后上下文类LoginContextHolder,就会把loginId 放到 ThreadLocal 里面,保证各线程的 loginId 互相不被其他线程干扰影响。LoginContextHolder.set("loginId", loginId);return true;}/*** 后置拦截器:Controller 执行完毕,但视图还未渲染时执行,就是数据还没有返回给前端* 一般用于:添加模型数据、封装响应,在数据查询出来还没有返回给前端之前,我们还可以添加一些数据或者做一些操作*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {// 暂时用不到,做下解释HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);}/*** 请求完全结束后处理:一般用于:记录日志、异常处理、清资源*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除此线程局部变量的值,这里就是移除掉 loginIdLoginContextHolder.remove();}
}
4、创建登录上下文对象:【LoginContextHolder】
解释:
把loginId 放到 ThreadLocal 里面,保证各线程的 loginId 互相不被其他线程干扰影响
代码:
/*** 登录的上下文对象** ThreadLocal解释:* ThreadLocal是java.lang下面的一个类,是用来解决java多线程程序中并发问题的一种途径;* 通过为每一个线程创建一份共享变量的【副本】来保证各个线程之间的变量的访问和修改互相不影响;** ThreadLocal存放的值是线程内共享的,线程间互斥的,主要用于线程内共享一些数据,避免通过参数来传递。** 比如 共享变量num=10 , A线程拿到num,改成100;B线程拿到num,改成200,然后A线程打印num,依然是100,线程之间是互斥的** 线程内共享:比如A线程有多个方法:方法1,方法2;方法3,:方法1把num改成100,方法2把num改成200,然后方法方法3打印num=200** 存储位置:每个线程在执行时,都有一个独立的线程局部存储空间,这个空间是用于存储该线程的线程局部变量(即 ThreadLocal 的副本)的** @author lujinhong* @since 2025-04-14*/public class LoginContextHolder {// 只对当前线程有效,子线程无法访问,线程池更不行。// private static final ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal<>();// 只适合临时新建的子线程,缺点:线程池中的线程是复用的,容易导致数据泄露private static final InheritableThreadLocal<Map<String, Object>> THREAD_LOCAL = new InheritableThreadLocal<>();// 线程池场景专用: 作用:比如num=10,A线程拿到num=10,B线程拿到num后改成15,当线程切换回A线程时,A线程持有的num依然是10// 实际项目用的一定是这个TransmittableThreadLocal,不过视频演示先用InheritableThreadLocal了解一下// private static final TransmittableThreadLocal<Map<String,Object>> transmitThreadLocal = new TransmittableThreadLocal<>();/*** 将此线程局部变量的当前线程副本中的值设置为指定值。(就是会将当前线程的 ThreadLocal 变量副本的值设置为你传入的指定值)* 当前线程副本中的值是存储在“局部变量”中的,不过这个局部变量是线程局部的,即它只属于当前线程,并且由 ThreadLocal 管理,而不是普通的局部变量* 许多应用程序不需要这项功能,它们只依赖于initialValue()方法来设置线程局部变量的值*/public static void set(String key, Object value) {Map<String, Object> map = getThreadLocalMap();map.put(key, value);}/*** 返回此线程局部变量的当前线程副本中的值。如果这是线程第一次调用该方法,则创建并初始化此福副本。*/public static Object get(String key) {Map<String, Object> map = getThreadLocalMap();Object object = map.get(key);return object;}/*** 获取当前线程的局部变量的值,做判断*/public static Map<String, Object> getThreadLocalMap() {// get() : 返回此线程局部变量的当前线程副本中的值。如果这是线程第一次调用该方法,则创建并初始化此副本。Map<String, Object> map = THREAD_LOCAL.get();if (Objects.isNull(map)){// 保证线程安全map = new ConcurrentHashMap<>();THREAD_LOCAL.set(map);}return map;}/*** 移除此线程局部变量的值*/public static void remove() {THREAD_LOCAL.remove();}/*** 获取当前线程的loginId*/public static String getLoginId(){String loginId = (String)getThreadLocalMap().get("loginId");return loginId;}
}
5、简单对外的Util工具类
解释:
就是定义一个工具类,让其他服务可以调用这个方法获取loginId
代码:
6、测试
如图,随便在哪里调用该方法都可以获取到loginId。可以随时随地获取到当前登录用户的 loginId了,不需要通过 HttpServletRequest 参数来获取了。这就证明微服务之间的上下文打通了