微服务之间打通用户上下文

发布于:2025-04-16 ⋅ 阅读:(35) ⋅ 点赞:(0)

打通上下文步骤

需求:

上下文打通,获取用户登录的唯一标识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 参数来获取了。

这就证明微服务之间的上下文打通了

在这里插入图片描述