HandlerMethodArgumentResolver用于统一获取当前登录用户
背景与需求
- 环境: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 个核心方法:
preHandlepostHandleafterCompletion
HandlerInterceptor 常用于拦截请求事件,如用户鉴权等。此外,Spring 还提供了多种解析器(Resolver),例如用于统一处理异常的 HandlerExceptionResolver,以及本文的主角 HandlerMethodArgumentResolver。
HandlerMethodArgumentResolver 是用于处理方法参数解析的接口,包含以下 2 个核心方法:
supportsParameter:判断是否支持该参数(满足某种要求返回true,方可进入resolveArgument做参数处理)。resolveArgument:执行具体的参数解析逻辑。
知识储备已到位,接下来着手实现,主要分为三步:
- 自定义权限拦截器
AuthenticationInterceptor,拦截所有请求,将 Token 解析为currentUser并存入request中。 - 自定义参数注解
@CurrentUser,添加至 Controller 的方法参数user之上。 - 自定义方法参数解析器
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。分别在 addInterceptors 和 addArgumentResolvers 方法中,添加自定义的拦截器和参数解析器。代码如下:
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);
}
}说明
- 版本时效:本文基于 Spring Boot 2.0.4.RELEASE 编写。该版本较旧,部分 API(如
MediaType.APPLICATION_JSON_UTF8)在新版本 Spring 中已废弃。 - 配置类建议:文中继承
WebMvcConfigurationSupport会导致 Spring Boot 的 MVC 自动配置失效。在新版本 Spring Boot 中,推荐实现WebMvcConfigurer接口而非继承WebMvcConfigurationSupport类,以保留自动配置特性。 - 代码语义:文中代码逻辑保持原意,实际使用时请根据项目依赖版本调整导入包及配置方式。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。