微服务之间打通用户上下文
打通上下文步骤
需求:
上下文打通,获取用户登录的唯一标识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放到请求头中
*/
@Override
public 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")) {
// 如果当前请求是登录,直接放行,不拦截,只有登录后,用户才会有loginId
return 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放一个 loginId
mutate.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 之前进行拦截处理,一般用于: 登录验证、权限校验、拦截
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取请求头的 loginId
String loginId = request.getHeader(SubjectConstants.LOGIN_ID);
// 把 loginId 存放到上下文里面,然后上下文类LoginContextHolder,就会把loginId 放到 ThreadLocal 里面,保证各线程的 loginId 互相不被其他线程干扰影响。
LoginContextHolder.set("loginId", loginId);
return true;
}
/**
* 后置拦截器:Controller 执行完毕,但视图还未渲染时执行,就是数据还没有返回给前端
* 一般用于:添加模型数据、封装响应,在数据查询出来还没有返回给前端之前,我们还可以添加一些数据或者做一些操作
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 暂时用不到,做下解释
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
/**
* 请求完全结束后处理:一般用于:记录日志、异常处理、清资源
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除此线程局部变量的值,这里就是移除掉 loginId
LoginContextHolder.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 参数来获取了。
这就证明微服务之间的上下文打通了