51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

SpringBoot中Controller接口参数这样处理太优雅了

环境: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中获取用户。

赞(3)
未经允许不得转载:工具盒子 » SpringBoot中Controller接口参数这样处理太优雅了