一、链路流程
想要在微服务中实现用户上下文打通,需要以下步骤:
- 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();
}
}