说明:在微服务架构中,用户身份经网关验证后,我们可以将用户信息,如ID加入到请求头上。后面的微服务中,可以设置一个拦截器,拦截请求,获取请求头上的用户ID,加入到ThreadLocal中。
最重要的是,在当前微服务发起请求,调用其他微服务时,需要将发出去的请求再进行拦截,把本地线程池中的用户ID再次放入到请求头中,这样就实现了用户信息在微服务中的流转,称为参数透传。本文介绍,上述参数透传的实现。
在微服务系统中,一般都会有一个共同模块,是所有微服务共同引用的模块,比如模块名为Base、Util、Feign-API,我们可以把参数透传的拦截器写在这个模块里面,这样其他微服务就不用再写拦截器的配置类了。
比如,我当前的这个系统,Feign-API模块是所有微服务共同引用的。里面写了一些共同的DTO对象,以及Feign-Client类对象;
实现
第一步:创建ThreadLocal类
在FeignAPI模块中,创建一个本地线程池对象,用于存储当前用户的ID;
/**
* 本地线程池
*/
public class TokenThreadLocal {
private static final ThreadLocal<Long> THREAD_LOCAL= new ThreadLocal<>();
/**
* 设置
* @param tokenId
*/
public static void set(Long tokenId){
THREAD_LOCAL.set(tokenId);
}
/**
* 获取
* @return
*/
public static Long get(){
return THREAD_LOCAL.get();
}
/**
* 移除
*/
public static void remove(){
THREAD_LOCAL.remove();
}
}
第二步:创建MVC拦截器
在FeignAPI模块中,创建MVC拦截器(HandlerInterceptor),拦截路径为所有路径;
package com.hzy.interceptor;
import com.hzy.config.TokenThreadLocal;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 接收请求拦截器
*/
public class AuthorizationInterceptor implements HandlerInterceptor {
/**
* 收到请求会执行的方法
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取请求头中的用户ID
String id = request.getHeader("authorization");
// 判断用户ID是否为空
if (id != null && id != ""){
// 将字符串转换为Long类型
long l = Long.parseLong(id);
// 将用户ID存入ThreadLocal中
TokenThreadLocal.set(l);
}else {
responseHandler(response);
return false;
}
// 放行
return true;
}
/**
* 请求返回客户端之前会执行的方法
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
/**
* 请求到客户端之后会执行的方法
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
TokenThreadLocal.remove();
}
/**
* 用户尚未登录的返回信息
* @param response
* @return
* @throws IOException
*/
private boolean responseHandler(HttpServletResponse response) throws IOException {
// 设置响应状态码 401 如果用户未登录 状态码都返回401
response.setStatus(HttpStatus.UNAUTHORIZED.value());
// 响应的数据类型和编码格式
response.setContentType("application/json;charset=UTF-8");
// 提示信息
response.getWriter().write("用户未登录");
return false;
}
}
拦截器配置类,设置为所有路径请求都拦截;
import com.hzy.interceptor.AuthorizationInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 接收请求拦截器配置类
*/
public class MVCInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthorizationInterceptor())
.addPathPatterns("/**");
}
}
第三步:创建Feign拦截器
创建Feign的拦截器(RequestInterceptor),该拦截器用于拦截当前微服务发出去的请求,代码内容写把当前本地线程池的用户ID再次放入到请求头中,传给下游微服务解析;
import com.hzy.config.TokenThreadLocal;
import feign.RequestInterceptor;
import feign.RequestTemplate;
/**
* 发送请求拦截器
*/
public class AuthorizationRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
// 从本地线程池中获取用户ID,放入请求头中
requestTemplate.header("authorization",TokenThreadLocal.get().toString());
}
}
测试
测试订单服务,生成订单中,会调用库存服务,查询库存信息。在MVC、Feign拦截器、调用库存服务行分别打断点,发送请求,查看执行过程;
请求通过网关,先被订单服务的MCV拦截器拦截到,可以看到当前用户的ID是2,放行……
然后,断点来到订单服务的,调用库存服务这里,放行,查看发出去的请求会不会被拦截住;
断点被Feign拦截器(RequestInterceptor)拦住,将参数放入到请求头中,放行……
请求又来到了MVC拦截器,注意哦,这里不是订单服务,是库存服务的拦截器;
这样,就实现了用户参数在微服务系统中的流转。
总结
使用一前一后两个拦截器,对微服务接受请求前,发送请求前分别进行拦截,实现参数透传。但是需要注意以下两点:
-
两个拦截器所依赖的是本地线程池,所以如果微服务发出的是异步消息(如MQ),就行不通(参考:使用Feign出现空指针异常);
-
拦截器是写在共同模块中的,所以参数得以流转的前提是当前微服务引用了共同模块;