前置声明
当前前后端分离开发项目中,后端某个请求向具体某个数据库中的多个表插入数据时,经常需要使用到当前登录用户的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 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 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@Componentpublic class WmTokenInterceptor implements HandlerInterceptor {@Overridepublic 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;}@Overridepublic 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中的变量进行及时清除。防止内存溢出。
参考
苍穹外卖
黑马头条