环境: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 ; } @Override public boolean supportsParameter(MethodParameter parameter) { // 参数上有TokenPrincipal注解的才会被该解析器处理 return findMethodAnnotation(TokenPrincipal.class, parameter) != null; } // 参数解析 @Override 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 ; } @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(new TokenArgumentResolver(this.usersService)) ; }}
以上就完成了所有需要的步骤及类,接下来进行测试
2.5 测试
获取用户完整信息
获取用户名username
通过自定义**HandlerMethodArgumentResolver
确实能够简化在Controller方法中获取用户信息的操作。然而,对于需要在多个组件或服务中共享用户信息的情况,结合使用ThreadLocal来保存当前用户信息仍然是最有效的策略。你可以通过过滤器或拦截器解析出用户信息存入ThreadLocal,而自定义的 HandlerMethodArgumentResolver
**从ThreadLocal中获取用户。