环境:SpringBoot3.2.5
1. 简介
在前后端分离的应用中,JWT token作为用户身份验证的关键手段,通常需要在后端解析以获取用户信息。虽然使用过滤器或拦截器结合ThreadLocal可以方便地保存和获取用户信息,但在某些场景下,我们可能希望直接在Controller的参数中直接获取这些信息,以减少代码的冗余和复杂性。
为此,我们可以利用Spring框架提供的自定义HandlerMethodArgumentResolver功能。通过实现这一接口,在控制器方法执行前自动从请求中解析JWT token,并将提取的用户信息作为参数直接传递给控制器方法。这种方式不仅简化了代码,还提高了代码的可读性和可维护性。接下来我将详细的介绍自定义HandlerMethodArguemntResolver的使用。
2. 实战案例
2.1 准备环境
public class Users {private String id ;/**用户名*/private String username ;/**密码*/private String password ;/**身份证*/private String idNo ;// getters, setters}
一个简单的用户实体对象,接下来写Service,该Service提供简单的登录和查询功能
@Servicepublic class UsersService {// 内存用户private static final List<Users> USERS = List.of(new Users("1", "admin", "123123", "111111"),new Users("2", "guest", "123456", "222222"));// 密钥private static final String SECRET = "aaaabbbbccccdddd" ;// 登录public String login(String username, String password) {Optional<Users> optionalUser = USERS.stream().filter(user -> user.getUsername().equals(username) && user.getPassword().equals(password)).findFirst() ;if (optionalUser.isPresent()) {Users user = optionalUser.get() ;Map<String, Object> claims = new HashMap<>();claims.put("id", user.getId()) ;String token = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, SECRET).compact() ;return token ;}return null ;}// 从当前的请求中获取token,接着查找对应的用户信息public Users getUser() {HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest() ;String token = request.getHeader(HttpHeaders.AUTHORIZATION) ;token = token.replace("Bearer ", "") ;Claims body = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody() ;String id = (String) body.get("id") ;return USERS.stream().filter(user -> id.equals(user.getId())).findFirst().orElseGet(() -> null) ;}}
上面的Service非常的简单,验证用户 / 生成token / 根据token查询用户。
2.2 定义Controller接口
@RestController@RequestMapping("/users")public class UsersController {private final UsersService usersService ;public UsersController(UsersService usersService) {this.usersService = usersService ;}@GetMapping("/login")public String login(String username, String password) {return this.usersService.login(username, password) ;}}
该Controller目前只有一个登录方法,先验证能正确登录&返回token。

接口正常返回了Token信息。接下来就是重点了,如何根据每次请求中携带的该token获取对应的用户信息!
这里我期望的是在Controller接口参数上能够通过一个注解就能读取到当前登录的用户信息,而如果只是想获取某个字段值比如id,idNo,那么通过某种表达式能自动的从当前用户中解析获取,如下接口形式:
// 获取当前用户的完整信息@GetMapping("get")public Users get(@TokenPrincipal Users user) {return user ;}// 获取当前用户的username信息@GetMapping("username")public String username(@TokenPrincipal(expression = "username") String username) {return username ;}
要实现上面的方式,我们就只能通过自定义HandlerMethodArgumentResolver来实现参数的解析。
2.3 自定义注解
@Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface TokenPrincipal {String expression() default "";}
该注解用于方法参数或者注解类上,同时定义了expression属性,该属性用来设置SpEL表达式。在Spring中通过SpEL表达式能非常方法的进行属性,方法,Bean对象的访问。如需要深入学习SpEL表达式,请查看下面这篇文章
玩转Spring表达式语言SpEL:在项目实践中与AOP的巧妙结合
2.4 自定义参数解析器
public class TokenArgumentResolver implements HandlerMethodArgumentResolver {private ExpressionParser parser = new SpelExpressionParser();private final UsersService usersService ;public TokenArgumentResolver(UsersService usersService) {this.usersService = usersService ;}public boolean supportsParameter(MethodParameter parameter) {// 参数上有TokenPrincipal注解的才会被该解析器处理return findMethodAnnotation(TokenPrincipal.class, parameter) != null;}// 参数解析public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {// 1.获取用户Object principal = this.usersService.getUser() ;if (principal == null) {return null ;}TokenPrincipal annotation = findMethodAnnotation(TokenPrincipal.class, parameter) ;String expressionToParse = annotation.expression() ;// 2.如果设置了表达式则进行SpEL配置if (StringUtils.hasLength(expressionToParse)) {StandardEvaluationContext context = new StandardEvaluationContext();context.setRootObject(principal) ;Expression expression = this.parser.parseExpression(expressionToParse) ;principal = expression.getValue(context) ;}// 3.判断类型是否相同if (principal != null && !ClassUtils.isAssignable(parameter.getParameterType(), principal.getClass())) {return null ;}return principal ;}private <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass, MethodParameter parameter) {// 这里就是查找当前参数上是否有TokenPrincipal注解}}
该参数解析器还是比较简单的,只处理那些参数上添加了@TokenPrincipal注解的
接下来就要将该解析器注册到参数解析器集合中
@Componentpublic class TokenWebMvcConfig implements WebMvcConfigurer {private final UsersService usersService ;public TokenWebMvcConfig(UsersService usersService) {this.usersService = usersService ;}@Overridepublic void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {resolvers.add(new TokenArgumentResolver(this.usersService)) ;}}
以上就完成了所有需要的步骤及类,接下来进行测试
2.5 测试
获取用户完整信息

获取用户名username

通过自定义HandlerMethodArgumentResolver确实能够简化在Controller方法中获取用户信息的操作。然而,对于需要在多个组件或服务中共享用户信息的情况,结合使用ThreadLocal来保存当前用户信息仍然是最有效的策略。你可以通过过滤器或拦截器解析出用户信息存入ThreadLocal,而自定义的HandlerMethodArgumentResolver从ThreadLocal中获取用户。
以上是本篇文章的全部内容,如对你有帮助帮忙点赞+转发+收藏
推荐文章

