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) ;
}