靈活!Spring Boot 自定義注解結(jié)合參數(shù)解析器實(shí)現(xiàn)權(quán)限控制
在現(xiàn)代 Web 應(yīng)用開發(fā)中,權(quán)限控制是至關(guān)重要的一個(gè)環(huán)節(jié),尤其是在微服務(wù)架構(gòu)和前后端分離的模式下。如何在保證安全性的同時(shí),兼顧開發(fā)的便捷性和代碼的可讀性,是開發(fā)者需要重點(diǎn)關(guān)注的問(wèn)題。
Spring Boot 提供了多種方式來(lái)實(shí)現(xiàn)權(quán)限管理,例如 Spring Security,但在某些場(chǎng)景下,我們希望有更輕量級(jí)、靈活的權(quán)限控制方案。本篇文章將介紹如何通過(guò) Spring Boot 3.4 中的 自定義注解 結(jié)合 AOP 和 參數(shù)解析器,實(shí)現(xiàn)一套可擴(kuò)展的權(quán)限控制方案。
本方案的核心思路是:
- 通過(guò) AOP 攔截
需要權(quán)限校驗(yàn)的方法,實(shí)現(xiàn)全局權(quán)限校驗(yàn)邏輯。 - 通過(guò)攔截器(Interceptor)
解析請(qǐng)求 Token,并將用戶信息存儲(chǔ)到上下文中,方便后續(xù)權(quán)限校驗(yàn)使用。 - 使用自定義參數(shù)解析器(HandlerMethodArgumentResolver)
在 Controller 方法參數(shù)中,通過(guò)注解直接獲取當(dāng)前登錄用戶信息,提高代碼的可讀性。 - 支持 SpEL(Spring 表達(dá)式語(yǔ)言)
以動(dòng)態(tài)方式獲取用戶數(shù)據(jù),實(shí)現(xiàn)更靈活的權(quán)限控制。
本文將結(jié)合代碼示例,詳細(xì)介紹該方案的實(shí)現(xiàn)方式,并最終構(gòu)建一套完整的權(quán)限校驗(yàn)框架。
自定義注解
獲取當(dāng)前用戶信息的注解
package com.icoderoad.auth;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface AuthUser {
/** 通過(guò) SpEL 表達(dá)式從當(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)限校驗(yàn)邏輯(全部匹配或部分匹配) */
Logical logic() default Logical.AND;
enum Logical {
AND, OR;
}
}核心組件實(shí)現(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);
}
}認(rèn)證攔截器
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, "無(wú)效 Token");
return false;
}
User user = jwtUtil.getUser(matcher.group("token"));
if (user == null) {
sendError(response, "登錄失效,請(qǐng)重新登錄");
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("請(qǐng)先登錄");
}
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/**");
}
}測(cè)試
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é)論
通過(guò)本篇文章的學(xué)習(xí),我們基于 Spring Boot 3.4 實(shí)現(xiàn)了一套高效、靈活的權(quán)限控制方案,核心組件包括:
- 自定義注解
@PreAuthorize
用于定義方法級(jí)權(quán)限控制。 - 攔截器(Interceptor)用于解析 Token 并獲取用戶信息。
- AOP 切面在方法執(zhí)行前進(jìn)行權(quán)限校驗(yàn)。
- 自定義參數(shù)解析器讓 Controller 層代碼更加簡(jiǎn)潔優(yōu)雅。
相比傳統(tǒng)的 Spring Security 方案,本方案的優(yōu)勢(shì)在于:
- 更加輕量級(jí)不依賴復(fù)雜的認(rèn)證機(jī)制,僅需少量代碼即可實(shí)現(xiàn)。
- 高度可擴(kuò)展可以靈活適配不同的認(rèn)證方式,如 JWT、OAuth2 等。
- 代碼解耦業(yè)務(wù)邏輯與權(quán)限校驗(yàn)分離,提升可維護(hù)性。
在實(shí)際項(xiàng)目中,該方案適用于對(duì)性能和靈活性要求較高的場(chǎng)景,開發(fā)者可以在此基礎(chǔ)上,結(jié)合自身業(yè)務(wù)需求,進(jìn)一步優(yōu)化和擴(kuò)展,如 動(dòng)態(tài)權(quán)限配置、RBAC(基于角色的訪問(wèn)控制) 等。
如果你在 Spring Boot 3.4 版本的開發(fā)中,正在尋找一種 既安全又高效的權(quán)限控制方案,不妨嘗試本文介紹的方法,相信會(huì)給你的項(xiàng)目帶來(lái)新的啟發(fā)和幫助。
































