SpringBoot通过拦截器和注解方式实现登录用户信息的统一获取

917 阅读1分钟

前言

在开发系统过程中总会碰到需要根据前段上送的token之类的凭证信息获取当前登录用户信息的场景,同时这些场景中获取用户信息的操作都是相同的,因此我们可以通过拦截器的方式在真正进入执行方法之前就将用户信息准备好,这样就不用每个开发人员都在自己的业务代码中添加获取当前登录用户信息的操作了,而是直接将拦截器中的当前用户信息直接拿来用就可以了

实现方式

存储用户信息的方式有两种:

  1. 将用户信息放入ThreadLocal中
  2. 方法参数中增加一个当前用户信息的参数,通过自定义参数解析器(HandlerMethodArgumentResolver)给此参数赋值

下面我们来看一下这两种方式的具体实现

  • ThreadLocal方式,此方式具体实现如下
  1. 注解类,只有运行加了此注解的方法时才会去获取当前登录用户信息,避免了不需要也去查询造成的资源浪费
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurrentUser {
}
  1. holder,用来将用户信息存储到ThreadLocal中
public class UserInfoHolder {

    private static final ThreadLocal<UserInfo> HOLDER = new ThreadLocal<>();

    public static void set(UserInfo userInfo){
        HOLDER.set(userInfo);
    }

    public static UserInfo get(){
        return HOLDER.get();
    }

    public static void remove(){
        HOLDER.remove();
    }

}
  1. 拦截器
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {
        String token = httpServletRequest.getParameter("token");
        //判断入参中的handler是否是HandlerMethod以及此方法上是否有CurrentUser注解
        if(handler instanceof HandlerMethod && hasCurrentUserAnnotation((HandlerMethod) handler)){
            //根据token获取当前用户信息
            UserInfo userInfo = new UserInfo();
            //放入到ThreadLocal中,一定要在afterCompletion方法中将ThreadLocal中的UserInfo数据删除,不然会造成内存泄漏,最终导致OOM
            UserInfoHolder.set(userInfo);
        }
        return false;
    }

    private boolean hasCurrentUserAnnotation(HandlerMethod handlerMethod){
        CurrentUser currentUser = handlerMethod.getMethodAnnotation(CurrentUser.class);
        if(currentUser!=null){
            return true;
        }else{
            return false;
        }
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
        UserInfoHolder.remove();
    }
}
  1. 具体方法中使用
public class XxxController {
    @CurrentUser
    public void xxxx(){
        UserInfo userInfo = UserInfoHolder.get();
        //具体业务代码
    }
    
}
  • 参数解析器方式
  1. 注解类,只有运行加了此注解的方法时才会去获取当前登录用户信息,避免了不需要也去查询造成的资源浪费
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurrentUser {
}
  1. 定义参数解析器
public class CurrentUserMethodArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.hasParameterAnnotation(CurrentUser.class)
                &&methodParameter.getParameterType()== UserInfo.class;
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        HttpServletRequest nativeRequest = (HttpServletRequest) nativeWebRequest.getNativeRequest();
        //注意此处名称应该与拦截器中set的参数名相同
        return nativeRequest.getAttribute("currentUser");
    }
}
  1. 定义拦截器
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {
        String token = httpServletRequest.getParameter("token");
        //判断入参中的handler是否是HandlerMethod以及此方法上是否有CurrentUser注解
        if(handler instanceof HandlerMethod && hasCurrentUserAnnotation((HandlerMethod) handler)){
            //根据token获取当前用户信息
            UserInfo userInfo = new UserInfo();
            //将用户信息放入request中
            //注意此处的属性名要与参数解析器中get的相同
            httpServletRequest.setAttribute("currentUser",userInfo);
        }
        return false;
    }

    private boolean hasCurrentUserAnnotation(HandlerMethod handlerMethod){
        CurrentUser currentUser = handlerMethod.getMethodAnnotation(CurrentUser.class);
        if(currentUser!=null){
            return true;
        }else{
            return false;
        }
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
        
    }
}
  1. 配置拦截器和参数解析器
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Bean
    public LoginInterceptor loginInterceptor(){
        return new LoginInterceptor();
    }
    
    @Bean
    public CurrentUserMethodArgumentResolver currentUserMethodArgumentResolver(){
        return new CurrentUserMethodArgumentResolver();
    }

    @Override
    public void addInterceptors(InterceptorRegistry interceptorRegistry) {
        interceptorRegistry.addInterceptor(loginInterceptor()).addPathPatterns("/**");
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> list) {
        list.add(currentUserMethodArgumentResolver());
    }
}
  1. 使用
public class XxxController {

    //拦截器和参数解析器会自动将用户信息赋值给userInfo参数,编码中可直接使用
    public void xxxx(@CurrentUser UserInfo userInfo){
        UserInfo userInfo = UserInfoHolder.get();
        //具体业务代码
    }
    
}