前置声明
当前前后端分离开发项目中,后端某个请求向具体某个数据库中的多个表插入数据时,经常需要使用到当前登录用户的Id(唯一标识)。在当前用户线程下以实现变量共享,同时为了避免不同用户线程之间操作变量的影响,往往选择ThreadLocal本地线程变量,实现线程内变量的共享读取,同时避免不同线程间操作的影响。
产生第一个问题?我们如何获取用户的线程Id并将其存入到本地线程变量ThreadLocal中?
http连接是无状态的,当前请求不会共享上一次请求的数据,所以需要结果Cookie、Session或Token一起使用,同时Token结合JWT令牌尝尝用于用户的登录校验,只有通过验证的连接才可以执行后续请求,没有通过登录验证的连接直接拒绝本次请求,提醒用户进行登录授权。在用户首次登陆时服务端会利用JWT令牌技术生成Token,并将其一起返回给用户,随后用户访问服务端时携带第一次生成的Token,服务端可以实现用户登陆校验。在生成Token的同时可以在payLoad中加入用户Id信息,实现每次连接服务端解析Token获取当前登陆用户Id的目的。
单体服务(苍穹外卖)如何获取用户Id
实现原理
苍穹外卖作为一个单体项目,整个项目内只有一个启动类,所有可以认为一个连接就是一个线程,项目结构如下:
在单体项目中获取用户Id并将其存入ThreadLocal变量中可以在项目拦截器中的以实现,具体流程如下:
在生成JWT Token的过程中返给UserId,在拦截请求验证过程中解析Token获取用户Id。
实现步骤
- 创建ThreadLocal类,主要功能包括设置UerId,获取UserId,清除UserId。
public class BaseContext {
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
/**
* 设置UserId值
* @param id
*/
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
/**
* 获取UserId值
* @return
*/
public static Long getCurrentId() {
return threadLocal.get();
}
/**
* 请求局部变量ThreadLocal中的UserId,防止内存溢出
*/
public static void removeCurrentId() {
threadLocal.remove();
}
}
- 在拦截器中解析Token获取用户Id,并将其存入上面创建的BaseContext类中。
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getUserTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
log.info("当前员工id:{}", userId);
// 将当前登陆ID放入ThreadLocal中
BaseContext.setCurrentId(userId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
- 上述代码块中主要使用的如下:直接调用静态方法setCurrentId将UserId添加到创建的ThreadLocal中。
- 此后直接从BaseContext中使用getCurrentId(...)获取当前登陆用户的Id。
微服务(黑马头条)如何获取用户Id
实现原理
相信做过黑马头条这个项目的同伴都不会陌生微服务这个概念,在黑马头条这个微服务项目中涉及到多个微服务,如:网关微服务、文章微服务、用户微服务......。各个微服务均有属于各自的启动类,各个微服务通过阿里开源nacos实现服务注册与分发,在这个项目中自媒体网关微服务和自媒体微服务通过nacos将其关联起来,自媒体相关请求到达时首先通过Nginx方向代理将其分配到自媒体网关微服务接口,对其进行登陆校验(Token解析相关工作),通过验证的请求,nacos会将其分发到自媒体微服务接口,实现后续操作。 两个微服务各自有各自的启动类,二者之间并不直接联系,对应不同的线程。如何使用TheradLocal实现共享变量的线程内部使用。
首先在自媒体网关微服务中对Token进行解析,并将其存入Header中,随后路由(重置请求)将其发送到自媒体微服务中,后续逻辑同单体服务。
实现步骤
- 构建ThreadLocal类。
public class WmThreadLocalUtil {
private static ThreadLocal<WmUser> WM_USER_THREAD_LOCAL = new ThreadLocal<>();
/**
* 设置用户信息
* @param wmUser
*/
public static void setUser(WmUser wmUser){
WM_USER_THREAD_LOCAL.set(wmUser);
}
/**
* 获取用户信息
* @return
*/
public static WmUser getUser(){
return WM_USER_THREAD_LOCAL.get();
}
/**
* 清理用户信息
*/
public static void clear(){
WM_USER_THREAD_LOCAL.remove();
}
}
- 自媒体网关微服务中对登陆进行校验并获取用户信息。
- 在自媒体微服务中配置拦截器对header进行拦截并进行存储。
@Slf4j
@Component
public class WmTokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String userId = request.getHeader("userId");
if(userId != null){
log.info("将用户信息存入到ThreadLocal中:{}", userId);
WmUser wmUser = new WmUser();
wmUser.setId(Integer.valueOf(userId));
WmThreadLocalUtil.setUser(wmUser);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
log.info("清理ThreadLocal中的用户信息");
WmThreadLocalUtil.clear();
}
}
在上面代码中重写postHandle(...)方法对ThreadLocal中的变量进行及时清除。防止内存溢出。
参考
苍穹外卖
黑马头条