在现代 Web 应用开发中,权限控制是至关重要的一个环节,尤其是在微服务架构和前后端分离的模式下。如何在保证安全性的同时,兼顾开发的便捷性和代码的可读性,是开发者需要重点关注的问题。
Spring Boot 提供了多种方式来实现权限管理,例如 Spring Security,但在某些场景下,我们希望有更轻量级、灵活的权限控制方案。本篇文章将介绍如何通过 Spring Boot 3.4 中的 自定义注解 结合 AOP 和 参数解析器,实现一套可扩展的权限控制方案。
本方案的核心思路是:
- 通过 AOP 拦截
需要权限校验的方法,实现全局权限校验逻辑。 - 通过拦截器(Interceptor)
解析请求 Token,并将用户信息存储到上下文中,方便后续权限校验使用。 - 使用自定义参数解析器(HandlerMethodArgumentResolver)
在 Controller 方法参数中,通过注解直接获取当前登录用户信息,提高代码的可读性。 - 支持 SpEL(Spring 表达式语言)
以动态方式获取用户数据,实现更灵活的权限控制。
本文将结合代码示例,详细介绍该方案的实现方式,并最终构建一套完整的权限校验框架。
自定义注解
获取当前用户信息的注解
package com.icoderoad.auth;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface AuthUser {
/** 通过 SpEL 表达式从当前登录用户信息中提取数据 */
String value() default "";
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
权限控制注解
package com.icoderoad.auth;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuthorize {
/** 需要的权限 */
String[] value() default {};
/** 权限校验逻辑(全部匹配或部分匹配) */
Logical logic() default Logical.AND;
enum Logical {
AND, OR;
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
核心组件实现
生成 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);
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
认证拦截器
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)));
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
权限切面
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("权限不足");
}
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);
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
配置拦截器
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/**");
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
测试
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));
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
结论
通过本篇文章的学习,我们基于 Spring Boot 3.4 实现了一套高效、灵活的权限控制方案,核心组件包括:
- 自定义注解
@PreAuthorize
用于定义方法级权限控制。 - 拦截器(Interceptor)用于解析 Token 并获取用户信息。
- AOP 切面在方法执行前进行权限校验。
- 自定义参数解析器让 Controller 层代码更加简洁优雅。
相比传统的 Spring Security 方案,本方案的优势在于:
- 更加轻量级不依赖复杂的认证机制,仅需少量代码即可实现。
- 高度可扩展可以灵活适配不同的认证方式,如 JWT、OAuth2 等。
- 代码解耦业务逻辑与权限校验分离,提升可维护性。
在实际项目中,该方案适用于对性能和灵活性要求较高的场景,开发者可以在此基础上,结合自身业务需求,进一步优化和扩展,如 动态权限配置、RBAC(基于角色的访问控制) 等。
如果你在 Spring Boot 3.4 版本的开发中,正在寻找一种 既安全又高效的权限控制方案,不妨尝试本文介绍的方法,相信会给你的项目带来新的启发和帮助。