背景与需求

  • 环境:Spring Boot 2.0.4.RELEASE
  • 需求:多个 Controller 方法需要在请求入口获取当前登录用户信息,以便进行后续业务操作。
  • 准备工作:前端每次请求携带 Token,后端封装工具方法 tokenUtils.getUserByToken(token),用于解析 Token 获取当前用户信息 (CurrentUserInfo)。

这是一个常见的业务场景,为实现该需求,通常有以下几种解决方案。

目录


一、最原始直接

即在每个 Controller 方法开始时,手动调用 tokenUtils.getUserByToken(token)。这种方式代码重复度高,不够优雅,维护成本较大。

二、AOP

使用 AOP(Aspect-Oriented Programming)可以解决很多切面类问题,例如将 currentUser 放入 request 中。但相比拦截器方案,AOP 的配置稍显厚重,且在某些场景下灵活性略低。

三、拦截器 + 方法参数解析器

使用 Spring MVC 拦截器 (HandlerInterceptor) 配合 方法参数解析器 (HandlerMethodArgumentResolver) 是最合适的方案。

Spring MVC 提供了 HandlerInterceptor 接口,包含以下 3 个核心方法:

  • preHandle
  • postHandle
  • afterCompletion

HandlerInterceptor 常用于拦截请求事件,如用户鉴权等。此外,Spring 还提供了多种解析器(Resolver),例如用于统一处理异常的 HandlerExceptionResolver,以及本文的主角 HandlerMethodArgumentResolver

HandlerMethodArgumentResolver 是用于处理方法参数解析的接口,包含以下 2 个核心方法:

  • supportsParameter:判断是否支持该参数(满足某种要求返回 true,方可进入 resolveArgument 做参数处理)。
  • resolveArgument:执行具体的参数解析逻辑。

知识储备已到位,接下来着手实现,主要分为三步:

  1. 自定义权限拦截器 AuthenticationInterceptor,拦截所有请求,将 Token 解析为 currentUser 并存入 request 中。
  2. 自定义参数注解 @CurrentUser,添加至 Controller 的方法参数 user 之上。
  3. 自定义方法参数解析器 CurrentUserMethodArgumentResolver,从 request 中取出 user,并赋值给添加了 @CurrentUser 注解的参数。

3.1 自定义权限拦截器

自定义权限拦截器 AuthenticationInterceptor,需实现 HandlerInterceptor 接口。在 preHandle 方法中调用 tokenUtils.getUserByToken(token) 获取当前用户,最后存入 request 属性中。代码如下:

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import edp.core.utils.TokenUtils;
import edp.core.consts.Consts;
import edp.davinci.model.User;

public class AuthenticationInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenUtils tokenUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("Authorization");
        User user = tokenUtils.getUserByToken(token);
        request.setAttribute(Consts.CURRENT_USER, user);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }
}

3.2 自定义参数注解

自定义方法参数注解 @CurrentUser。被该注解修饰的参数值,将由方法参数解析器 CurrentUserMethodArgumentResolver 进行“注入”。代码如下:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义当前用户注解
 * 注解参数
 * 此注解在验证 token 通过后,获取当前 token 包含用户
 */
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}

在各 Controller 类的 RequestMapping 方法参数上添加此注解,示例如下:

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import edp.davinci.core.common.Constants;
import edp.core.annotation.CurrentUser;
import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping(value = Constants.BASE_API_PATH + "/download")
public class DownloadController {

    @GetMapping(value = "/page", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public ResponseEntity getDownloadRecordPage(@CurrentUser User user, HttpServletRequest request) {
        ...
    }
}

3.3 自定义方法参数解析器

自定义方法参数解析器 CurrentUserMethodArgumentResolver,需实现 HandlerMethodArgumentResolver 接口。代码如下:

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import edp.core.annotation.CurrentUser;
import edp.core.consts.Consts;
import edp.davinci.model.User;

/**
 * @CurrentUser 注解 解析器
 */
public class CurrentUserMethodArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().isAssignableFrom(User.class)
                && parameter.hasParameterAnnotation(CurrentUser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        return  (User) webRequest.getAttribute(Consts.CURRENT_USER, RequestAttributes.SCOPE_REQUEST);
    }
}

众所周知,在传统的 Spring MVC 项目中,拦截器定义好后需要在 springmvc.xml 配置文件中注册。而在 Spring Boot 中,省去了许多 XML 配置,取而代之的是使用 @Configuration 标识的配置类。对应 Spring MVC 配置的类通常需继承 WebMvcConfigurationSupport。同理,解析器定义好后,也需被添加到 Spring MVC 的配置类中。

3.4 配置 MVC

定义 MVC 配置类,需继承 WebMvcConfigurationSupport。分别在 addInterceptorsaddArgumentResolvers 方法中,添加自定义的拦截器和参数解析器。代码如下:

import static edp.core.consts.Consts.EMPTY;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.serializer.ValueFilter;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;

import edp.davinci.core.common.Constants;
import edp.davinci.core.inteceptor.AuthenticationInterceptor;
import edp.davinci.core.inteceptor.CurrentUserMethodArgumentResolver;


@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

    @Value("${file.userfiles-path}")
    private String filePath;

    /**
     * 登录校验拦截器
     *
     * @return
     */
    @Bean
    public AuthenticationInterceptor loginRequiredInterceptor() {
        return new AuthenticationInterceptor();
    }

    /**
     * CurrentUser 注解参数解析器
     *
     * @return
     */
    @Bean
    public CurrentUserMethodArgumentResolver currentUserMethodArgumentResolver() {
        return new CurrentUserMethodArgumentResolver();
    }

    /**
     * 参数解析器
     *
     * @param argumentResolvers
     */
    @Override
    protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(currentUserMethodArgumentResolver());
        super.addArgumentResolvers(argumentResolvers);
    }

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginRequiredInterceptor())
                .addPathPatterns(Constants.BASE_API_PATH + "/**")
                .excludePathPatterns(Constants.BASE_API_PATH + "/login");
        super.addInterceptors(registry);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/META-INF/resources/")
                .addResourceLocations("classpath:/static/page/")
                .addResourceLocations("classpath:/static/templates/")
                .addResourceLocations("file:" + filePath);
    }

    @Override
    protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(SerializerFeature.QuoteFieldNames,
                SerializerFeature.WriteEnumUsingToString,
                SerializerFeature.WriteMapNullValue,
                SerializerFeature.WriteDateUseDateFormat,
                SerializerFeature.DisableCircularReferenceDetect);
        fastJsonConfig.setSerializeFilters((ValueFilter) (o, s, source) -> {
            if (null != source && (source instanceof Long || source instanceof BigInteger) && source.toString().length() > 15) {
                return source.toString();
            } else {
                return null == source ? EMPTY : source;
            }
        });

        // 处理中文乱码问题
        List<MediaType> fastMediaTypes = new ArrayList<>();
        fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
        fastConverter.setSupportedMediaTypes(fastMediaTypes);
        fastConverter.setFastJsonConfig(fastJsonConfig);
        converters.add(fastConverter);
    }
}

说明

  1. 版本时效:本文基于 Spring Boot 2.0.4.RELEASE 编写。该版本较旧,部分 API(如 MediaType.APPLICATION_JSON_UTF8)在新版本 Spring 中已废弃。
  2. 配置类建议:文中继承 WebMvcConfigurationSupport 会导致 Spring Boot 的 MVC 自动配置失效。在新版本 Spring Boot 中,推荐实现 WebMvcConfigurer 接口而非继承 WebMvcConfigurationSupport 类,以保留自动配置特性。
  3. 代码语义:文中代码逻辑保持原意,实际使用时请根据项目依赖版本调整导入包及配置方式。