靈活!Spring Boot 自定義注解結(jié)合參數(shù)解析器實現(xiàn)權(quán)限控制
在現(xiàn)代 Web 應(yīng)用開發(fā)中,權(quán)限控制是至關(guān)重要的一個環(huán)節(jié),尤其是在微服務(wù)架構(gòu)和前后端分離的模式下。如何在保證安全性的同時,兼顧開發(fā)的便捷性和代碼的可讀性,是開發(fā)者需要重點關(guān)注的問題。
Spring Boot 提供了多種方式來實現(xiàn)權(quán)限管理,例如 Spring Security,但在某些場景下,我們希望有更輕量級、靈活的權(quán)限控制方案。本篇文章將介紹如何通過 Spring Boot 3.4 中的 自定義注解 結(jié)合 AOP 和 參數(shù)解析器,實現(xiàn)一套可擴展的權(quán)限控制方案。
本方案的核心思路是:
- 通過 AOP 攔截
需要權(quán)限校驗的方法,實現(xiàn)全局權(quán)限校驗邏輯。 - 通過攔截器(Interceptor)
解析請求 Token,并將用戶信息存儲到上下文中,方便后續(xù)權(quán)限校驗使用。 - 使用自定義參數(shù)解析器(HandlerMethodArgumentResolver)
在 Controller 方法參數(shù)中,通過注解直接獲取當(dāng)前登錄用戶信息,提高代碼的可讀性。 - 支持 SpEL(Spring 表達式語言)
以動態(tài)方式獲取用戶數(shù)據(jù),實現(xiàn)更靈活的權(quán)限控制。
本文將結(jié)合代碼示例,詳細介紹該方案的實現(xiàn)方式,并最終構(gòu)建一套完整的權(quán)限校驗框架。
自定義注解
獲取當(dāng)前用戶信息的注解
package com.icoderoad.auth;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface AuthUser {
/** 通過 SpEL 表達式從當(dāng)前登錄用戶信息中提取數(shù)據(jù) */
String value() default "";
}
權(quán)限控制注解
package com.icoderoad.auth;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuthorize {
/** 需要的權(quán)限 */
String[] value() default {};
/** 權(quán)限校驗邏輯(全部匹配或部分匹配) */
Logical logic() default Logical.AND;
enum Logical {
AND, OR;
}
}
核心組件實現(xiàn)
生成 Token 的工具類
package com.icoderoad.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.function.Function;
@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 = objectMapper;
}
/** 生成 JWT 令牌 */
public String generateToken(User user) {
try {
String payload = objectMapper.writeValueAsString(user);
return createToken(payload);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
private String createToken(String payload) {
return Jwts.builder()
.claim("info", payload)
.subject("auth_token")
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(Keys.hmacShaKeyFor(secret.getBytes()))
.compact();
}
/** 解析 Token 獲取用戶信息 */
public User getUser(String token) {
try {
String info = (String) getClaimFromToken(token, claims -> claims.get("info"));
return objectMapper.readValue(info, User.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
.build()
.parseClaimsJws(token)
.getBody();
return claimsResolver.apply(claims);
}
}
認證攔截器
package com.icoderoad.interceptor;
import com.icoderoad.utils.JwtUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.*;
import java.io.IOException;
import java.util.regex.*;
@Component
public class AuthInterceptor implements HandlerInterceptor {
private static final Pattern AUTH_PATTERN = 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 (authorization == null || !authorization.startsWith("Bearer ")) {
sendError(response, "缺失 Token");
return false;
}
Matcher matcher = AUTH_PATTERN.matcher(authorization);
if (!matcher.matches()) {
sendError(response, "無效 Token");
return false;
}
User user = jwtUtil.getUser(matcher.group("token"));
if (user == null) {
sendError(response, "登錄失效,請重新登錄");
return false;
}
SecurityContext.setUser(user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
SecurityContext.clear();
}
private void sendError(HttpServletResponse response, String message) throws IOException {
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(objectMapper.writeValueAsString(Map.of("code", -1, "message", message)));
}
}
權(quán)限切面
package com.icoderoad.aspect;
import com.icoderoad.annotation.PreAuthorize;
import com.icoderoad.context.SecurityContext;
import com.icoderoad.exception.AuthException;
import com.icoderoad.model.User;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.Set;
@Aspect
@Component
public class PermissionAspect {
@Around("@annotation(preAuthorize)")
public Object checkPermission(ProceedingJoinPoint joinPoint, PreAuthorize preAuthorize) throws Throwable {
User user = SecurityContext.getUser();
if (user == null) {
throw new AuthException("請先登錄");
}
Set<String> requiredPerms = Set.of(preAuthorize.value());
Set<String> userPerms = user.getPermissions();
boolean hasPermission = validatePermissions(requiredPerms, userPerms, preAuthorize.logic());
if (!hasPermission) {
throw new AuthException("權(quán)限不足");
}
return joinPoint.proceed();
}
private boolean validatePermissions(Set<String> required, Set<String> has, PreAuthorize.Logical logic) {
return logic == PreAuthorize.Logical.AND ? has.containsAll(required) : !Collections.disjoint(required, has);
}
}
配置攔截器
package com.icoderoad.config;
import com.icoderoad.interceptor.AuthInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
@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(authInterceptor).addPathPatterns("/users/**");
}
}
測試
package com.icoderoad.controller;
import com.icoderoad.model.User;
import com.icoderoad.util.JwtUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.Set;
@RestController
public class LoginController {
private final JwtUtil jwtUtil;
public LoginController(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@GetMapping("/login")
public ResponseEntity<Object> login(@RequestParam String username) {
User user = new User(1L, username, Set.of("USER"));
String token = jwtUtil.generateToken(user);
return ResponseEntity.ok(Map.of("token", token));
}
}
結(jié)論
通過本篇文章的學(xué)習(xí),我們基于 Spring Boot 3.4 實現(xiàn)了一套高效、靈活的權(quán)限控制方案,核心組件包括:
- 自定義注解
@PreAuthorize
用于定義方法級權(quán)限控制。 - 攔截器(Interceptor)用于解析 Token 并獲取用戶信息。
- AOP 切面在方法執(zhí)行前進行權(quán)限校驗。
- 自定義參數(shù)解析器讓 Controller 層代碼更加簡潔優(yōu)雅。
相比傳統(tǒng)的 Spring Security 方案,本方案的優(yōu)勢在于:
- 更加輕量級不依賴復(fù)雜的認證機制,僅需少量代碼即可實現(xiàn)。
- 高度可擴展可以靈活適配不同的認證方式,如 JWT、OAuth2 等。
- 代碼解耦業(yè)務(wù)邏輯與權(quán)限校驗分離,提升可維護性。
在實際項目中,該方案適用于對性能和靈活性要求較高的場景,開發(fā)者可以在此基礎(chǔ)上,結(jié)合自身業(yè)務(wù)需求,進一步優(yōu)化和擴展,如 動態(tài)權(quán)限配置、RBAC(基于角色的訪問控制) 等。
如果你在 Spring Boot 3.4 版本的開發(fā)中,正在尋找一種 既安全又高效的權(quán)限控制方案,不妨嘗試本文介紹的方法,相信會給你的項目帶來新的啟發(fā)和幫助。