Spring Boot自定義注解+參數(shù)解析器,構(gòu)建靈活的安全認(rèn)證機(jī)制
環(huán)境:SpringBoot3.4.0
1. 簡介
通過Spring AOP 實(shí)現(xiàn)權(quán)限認(rèn)證,是構(gòu)建安全Java應(yīng)用的一種高效方式。Spring AOP允許開發(fā)者在方法執(zhí)行的前、后或拋出異常時(shí),自動執(zhí)行特定的邏輯,而無需修改原有的業(yè)務(wù)代碼。在權(quán)限認(rèn)證的場景下,開發(fā)者可以利用AOP的攔截機(jī)制,在方法執(zhí)行前檢查用戶是否具備相應(yīng)的權(quán)限。
本篇文章將通過AOP + 自定義注解實(shí)現(xiàn)權(quán)限的認(rèn)證,自定義參數(shù)解析器便捷的獲取當(dāng)前登錄人的信息。
本篇文章將會應(yīng)用到如下的技術(shù)點(diǎn):
- AOP
攔截需要權(quán)限校驗(yàn)的方法。 - 攔截器(Interceptor)
攔截請求解析token,最后將其保存到當(dāng)前的上下文中。 - 自定義參數(shù)解析器(HandlerMethodArgumentResolver)
在Controller接口參數(shù)中通過自定義的注解快捷獲取當(dāng)前登錄人的信息。 - SpEL表達(dá)式
以表達(dá)式的方式獲取當(dāng)前登錄人的具體數(shù)據(jù)。
2. 實(shí)戰(zhàn)案例
2.1 自定義注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface AuthUser {
/**從當(dāng)前登錄信息中獲取用戶信息;支持SpEL表達(dá)式*/
String value() default "" ;
}
權(quán)限注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuthorize {
/**權(quán)限*/
String[] value() default {} ;
/**權(quán)限邏輯驗(yàn)證; 全部匹配還是部分*/
Logical logic() default Logical.AND;
public enum Logical {
AND, OR ;
}
}
2.2 基礎(chǔ)類定義
解析生成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();
}
}
登錄成功的用戶信息將保存到當(dāng)前線程上下文中。
2.3 核心組件實(shí)現(xiàn)
請求攔截器定義,所有被攔截的請求都會進(jìn)行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) ;
}
}
如果認(rèn)證沒有通過,那么將直接在該攔截器中返回錯(cuò)誤信息。
切面定義
該切面中將攔截所有使用了@PreAuthorize注解的方法,進(jìn)行權(quán)限的驗(yàn)證。
@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("沒有權(quán)限");
}
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);
}
}
}
權(quán)限驗(yàn)證中,會根據(jù)配置的logic屬性邏輯(AND,OR)進(jìn)行不同的驗(yàn)證;要么全部匹配,要么有任何一個(gè)匹配的都算成功。
完成以上核心組件后,接下來就需要進(jìn)行配置:
@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/**") ;
}
}
接下來,我們就可以進(jìn)行測試
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))) ;
}
}
簡單的登錄接口。
需要權(quán)限認(rèn)證的接口定義如下:
@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("創(chuàng)建成功") ;
}
}
通過postman測試。
獲取token成功。
訪問沒有攜帶token的接口/users
header中輸入token后
調(diào)用成功。
2.5 自定義參數(shù)解析器
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 ;
}
}
該參數(shù)解析器將會處理Controller接口參數(shù)上有@AuthUser注解的參數(shù)。
注冊上面的解析器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthUserArgumentResolver()) ;
}
}
下面就可以通過如下接口進(jìn)行測試了。
@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) ;
}
上面2個(gè)接口測試結(jié)果如下:
圖片
圖片
成功獲取登錄信息。