Spring Boot自定义注解+参数解析器构建灵活的安全认证机制

135 阅读4分钟

1. 简介

通过Spring AOP 实现权限认证,是构建安全Java应用的一种高效方式。Spring AOP允许开发者在方法执行的前、后或抛出异常时,自动执行特定的逻辑,而无需修改原有的业务代码。在权限认证的场景下,开发者可以利用AOP的拦截机制,在方法执行前检查用户是否具备相应的权限。

本篇文章将通过AOP + 自定义注解实现权限的认证,自定义参数解析器便捷的获取当前登录人的信息。

本篇文章将会应用到如下的技术点:

  • AOP
    拦截需要权限校验的方法。
  • 拦截器(Interceptor)
    拦截请求解析token,最后将其保存到当前的上下文中。
  • 自定义参数解析器(HandlerMethodArgumentResolver)
    在Controller接口参数中通过自定义的注解快捷获取当前登录人的信息。
  • SpEL表达式
    以表达式的方式获取当前登录人的具体数据。

2. 实战案例

2.1 自定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface AuthUser {
  /**从当前登录信息中获取用户信息;支持SpEL表达式*/
  String value() default "" ;
}

权限注解

复制

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuthorize {
  
  /**权限*/
  String[] value() default {} ;
  /**权限逻辑验证; 全部匹配还是部分*/
  Logical logic() default Logical.AND;
  
  public enum Logical {
    AND, OR ;
  }
}

2.2 基础类定义

解析生成Token工具类

@Component
public class JwtUtil {
  @Value("${jwt.secret}")
  private String secret ;
  @Value("${jwt.expiration}")
  private Long expiration ;
  
  private final ObjectMapper objectMapper ;
  public JwtUtil(ObjectMapper objectMapper) {
    this.objectMapper = new ObjectMapper() ;
  }
  /**生成JWT令牌*/
  public String generateToken(User user) {
    String json = null ;
    try {
      json = this.objectMapper.writeValueAsString(user);
    } catch (JsonProcessingException e) {
      throw new RuntimeException(e) ;
    }
    return createToken(json) ;
  }
  private String createToken(String payload) {
    return Jwts.builder()
        .claims().add("info", payload).subject("pack_xxxooo")
        .issuedAt(new Date(System.currentTimeMillis()))
        .expiration(new Date(System.currentTimeMillis() + expiration * 1000)).and()
        .signWith(Keys.hmacShaKeyFor(secret.getBytes())).compact() ;
  }
  /**从令牌中获取用户名*/
  public User getUser(String token) {
    Object ret = getClaimFromToken(token, claims -> claims.get("info")) ;
    User value = null;
    try {
      value = new ObjectMapper().readValue(ret.toString(), User.class);
    } catch (Exception e) {
      throw new RuntimeException(e) ;
    }
    return value ;
  }
  /**从令牌中获取声明*/
  private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
    final Claims claims = (Claims) Jwts.parser()
        .verifyWith(Keys.hmacShaKeyFor(secret.getBytes()))
        .build()
        .parse(token)
        .getPayload();
    return claimsResolver.apply(claims);
  }
}

全局异常处理

复制

@RestControllerAdvice
public class GlobalControllerAdvice {
  @ExceptionHandler(AuthException.class)
  public ResponseEntity<Object> authException(AuthException e) {
    return ResponseEntity.ok(Map.of("code", -1, "message", e.getMessage())) ;
  }
}

安全上下文对象

public class SecurityContext {
  private static final ThreadLocal<User> context = new ThreadLocal<>();
  public static void setUser(User user) {
    context.set(user);
  }
  public static User getUser() {
    return context.get();
  }
  public static void clear() {
    context.remove();
  }
}

登录成功的用户信息将保存到当前线程上下文中。

2.3 核心组件实现

请求拦截器定义,所有被拦截的请求都会进行token的解析。

@Component
public class AuthInterceptor implements HandlerInterceptor {
  private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$",
      Pattern.CASE_INSENSITIVE);
  
  private final JwtUtil jwtUtil ;
  private final ObjectMapper objectMapper ;
  public AuthInterceptor(JwtUtil jwtUtil, ObjectMapper objectMapper) {
    this.jwtUtil = jwtUtil;
    this.objectMapper = objectMapper ;
  }
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String authorization = request.getHeader(HttpHeaders.AUTHORIZATION) ;
    if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
      error(response, "缺失token") ;
      return false ;
    }
    Matcher matcher = authorizationPattern.matcher(authorization);
    if (!matcher.matches()) {
      error(response, "无效token") ;
      return false ;
    }
    String token = matcher.group("token") ;
    User user = parseToken(token);
    if (user == null) {
      error(response, "登录无效,重新登录") ;
      return false ;
    }
    SecurityContext.setUser(user);
    return true;
  }
  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    SecurityContext.clear();
  }
  private void error(HttpServletResponse response, String message) throws Exception {
    response.setContentType("application/json;charset=utf-8") ;
    response.getWriter().print(this.objectMapper.writeValueAsString(Map.of("code", -1, "message", message))) ;
  }
  
  /**通过token,解析获取User信息*/
  private User parseToken(String token) {
    return this.jwtUtil.getUser(token) ;
  }
}

如果认证没有通过,那么将直接在该拦截器中返回错误信息。

切面定义

该切面中将拦截所有使用了@PreAuthorize注解的方法,进行权限的验证。

@Aspect
@Component
public class PermissionAspect {
  @Around("@annotation(authority)")
  public Object checkPermission(ProceedingJoinPoint joinPoint, PreAuthorize authority) throws Throwable {
    User user = SecurityContext.getUser();
    if (user == null) {
      throw new AuthException("请登录") ;
    }
    Set<String> requiredPerms = Set.of(authority.value()) ;
    Set<String> userPerms = user.getPermissions() ;
    boolean hasPermission = checkLogic(requiredPerms, userPerms, authority.logic());
    if (!hasPermission) {
      throw new AuthException("没有权限");
    }
    return joinPoint.proceed();
  }
  private boolean checkLogic(Set<String> required, Set<String> has, Logical logic) {
    if (Logical.AND == logic) {
      return has.containsAll(required);
    } else {
      return !Collections.disjoint(required, has);
    }
  }
}

权限验证中,会根据配置的logic属性逻辑(AND,OR)进行不同的验证;要么全部匹配,要么有任何一个匹配的都算成功。

完成以上核心组件后,接下来就需要进行配置:

复制

@Configuration
public class WebConfig implements WebMvcConfigurer {
  private final AuthInterceptor authInterceptor ;
  public WebConfig(AuthInterceptor authInterceptor) {
    this.authInterceptor = authInterceptor;
  }
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(this.authInterceptor).addPathPatterns("/users/**") ;
  }
}

接下来,我们就可以进行测试

2.4 测试

@RestController
public class LoginController {
  
  private final JwtUtil jwtUtil ;
  public LoginController(JwtUtil jwtUtil) {
    this.jwtUtil = jwtUtil;
  }
  @GetMapping("/login")
  public ResponseEntity<Object> login(String username) {
    User user = new User() ;
    user.setId(1L) ;
    user.setName("pack") ;
    user.setIdCard("100819883") ;
    user.setUsername(username) ;
    user.setPermissions(Set.of("C", "R", "U", "D")) ;
    return ResponseEntity.ok(Map.of("token", this.jwtUtil.generateToken(user))) ;
  }
}

简单的登录接口。

需要权限认证的接口定义如下:

@RestController
@RequestMapping("/users")
public class UserController {
  @GetMapping("")
  public ResponseEntity<Object> query() {
    return ResponseEntity.ok("查询用户") ;
  }
  
  @GetMapping("/create")
  @PreAuthorize(value = {"C", "X"}, logic = Logical.OR)
  public ResponseEntity<Object> create() {
    return ResponseEntity.ok("创建成功") ;
  }
}

通过postman测试。

2.5 自定义参数解析器

public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver {
  private final ExpressionParser parser = new SpelExpressionParser();
  
  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    return parameter.hasParameterAnnotation(AuthUser.class) ;
  }
  @Override
  public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    AuthUser authUser = parameter.getParameterAnnotation(AuthUser.class) ;
    String value = authUser.value() ;
    if (!StringUtils.hasLength(value)) {
      User user = SecurityContext.getUser() ;
      if (parameter.getParameterType().isAssignableFrom(user.getClass())) {
        return user ;
      }
      return null ; 
    }
    EvaluationContext context = createEvaluationContext() ;
    return parser.parseExpression(value).getValue(context, parameter.getParameterType()) ;
  }
  
  private EvaluationContext createEvaluationContext() {
    StandardEvaluationContext context = new StandardEvaluationContext(SecurityContext.getUser()) ;
    return context ;
  }
}

该参数解析器将会处理Controller接口参数上有@AuthUser注解的参数。

注册上面的解析器

@Configuration
public class WebConfig implements WebMvcConfigurer {  
  @Override
  public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
    resolvers.add(new AuthUserArgumentResolver()) ;
  }
}

下面就可以通过如下接口进行测试了。

@GetMapping("/info")
public ResponseEntity<Object> info(@AuthUser User user) {
  return ResponseEntity.ok(user) ;
}
@GetMapping("/name")
public ResponseEntity<Object> info(@AuthUser("name") String name) {
  return ResponseEntity.ok(name) ;
}