切换语言为:繁体

SpringCloud 微服务实现统一封装用户信息,代码任意位置随意获取

  • 爱糖宝
  • 2024-08-21
  • 2048
  • 0
  • 0

前言

在开发中,发现一个问题:当其它模块想要获取用户信息时,例如存储一些业务数据时需要获取用户 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 源码中学习到的。

总结

以上,就完成了公共用户信息获取工具的封装,支持多模块共享,希望可以帮到你。

0条评论

您的电子邮件等信息不会被公开,以下所有项均必填

OK! You can skip this field.