前言
在开发中,发现一个问题:当其它模块想要获取用户信息时,例如存储一些业务数据时需要获取用户 id,都需要先从请求头上拿到 token,在调用 AuthUtil 工具类去解析,最后拿到用户信息。每个模块都这么写难免显得繁琐,并且我认为 token 这东西,让认证服务来操作就好了,其它的业务服务就专注于业务处理。所以,我考虑来封装一个用户工具类,对外提供获取用户信息的功能,并且不需要传入 token 来获取,本文就来分享一下。
工具类封装
封装工具类本身并不难,难的是我的想法是,当获取用户信息时,不要把 token 掺和进来,最好是业务代码中调用一个无参的方法,就获取到用户信息了。
这个地方我在 SpringSecurity 中受到了启发,在 SpringSecurity 中,有一个组件叫做 SecurityContextHolder,它里面就封装了用户信息,他的主要做法是在一个请求开始时,将用户信息从请求信息中解析出来,设置到一个ThreadLocal 的变量中,请求结束时,在将当前用户信息从 ThreadLocal 中清除,这样在整个请求流程中,我可以从任意位置调用SecurityContextHolder 来获取用户信息,并且多线程互不影响,我感觉这个实现方案很好,就自己学习着实现了一下。
首先自定义了一个工具类,里面就是封装了一个ThreadLocal 的 String 类型变量,用来封装用户 id,同时提供了 set、get 方法,还有一个clear 方法,在这个方法中,调用了ThreadLocal 变量的 remove 方法,这是因为ThreadLocal 应用了一个线程池,里面的线程是会被复用的,如果使用完毕后不清除,会出现线程安全问题。
/** * 自定义的上下文类,使用ThreadLocal来保存每个线程访问时的userId */ public class UserUtil { private static final ThreadLocal<String> USER_ID = new ThreadLocal<>(); public static void setUserId(String userId) { USER_ID.set(userId); } public static String getUserId() { return USER_ID.get(); } public static void clear() { USER_ID.remove(); } }
工具类封装好了,现在需要在请求开始时调用setUserId 方法,并且在请求结束时调用clear 方法,这个操作使用拦截器来做就比较合适。如下,自定义了一个拦截器,在请求开始时保存用户信息,在请求结束时清除用户信息。
/** * 拦截器:用于将请求头中的用户信息解析 封装userId到UserContext中 */ public class UserInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Token"); if (StrUtil.isNotBlank(token)) { String uid = AuthUtil.getLoginUid(token); UserUtil.setUserId(uid); } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserUtil.clear(); } }
还需要进行一些配置,注册这个拦截器,代码比较简单,贴一下吧
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserInterceptor()); } }
上述代码完成后,每个请求开始时,都会在拦截器中 封装用户信息,也会在请求结束时清除。在整个请求的任意位置,都可以调用getUserId 方法获取用户 id 了。
跨模块共享
上面的代码,展示了在一个 SpringBoot 模块中如何配置用户信息获取的工具类,但是如果在微服务项目中,我不想在每个模块中都配置一遍这些内容,所以我考虑在项目的 common 模块封装,这样其它模块仅调用即可,就算有什么改动,也只改动一处即可。
首先要做的就是将上面的代码,迁移到 common 模块中,然后其它模块中直接调用 UserUtil 的 getUserId 方法即可获取当前登录用户 id。
还有两个问题需要处理一下:
第一、 common 工程为 maven 工程,所以标注了@Configuration 注解的类是不会被扫描的,自动扫描@Configuration 是 SpringBoot 提供的自动配置特性。
这里需要在 common 工厂的 resources 目录下创建一个文件: \resources\META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports
里面配置想要被扫描的配置类路径
com.xb.blog.common.core.config.WebMvcConfig
这样这个配置类就可以被扫描到了。
第二、common 模块被所有模块依赖,common 为了实现拦截器必须引入 web 的 starter,但是这和 gateway 的 WebFlux 冲突,gateway 模块并不需要引入 web starter。所以在WebMvcConfig 中还需要添加一处配置
@Configuration @ConditionalOnClass(name = "org.springframework.web.servlet.DispatcherServlet") public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserInterceptor()); } }
使用条件装配注解@ConditionalOnClass,可实现只有模块中存在DispatcherServlet 才应用这个配置,这也是从 SpringBoot 源码中学习到的。
总结
以上,就完成了公共用户信息获取工具的封装,支持多模块共享,希望可以帮到你。