用户上下文打通

594 阅读3分钟

一、链路流程

image.png

想要在微服务中实现用户上下文打通,需要以下步骤:

  • 1.前端要把satoken放到请求头中再调网关接口;(权限验证时已实现)
  • 2.通过网关拦截器LoginFilter获取tokeninfo中的loginId,存放至请求头中再放行(转发到微服务);
  • 3.微服务中添加MVC拦截器,把请求头中的loginId放到ThreadLocal,实现当前服务的上下文打通;
  • 4.然而在rpc时,调用方请求头中的内容不支持不同线程间传递,所以添加feign拦截器,把ThreadLocal中的信息添加至feign请求头中再转发。

二、网关拦截器

当前端发送请求时,会经过gateway进行token检验;然而后续微服务间进行rpc时,会直接调用nacos的进行转发,不经过gateway,此时为了方便,进行用户信息上下文打通,要在gateway的拦截器中set好username,在gateway将请求转发至nacos时,把username放到header中,微服务拦截器拿到username后放到ThreadLocalUtils中可供该微服务任意使用。

@Component
@Log4j2
public class LoginFilter implements WebFilter {

    @Override
    @SneakyThrows
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpRequest.Builder mutate = request.mutate(); //构造者模式,封装了request等所有属性
        //登录请求没有token,放行
        String url = request.getURI().getPath(); //获取请求url
        log.info("LoginFilter.filter.url:{}", url);
        if(url.equals("/auth/user/doLogin")) {
            return chain.filter(exchange); //放行到下一个filter链路
        }

        SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
        String loginId = (String) tokenInfo.getLoginId();
        if(StringUtils.isEmpty(loginId)) {
            throw new Exception("未获取到用户信息");
        }
        mutate.header("loginId", loginId);
        return chain.filter(exchange.mutate().request(mutate.build()).build()); //把变化后的属性传递往下传
        //exchange.mutate():exchange的构造者对象
    }
}

三、微服务的MVC拦截器

3.1 LoginContextHolder工具类与ThreadLocal进行原子性交互

  • ThreadLocal不能在线程中进行传递;
  • InheritableThreadLocal可在线程间传递,但不能线程复用;此处暂时使用第二种
  • TransmittableThreadLocal用于解决线程池场景下ThreadLocal传递的问题的工具类(需要引入依赖)
  • 不止存放一个loginId,所以使用map作为泛型。
public class LoginContextHolder {
    private static final InheritableThreadLocal<Map<String, Object>> THREAD_LOCAL = new InheritableThreadLocal<>();

    /**
     * set
     */
    public static void set(String key, Object value) {
        Map<String, Object> map = getThreadLocalMap();
        map.put(key, value);
        THREAD_LOCAL.set(map);
    }

    /**
     * get
     */
    public static Object get(String key) {
        Map<String, Object> threadLocalMap = getThreadLocalMap();
        return threadLocalMap.get(key);
    }

    /**
     * remove
     */
    public static void remove() {
        THREAD_LOCAL.remove();
    }

    /**
     * getLoginId
     */
    public static String getLoginId() {
        return (String) getThreadLocalMap().get("loginId");
    }

    /**
     * 当map有值,就复用map,防止每次set都把原有的map覆盖掉。
     */
    private static Map<String, Object> getThreadLocalMap() {
        Map<String, Object> map = THREAD_LOCAL.get();
        if(Objects.isNull(map)) {
            map = new ConcurrentHashMap<>();
            THREAD_LOCAL.set(map);
        }
        return map;
    }
}

3.2 创建ThreadLocalUtils,进行防腐

public class LoginUtil {

    /**
     * getLoginId
     * @return
     */
    public static String getLoginId() {
        return LoginContextHolder.getLoginId();
    }

}

3.3 自定义MVC登录拦截器

ConcurrentHashMap不能放null,所以加一层判空,避免loginId为空时报错

public class LoginInterceptor implements HandlerInterceptor {

    @Override //目标方法前执行
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String loginId = request.getHeader("loginId");
        if(StringUtils.isNotBlank(loginId)) { 
            LoginContextHolder.set("loginId", loginId);
        }
        return true;
    }

    @Override //目标方法后执行
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        LoginContextHolder.remove(); //防止内存泄漏, 每次方法执行完毕后清空ThreadLocal
    }
}

3.4 将登录拦截器放到配置类

auth微服务要放行/user/doLogin

@Configuration
public class GlobalConfig extends WebMvcConfigurationSupport {

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        //父类方法中未做任何配置,子类不需要super,可直接重写
        registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**"); //拦截所有
    }
}

四、feign拦截器

当subject微服务调用auth微服务时,调用方请求头的内容不能直接进行传递,在auth拦截器中无法从请求头中获取loginId,需要通过feign拦截器进行一次转换,把ThreadLocal中的loginId放到feign请求头中。

  • RequestContextHolder.getRequestAttributes():从当前线程的 ThreadLocal 中获取请求上下文信息,并将其作为 RequestAttributes 类型返回
  • ServletRequestAttributes 继承自 RequestAttributes,用于存储和管理与 Servlet 请求相关的属性信息。
  • requestAttributes.getRequest():获取 HttpServletRequest 对象,通过这个对象可以访问到请求的详细信息,如请求头、请求参数、请求方法、请求 URI 等。
@Component
public class FeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();

        if(Objects.nonNull(request)) {
            String loginId = request.getHeader("loginId");
            if(!StringUtils.isEmpty(loginId)) {
                requestTemplate.header("loginId", loginId);
            }
        }
    }
}

通过配置类,将自定义feign拦截器添加IOC容器中,覆盖默认RequestInterceptor拦截器

@Configuration
public class FeignConfiguration {

    @Bean
    public RequestInterceptor requestInterceptor() {
        return new FeignRequestInterceptor();
    }
}