---
title: "03-权限校验最佳实践模型代码"
created: 2025-12-02
tags:
- 项目
aliases:
- 权限校验最佳实践模型代码
---
# 权限校验最佳实践模型代码
## **分层架构**
```java
src/main/java/com/zwnsyw/zwwwspringbootbasetemplate
├── security/ # 安全模块
│ ├── annotation/ # 注解定义
│ │ ├── Anonymous.java # 匿名访问
│ │ ├── RequiresPermission.java # 权限校验
│ │ └── RequiresRole.java # 角色校验
│ │
│ ├── config/ # 配置
│ │ ├── SecurityConfig.java # 安全配置
│ │ └── AnonymousUrlConfig.java # 匿名URL配置
│ │
│ ├── context/ # 安全上下文
│ │ └── SecurityContext.java # 当前用户上下文
│ │
│ │── enums/ # 枚举
│ │ └── Logical.java # 逻辑符枚举
│ │
│ │── handler/ # 权限处理器
│ │ └── PermissionHandler.java # 权限校验逻辑
│ │
│ │── interceptor/ # 拦截器
│ │ └── AuthorizationInterceptor.java # 鉴权拦截器
│ │
│ └── utils/ # 工具类
│ └── SecurityUtils.java # 安全工具类
```
## **代码**
### **1. 注解定义**
#### `Anonymous`
```java
package com.zwnsyw.zwwwspringbootbasetemplate.security.annotation;
import java.lang.annotation.*;
/**
* 标记接口允许匿名访问(无需登录)
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Anonymous {
}
```
#### `RequiresPermission`
```java
package com.zwnsyw.zwwwspringbootbasetemplate.security.annotation;
import com.zwnsyw.zwwwspringbootbasetemplate.security.enums.Logical;
import java.lang.annotation.*;
/**
* 权限校验注解
*
* 使用示例:
* - @RequiresPermission("system:user:add") 单个权限
* - @RequiresPermission(value = {"user:add", "user:edit"}, logical = Logical.OR) 任一权限
*
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequiresPermission {
/**
* 需要的权限码
*/
String[] value();
/**
* 多个权限之间的逻辑关系
*/
Logical logical() default Logical.AND;
}
```
#### `RequiresRole`
```java
package com.zwnsyw.zwwwspringbootbasetemplate.security.annotation;
import com.zwnsyw.zwwwspringbootbasetemplate.security.enums.Logical;
import java.lang.annotation.*;
/**
* 角色校验注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequiresRole {
/**
* 需要的角色
*/
String[] value();
/**
* 多个角色之间的逻辑关系
*/
Logical logical() default Logical.AND;
}
```
#### `Logical`
```java
package com.zwnsyw.zwwwspringbootbasetemplate.security.enums;
/**
* 逻辑枚举
*/
public enum Logical {
/**
* 必须满足所有条件
*/
AND,
/**
* 满足任一条件即可
*/
OR
}
```
### **2. 安全上下文(**存储当前请求的用户信息**)**
```java
package com.zwnsyw.zwwwspringbootbasetemplate.security.context;
import com.zwnsyw.zwwwspringbootbasetemplate.exception.BusinessException;
import com.zwnsyw.zwwwspringbootbasetemplate.exception.ErrorCode;
import com.zwnsyw.zwwwspringbootbasetemplate.model.vo.LoginUserVO;
import lombok.extern.slf4j.Slf4j;
/**
* 安全上下文 - 使用 ThreadLocal 存储当前请求的用户信息
*
* ==================== 工作原理 ====================
*
* 每个 HTTP 请求线程独立存储一份用户信息,不同线程互不影响:
*
* Thread-1 (请求A): SecurityContext -> User A
* Thread-2 (请求B): SecurityContext -> User B
* Thread-3 (请求C): SecurityContext -> User C
*
* ==================== 关键特性 ====================
*
* 1. ThreadLocal 隔离:每个请求线程独立存储
* 2. 自动清理:拦截器 afterCompletion 中调用 clear()
* 3. 防止泄露:线程池复用线程时防止数据混乱
* 4. 获取用户:getCurrentUser()、requireCurrentUser()、getCurrentUserId()
*
* ==================== 使用方式 ====================
*
* // 在 Controller 中使用
* @GetMapping("/profile")
* public ResponseEntity> getProfile() {
* LoginUserVO user = SecurityContext.requireCurrentUser();
* return ResponseEntity.ok(user);
* }
*
* // 在 Service 中使用
* public void updateOrder(Order order) {
* Long userId = SecurityContext.requireCurrentUserId();
* // 验证用户是否为订单所有者
* if (!order.getUserId().equals(userId)) {
* throw new BusinessException("无权操作他人订单");
* }
* }
*
* // 判断是否登录
* if (SecurityContext.isAuthenticated()) {
* // 用户已登录
* }
*/
@Slf4j
public class SecurityContext {
// ThreadLocal 容器,每个线程存储一份用户信息
private static final ThreadLocal USER_HOLDER = new ThreadLocal<>();
private SecurityContext() {
// 私有构造函数,防止误操作实例化
}
/**
* 设置当前请求的用户信息
*
* 由 AuthorizationInterceptor 在请求处理前调用
*
*
* @param user 登录用户信息,null 时清除之前的信息
*/
public static void setCurrentUser(LoginUserVO user) {
if (user == null) {
clear();
} else {
USER_HOLDER.set(user);
log.debug("Set current user: {}", user.getId());
}
}
/**
* 获取当前请求的用户信息
*
* 如果用户未登录返回 null
*
*
* @return 当前登录用户,未登录时返回 null
*/
public static LoginUserVO getCurrentUser() {
return USER_HOLDER.get();
}
/**
* 获取当前用户信息,如果未登录抛出异常
*
* 推荐在需要用户信息的地方使用此方法
*
*
* @return 当前登录用户
* @throws BusinessException 用户未登录时抛出 NOT_LOGIN_ERROR
*/
public static LoginUserVO requireCurrentUser() {
LoginUserVO user = getCurrentUser();
if (user == null) {
log.warn("User not authenticated when calling requireCurrentUser()");
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
}
return user;
}
/**
* 获取当前用户的 ID
*
* 如果用户未登录返回 null
*
*
* @return 当前用户 ID,未登录时返回 null
*/
public static Long getCurrentUserId() {
LoginUserVO user = getCurrentUser();
return user != null ? user.getId() : null;
}
/**
* 获取当前用户的 ID,如果未登录抛出异常
*
* 推荐在需要用户 ID 的地方使用此方法
*
*
* @return 当前用户 ID
* @throws BusinessException 用户未登录时抛出 NOT_LOGIN_ERROR
*/
public static Long requireCurrentUserId() {
return requireCurrentUser().getId();
}
/**
* 获取当前用户名
*
* @return 当前用户名
*/
public static String getCurrentUsername() {
LoginUserVO user = getCurrentUser();
return user != null ? user.getUserName() : null;
}
/**
* 清除当前线程的用户信息
*
* 由 AuthorizationInterceptor.afterCompletion() 自动调用
* 防止线程池中线程复用时信息泄露
*
*/
public static void clear() {
USER_HOLDER.remove();
log.debug("SecurityContext cleared");
}
/**
* 判断用户是否已认证(已登录)
*
* 用于条件判断,不抛异常
*
*
* @return true 用户已登录,false 用户未登录
*/
public static boolean isAuthenticated() {
return getCurrentUser() != null;
}
/**
* 判断当前用户 ID 是否与指定 ID 相同
*
* 用于数据级权限检查
*
*
* @param userId 要比较的用户 ID
* @return true 相同,false 不相同或未登录
*/
public static boolean isCurrentUser(Long userId) {
if (userId == null) {
return false;
}
Long currentUserId = getCurrentUserId();
return currentUserId != null && currentUserId.equals(userId);
}
/**
* 判断当前用户是否具有指定角色
*
* 这里仅做简单演示,实际应该通过 PermissionHandler 检查
*
*
* @param role 角色名
* @return true 拥有指定角色,false 没有或未登录
*/
public static boolean hasRole(String role) {
LoginUserVO user = getCurrentUser();
if (user == null) {
return false;
}
// // 检查单角色
// if (role.equals(user.getUserRole())) {
// return true;
// }
// 检查多角色集合
if (user.getRoles() != null) {
return user.getRoles().contains(role);
}
return false;
}
}
```
### **3. 权限处理器**
```java
package com.zwnsyw.zwwwspringbootbasetemplate.security.handler;
import com.zwnsyw.zwwwspringbootbasetemplate.model.vo.LoginUserVO;
import com.zwnsyw.zwwwspringbootbasetemplate.security.enums.Logical;
import com.zwnsyw.zwwwspringbootbasetemplate.security.context.SecurityContext;
import com.zwnsyw.zwwwspringbootbasetemplate.service.PermissionCacheService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Set;
import static com.zwnsyw.zwwwspringbootbasetemplate.constant.UserConstant.ADMIN_ROLE;
import static com.zwnsyw.zwwwspringbootbasetemplate.constant.UserConstant.SUPER_PERMISSION;
/**
* 权限和角色校验处理器
*
* ==================== 权限模型 ====================
*
* 1. 超级权限:如果用户拥有 SUPER_PERMISSION,则拥有所有权限
* 2. 管理员角色:如果用户角色包含 ADMIN_ROLE,则拥有所有权限
* 3. 权限集合:检查用户的 permissions 集合
* 4. 角色集合:检查用户的 roles 集合
*
* ==================== 逻辑关系 ====================
*
* AND 逻辑:必须拥有所有权限/角色
* OR 逻辑:只需拥有任一权限/角色
*
* ==================== 使用流程 ====================
*
* 1. AuthorizationInterceptor 读取 @RequiresPermission/@RequiresRole 注解
* 2. 调用 hasPermission/hasRole 方法进行校验
* 3. 如果校验失败抛出 BusinessException
*/
@Slf4j
@Component
public class PermissionHandler {
private PermissionCacheService permissionCacheService;
@Autowired(required = false)
public void setPermissionCacheService(PermissionCacheService permissionCacheService) {
this.permissionCacheService = permissionCacheService;
}
/**
* 校验用户是否拥有指定权限
*
* 校验优先级(从高到低):
* 1. 如果用户拥有超级权限 (SUPER_PERMISSION),直接返回 true
* 2. 如果用户是管理员但没有配置权限,返回 true
* 3. 检查用户的权限集合 (AND/OR 逻辑)
*
*
* @param permissions 需要的权限数组
* @param logical 多权限之间的逻辑关系 (AND/OR)
* @return true 拥有权限,false 没有权限
*/
public boolean hasPermission(String[] permissions, Logical logical) {
if (permissions == null || permissions.length == 0) {
log.warn("Permission array is null or empty");
return false;
}
LoginUserVO user = SecurityContext.getCurrentUser();
if (user == null) {
log.debug("User not authenticated, permission denied");
return false;
}
// 优先使用 LoginUserVO 中的权限集合
Set userPermissions = user.getPermissions();
// 如果 LoginUserVO 中没有权限,从缓存加载
if ((userPermissions == null || userPermissions.isEmpty()) && permissionCacheService != null) {
userPermissions = permissionCacheService.getPermissions(user.getId());
}
// 如果仍然没有权限配置
if (userPermissions == null || userPermissions.isEmpty()) {
// 管理员拥有所有权限
boolean isAdmin = isAdmin(user);
log.debug("User {} has no permission config, isAdmin: {}", user.getId(), isAdmin);
return isAdmin;
}
// 超级权限检查
if (userPermissions.contains(SUPER_PERMISSION)) {
log.debug("User {} has super permission", user.getId());
return true;
}
// 根据逻辑关系检查权限
boolean hasPermission;
if (logical == Logical.AND) {
hasPermission = Arrays.stream(permissions).allMatch(userPermissions::contains);
} else {
hasPermission = Arrays.stream(permissions).anyMatch(userPermissions::contains);
}
if (!hasPermission) {
log.warn("User {} missing permissions: {}, logical: {}",
user.getId(), Arrays.toString(permissions), logical);
}
return hasPermission;
}
/**
* 校验用户是否拥有指定角色
*
* 校验优先级(从高到低):
* 1. 如果用户是管理员,拥有所有角色
* 2. 检查用户的角色集合
* 3. 根据 AND/OR 逻辑判断是否拥有指定角色
*
*
* @param roles 需要的角色数组
* @param logical 多角色之间的逻辑关系 (AND/OR)
* @return true 拥有指定角色,false 没有指定角色
*/
public boolean hasRole(String[] roles, Logical logical) {
if (roles == null || roles.length == 0) {
log.warn("Role array is null or empty");
return false;
}
LoginUserVO user = SecurityContext.getCurrentUser();
if (user == null) {
log.debug("User not authenticated, role denied");
return false;
}
// 管理员拥有所有角色权限
if (isAdmin(user)) {
log.debug("User {} is admin, has all roles", user.getId());
return true;
}
// 优先使用 LoginUserVO 中的角色集合
Set userRoles = user.getRoles();
// 如果 LoginUserVO 中没有角色,从缓存加载
if ((userRoles == null || userRoles.isEmpty()) && permissionCacheService != null) {
userRoles = permissionCacheService.getRoles(user.getId());
}
// 如果仍然没有角色
if (userRoles == null || userRoles.isEmpty()) {
log.debug("User {} has no role config", user.getId());
return false;
}
// 根据逻辑关系检查角色
boolean hasRole;
if (logical == Logical.AND) {
hasRole = Arrays.stream(roles).allMatch(userRoles::contains);
} else {
hasRole = Arrays.stream(roles).anyMatch(userRoles::contains);
}
if (!hasRole) {
log.warn("User {} missing roles: {}, logical: {}",
user.getId(), Arrays.toString(roles), logical);
}
return hasRole;
}
/**
* 判断用户是否为管理员
*
* 检查用户角色集合中是否包含 ADMIN_ROLE
*
*
* @param user 登录用户信息
* @return true 是管理员,false 不是管理员
*/
private boolean isAdmin(LoginUserVO user) {
if (user == null) {
return false;
}
// 使用 LoginUserVO 提供的便捷方法
return user.isAdmin();
}
/**
* 检查单个权限
*
* @param permission 权限标识
* @return true 拥有权限,false 没有权限
*/
public boolean checkPermission(String permission) {
return hasPermission(new String[]{permission}, Logical.AND);
}
/**
* 检查单个角色
*
* @param role 角色标识
* @return true 拥有角色,false 没有角色
*/
public boolean checkRole(String role) {
return hasRole(new String[]{role}, Logical.AND);
}
}
```
### **4. 匿名 URL 配置**
```java
package com.zwnsyw.zwwwspringbootbasetemplate.security.config;
import com.zwnsyw.zwwwspringbootbasetemplate.security.annotation.Anonymous;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/**
* 匿名访问 URL 配置
*
* 自动扫描 @Anonymous 注解,收集允许匿名访问的 URL
*
*
* ==================== 工作流程 ====================
*
* 1. 在拦截器中调用 isAnonymousUrl() 检查请求
* 2. 通过 AntPathMatcher 匹配请求路径是否允许匿名访问
* 3. 首次调用时自动扫描 @Anonymous 注解
* 4. 后续调用使用缓存的 URL 列表
*
* ==================== 使用方式 ====================
*
* 在拦截器中使用:
*
* @Component
* public class AuthorizationInterceptor implements HandlerInterceptor {
* @Autowired
* private AnonymousUrlConfig anonymousUrlConfig;
*
* public boolean preHandle(HttpServletRequest request, ...) {
* String requestUri = request.getRequestURI();
*
* if (anonymousUrlConfig.isAnonymousUrl(requestUri)) {
* // 允许匿名访问
* return true;
* }
* // 需要验证权限
* }
* }
*
* ==================== @Anonymous 注解位置 ====================
*
* 1. 方法级别(推荐):
* @GetMapping("/public")
* @Anonymous
* public ResponseEntity> publicApi() {
* return ResponseEntity.ok("公开接口");
* }
*
* 2. 类级别(整个 Controller 允许匿名):
* @RestController
* @Anonymous
* public class PublicController { }
*
* ==================== 关键特性 ====================
*
* - 支持路径变量:/user/{id} -> /user/*
* - 线程安全:使用 volatile + synchronized 保证只扫描一次
* - 延迟加载:首次调用时才扫描注解
* - 缓存机制:扫描结果缓存在内存中
*/
@Slf4j
@Configuration
public class AnonymousUrlConfig {
private static final Pattern PATH_VARIABLE_PATTERN = Pattern.compile("\\{[^}]+}");
private final AntPathMatcher pathMatcher = new AntPathMatcher();
@Getter
private volatile Set anonymousUrls;
private final ObjectProvider requestMappingHandlerMappingProvider;
/**
* 使用 ObjectProvider 延迟加载,避免 Bean 创建顺序问题
*/
public AnonymousUrlConfig(ObjectProvider requestMappingHandlerMappingProvider) {
this.requestMappingHandlerMappingProvider = requestMappingHandlerMappingProvider;
}
/**
* 检查请求 URL 是否允许匿名访问
*
* ==================== 处理流程 ====================
*
* 1. 规范化请求路径
* - 原始路径:/api/auth/login
* - 处理后:/auth/login(移除 /api 前缀)
*
* 2. 首次加载匿名 URL 列表
* - 扫描所有 @Anonymous 注解的方法/类
* - 缓存到 anonymousUrls
*
* 3. 匹配请求路径
* - 使用 AntPathMatcher 进行模式匹配
* - 支持通配符:/user/* 匹配 /user/123
*
* @param requestUrl 请求 URL(完整路径,包含 /api 前缀)
* @return 是否允许匿名访问
*
* 使用示例:
* boolean isAnonymous = anonymousUrlConfig.isAnonymousUrl("/api/auth/login");
*/
public boolean isAnonymousUrl(String requestUrl) {
// 第一步:规范化请求路径
String normalizedUrl = normalizeRequestPath(requestUrl);
log.trace("Checking anonymous URL: original={}, normalized={}", requestUrl, normalizedUrl);
// 第二步:首次调用时才加载匿名 URL 列表
if (anonymousUrls == null) {
synchronized (this) {
if (anonymousUrls == null) {
loadAnonymousUrls();
}
}
}
// 第三步:遍历所有匿名 URL 模式,使用 AntPathMatcher 判断是否匹配
for (String pattern : anonymousUrls) {
if (pathMatcher.match(pattern, normalizedUrl)) {
log.debug("Request matched anonymous URL pattern: url={}, pattern={}", normalizedUrl, pattern);
return true;
}
}
log.trace("Request does not match any anonymous URL pattern: {}", normalizedUrl);
return false;
}
/**
* 规范化请求路径
*
* 作用:移除服务器上下文路径前缀,只保留业务 URI
*
* 处理规则:
* 1. 如果以 /api 开头,去掉 /api(匹配 server.servlet.context-path=/api)
* 2. 否则保持原样
*
* 示例:
* /api/auth/login → /auth/login
* /auth/login → /auth/login
* /api/user/123 → /user/123
*
* @param requestUrl 原始请求 URL
* @return 规范化后的路径
*/
private String normalizeRequestPath(String requestUrl) {
if (requestUrl == null) {
return "";
}
// ✅ 方法一:移除已知的上下文路径前缀
// 适用于:server.servlet.context-path=/api 的配置
if (requestUrl.startsWith("/api")) {
return requestUrl.substring(4); // 移除 "/api" 前缀
}
// ✅ 方法二:如果需要支持动态上下文路径,可以从 HTTP Request 中获取
// 但由于 isAnonymousUrl 方法没有 request 参数,这里保持简单处理
return requestUrl;
}
/**
* 扫描 @Anonymous 注解,加载匿名访问 URL
*
* 此方法在首次调用 isAnonymousUrl() 时执行
*
* ==================== 扫描流程 ====================
*
* 1. 获取所有 Spring MVC 映射的处理方法
* 2. 检查是否有 @Anonymous 注解
* 3. 提取 URL 路径模式
* 4. 将路径变量转换为通配符
* 5. 缓存到 anonymousUrls
*/
private synchronized void loadAnonymousUrls() {
// 双重检查:如果已加载,直接返回
if (anonymousUrls != null) {
return;
}
Set urls = new HashSet<>();
RequestMappingHandlerMapping mapping = requestMappingHandlerMappingProvider.getIfAvailable();
if (mapping == null) {
log.warn("RequestMappingHandlerMapping not available, using empty anonymous URL list");
anonymousUrls = urls;
return;
}
Map handlerMethods = mapping.getHandlerMethods();
handlerMethods.forEach((mappingInfo, handlerMethod) -> {
// 检查是否有 @Anonymous 注解
if (hasAnonymousAnnotation(handlerMethod)) {
// 兼容两种路径匹配策略
Set patterns = new HashSet<>();
// Spring Boot 2.6+ 使用 PathPatternsCondition
if (mappingInfo.getPathPatternsCondition() != null) {
mappingInfo.getPathPatternsCondition().getPatterns()
.forEach(p -> patterns.add(p.getPatternString()));
}
// 旧版本使用 PatternsCondition
else if (mappingInfo.getPatternsCondition() != null) {
patterns.addAll(mappingInfo.getPatternsCondition().getPatterns());
}
patterns.forEach(pattern -> {
// 将路径变量转换为 AntPattern
// 例如:/user/{id} -> /user/*
String antPattern = PATH_VARIABLE_PATTERN.matcher(pattern).replaceAll("*");
urls.add(antPattern);
log.debug("Registered anonymous URL pattern: {} -> {}", pattern, antPattern);
});
}
});
anonymousUrls = urls;
log.info("Loaded {} anonymous URL patterns: {}", urls.size(), urls);
}
/**
* 判断 Handler 方法或其所属类是否有 @Anonymous 注解
*
* ==================== 检查顺序 ====================
*
* 1. 优先检查方法级别的注解
* - 如果方法有 @Anonymous,直接返回 true
*
* 2. 再检查类级别的注解
* - 如果类有 @Anonymous,该类的所有方法都允许匿名访问
*
* @param handlerMethod Handler 方法
* @return 是否有 @Anonymous 注解
*/
private boolean hasAnonymousAnnotation(HandlerMethod handlerMethod) {
// 先检查方法级别的注解
Anonymous methodAnnotation = AnnotationUtils.findAnnotation(
handlerMethod.getMethod(),
Anonymous.class
);
if (methodAnnotation != null) {
return true;
}
// 再检查类级别的注解
Anonymous classAnnotation = AnnotationUtils.findAnnotation(
handlerMethod.getBeanType(),
Anonymous.class
);
return classAnnotation != null;
}
/**
* 获取当前已加载的所有匿名 URL(用于调试)
*
* @return 匿名 URL 集合
*/
public Set getLoadedAnonymousUrls() {
if (anonymousUrls == null) {
synchronized (this) {
if (anonymousUrls == null) {
loadAnonymousUrls();
}
}
}
return new HashSet<>(anonymousUrls);
}
/**
* 打印所有匿名 URL(用于调试和监控)
*
* 使用示例:
* anonymousUrlConfig.printAnonymousUrls();
*/
public void printAnonymousUrls() {
Set urls = getLoadedAnonymousUrls();
log.info("========== Anonymous URLs ==========");
urls.forEach(url -> log.info(" - {}", url));
log.info("Total: {}", urls.size());
log.info("====================================");
}
}
```
### **5. 鉴权拦截器**
```java
package com.zwnsyw.zwwwspringbootbasetemplate.security.interceptor;
import com.zwnsyw.zwwwspringbootbasetemplate.exception.BusinessException;
import com.zwnsyw.zwwwspringbootbasetemplate.exception.ErrorCode;
import com.zwnsyw.zwwwspringbootbasetemplate.model.vo.LoginUserVO;
import com.zwnsyw.zwwwspringbootbasetemplate.security.annotation.RequiresPermission;
import com.zwnsyw.zwwwspringbootbasetemplate.security.annotation.RequiresRole;
import com.zwnsyw.zwwwspringbootbasetemplate.security.config.AnonymousUrlConfig;
import com.zwnsyw.zwwwspringbootbasetemplate.security.context.SecurityContext;
import com.zwnsyw.zwwwspringbootbasetemplate.security.handler.PermissionHandler;
import com.zwnsyw.zwwwspringbootbasetemplate.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* 鉴权拦截器 - 权限和认证的核心
*
* ==================== 执行流程 ====================
*
* preHandle (请求处理前):
* 1. 检查是否是 HandlerMethod(Controller 方法)
* 2. 放行 OPTIONS 请求(CORS 预检)
* 3. 检查 URL 是否允许匿名访问
* - 是:尝试获取用户信息(可选),继续处理
* - 否:必须获取用户信息,否则拒绝
* 4. 检查 @RequiresPermission 和 @RequiresRole 注解
*
* afterCompletion (请求完成后,总是会调用):
* 5. 清理 ThreadLocal 中的用户信息(防止线程池泄露)
*
* ==================== 注意事项 ====================
*
* - 即使发生异常,afterCompletion 也会被调用
* - OPTIONS 请求自动放行(CORS 预检)
* - 非 Controller 方法(静态资源等)自动放行
*
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthorizationInterceptor implements HandlerInterceptor {
private final UserService userService;
private final AnonymousUrlConfig anonymousUrlConfig;
private final PermissionHandler permissionHandler;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 1. 非 Controller 方法直接放行(静态资源、异常处理等)
if (!(handler instanceof HandlerMethod)) {
log.debug("Not a HandlerMethod, pass through");
return true;
}
String requestUri = request.getRequestURI();
String method = request.getMethod();
// 2. OPTIONS 请求放行(CORS 预检)
if ("OPTIONS".equalsIgnoreCase(method)) {
log.debug("OPTIONS request, pass through for CORS preflight");
return true;
}
log.debug("Checking authorization for {} {}", method, requestUri);
// 3. 检查是否匿名访问 URL
if (anonymousUrlConfig.isAnonymousUrl(requestUri)) {
log.debug("Anonymous URL, no authentication required");
// 尝试获取用户信息(用于日志、审计等)
trySetCurrentUser(request);
return true;
}
// 4. 非匿名 URL 必须获取用户信息
LoginUserVO loginUser = userService.getLoginUser(request);
if (loginUser == null) {
log.warn("Authentication required but user not found for {}", requestUri);
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
}
// 将用户信息存储到 ThreadLocal
SecurityContext.setCurrentUser(loginUser);
log.debug("User authenticated: {}", loginUser.getId());
// 5. 检查权限和角色注解
HandlerMethod handlerMethod = (HandlerMethod) handler;
checkPermissionAnnotations(handlerMethod);
checkRoleAnnotations(handlerMethod);
return true;
}
/**
* 请求完成后必须清理 ThreadLocal
* 即使发生异常也会调用此方法
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
// 清理 ThreadLocal,防止线程池复用时信息泄露
if (SecurityContext.isAuthenticated()) {
Long userId = SecurityContext.getCurrentUserId();
log.debug("Clearing SecurityContext for user {}", userId);
}
SecurityContext.clear();
}
/**
* 尝试获取用户信息(匿名访问时使用)
* 不抛异常,失败时忽略
*/
private void trySetCurrentUser(HttpServletRequest request) {
try {
LoginUserVO loginUser = userService.getLoginUser(request);
if (loginUser != null) {
SecurityContext.setCurrentUser(loginUser);
log.debug("Set user info for anonymous request: {}", loginUser.getId());
}
} catch (Exception e) {
// 忽略异常,匿名访问不需要用户信息
log.trace("Failed to get user info for anonymous request", e);
}
}
/**
* 检查权限注解
*/
private void checkPermissionAnnotations(HandlerMethod handlerMethod) {
Method method = handlerMethod.getMethod();
Class> beanType = handlerMethod.getBeanType();
// 优先检查方法级别注解
RequiresPermission methodPermission = method.getAnnotation(RequiresPermission.class);
if (methodPermission != null) {
validatePermission(methodPermission, method.getName(), "方法");
return;
}
// 再检查类级别注解
RequiresPermission classPermission = beanType.getAnnotation(RequiresPermission.class);
if (classPermission != null) {
validatePermission(classPermission, beanType.getName(), "类");
}
}
/**
* 检查角色注解
*/
private void checkRoleAnnotations(HandlerMethod handlerMethod) {
Method method = handlerMethod.getMethod();
Class> beanType = handlerMethod.getBeanType();
// 优先检查方法级别注解
RequiresRole methodRole = method.getAnnotation(RequiresRole.class);
if (methodRole != null) {
validateRole(methodRole, method.getName(), "方法");
return;
}
// 再检查类级别注解
RequiresRole classRole = beanType.getAnnotation(RequiresRole.class);
if (classRole != null) {
validateRole(classRole, beanType.getName(), "类");
}
}
/**
* 验证权限,失败抛出异常
*/
private void validatePermission(RequiresPermission annotation, String target, String type) {
if (!permissionHandler.hasPermission(annotation.value(), annotation.logical())) {
String permissions = Arrays.toString(annotation.value());
Long userId = SecurityContext.getCurrentUserId();
log.warn("{} {} - 权限不足 - userId: {}, required: {}, logical: {}",
type, target, userId, permissions, annotation.logical());
throw new BusinessException(ErrorCode.FORBIDDEN,
"权限不足,需要 " + permissions + " 权限");
}
}
/**
* 验证角色,失败抛出异常
*/
private void validateRole(RequiresRole annotation, String target, String type) {
if (!permissionHandler.hasRole(annotation.value(), annotation.logical())) {
String roles = Arrays.toString(annotation.value());
Long userId = SecurityContext.getCurrentUserId();
log.warn("{} {} - 角色不足 - userId: {}, required: {}, logical: {}",
type, target, userId, roles, annotation.logical());
throw new BusinessException(ErrorCode.NO_AUTH_ERROR,
"角色权限不足,需要 " + roles + " 角色");
}
}
}
```
### **6. 安全配置(注册拦截器)**
```java
package com.zwnsyw.zwwwspringbootbasetemplate.security.config;
import com.zwnsyw.zwwwspringbootbasetemplate.security.interceptor.AuthorizationInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class SecurityConfig implements WebMvcConfigurer {
private final AuthorizationInterceptor authorizationInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authorizationInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
// 静态资源
"/static/**",
"/favicon.ico",
// Swagger/Knife4j
"/doc.html",
"/swagger-resources/**",
"/webjars/**",
"/v3/api-docs/**",
// 健康检查和错误页面
"/health",
"/error"
);
}
}
```
### **7. 安全工具类**
```java
package com.zwnsyw.zwwwspringbootbasetemplate.security.utils;
import com.zwnsyw.zwwwspringbootbasetemplate.exception.BusinessException;
import com.zwnsyw.zwwwspringbootbasetemplate.exception.ErrorCode;
import com.zwnsyw.zwwwspringbootbasetemplate.model.vo.LoginUserVO;
import com.zwnsyw.zwwwspringbootbasetemplate.security.context.SecurityContext;
import com.zwnsyw.zwwwspringbootbasetemplate.security.enums.Logical;
import com.zwnsyw.zwwwspringbootbasetemplate.security.handler.PermissionHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 安全工具类 - 提供权限校验、用户获取等便捷方法
*
* ==================== 使用场景 ====================
*
* 1. 获取当前用户信息
* LoginUserVO user = SecurityUtils.requireLoginUser();
* Long userId = SecurityUtils.getUserId();
*
* 2. 权限校验(失败抛异常)
* SecurityUtils.checkPermission("user:add");
* SecurityUtils.checkAnyPermission("user:add", "user:edit");
* SecurityUtils.checkRole("admin");
*
* 3. 权限判断(返回 boolean)
* if (SecurityUtils.hasPermission("user:delete")) { }
* if (SecurityUtils.isAdmin()) { }
*
* 4. 数据级权限检查
* SecurityUtils.checkSelfOrAdmin(userId); // 只允许本人或管理员
* SecurityUtils.checkOwner(ownerId); // 只允许所有者
*
* ==================== 方法分类 ====================
*
* 获取用户信息类:
* - getLoginUser()
* - requireLoginUser()
* - getUserId()
* - getUsername()
*
* 权限校验类(失败抛异常):
* - checkPermission()
* - checkAnyPermission()
* - checkRole()
* - checkAnyRole()
* - checkSelfOrAdmin()
* - checkOwner()
*
* 权限判断类(返回 boolean):
* - hasPermission()
* - hasRole()
* - isAdmin()
* - isSuperAdmin()
* - isAuthenticated()
* - isSelfOrAdmin()
*
* ==================== 线程安全 ====================
*
* 所有方法都是线程安全的,基于 ThreadLocal 实现
* 每个请求线程独立存储用户信息
*/
@Slf4j
@Component
public class SecurityUtils {
private static PermissionHandler permissionHandler;
/**
* Spring 注入 PermissionHandler
* 在构造器中初始化静态变量,避免空指针
*/
public SecurityUtils(PermissionHandler permissionHandler) {
SecurityUtils.permissionHandler = permissionHandler;
}
// ==================== 用户信息获取 ====================
/**
* 获取当前登录用户(可能为 null)
*
* 用于可选判断场景,不抛异常
*
*
* @return 当前登录用户,未登录时返回 null
*/
public static LoginUserVO getLoginUser() {
return SecurityContext.getCurrentUser();
}
/**
* 获取当前登录用户(必须登录)
*
* 推荐使用此方法,确保用户已登录
*
*
* @return 当前登录用户
* @throws BusinessException NOT_LOGIN_ERROR 用户未登录时抛出
*/
public static LoginUserVO requireLoginUser() {
return SecurityContext.requireCurrentUser();
}
/**
* 获取当前用户 ID(可能为 null)
*
* @return 当前用户 ID,未登录时返回 null
*/
public static Long getUserId() {
return SecurityContext.getCurrentUserId();
}
/**
* 获取当前用户 ID(必须登录)
*
* 推荐使用此方法
*
*
* @return 当前用户 ID
* @throws BusinessException NOT_LOGIN_ERROR 用户未登录时抛出
*/
public static Long requireUserId() {
return SecurityContext.requireCurrentUserId();
}
/**
* 获取当前用户名
*
* @return 用户名,未登录时返回 null
*/
public static String getUsername() {
return SecurityContext.getCurrentUsername();
}
// ==================== 权限校验(失败抛异常)====================
/**
* 校验用户是否拥有指定权限,无权限则抛异常
*
* 用于强制要求权限的场景
* 失败时抛出 FORBIDDEN 异常
*
*
* @param permissions 需要的权限(支持多个,AND 逻辑)
* @throws BusinessException FORBIDDEN 无权限时抛出
*
* 示例:
* SecurityUtils.checkPermission("user:add");
* SecurityUtils.checkPermission("user:edit", "user:delete");
*/
public static void checkPermission(String... permissions) {
if (!permissionHandler.hasPermission(permissions, Logical.AND)) {
Long userId = SecurityContext.getCurrentUserId();
log.warn("Permission denied for user: {}, required permissions: {}", userId, permissions);
throw new BusinessException(ErrorCode.FORBIDDEN, "权限不足,无法执行此操作");
}
}
/**
* 校验用户是否拥有任一权限,无权限则抛异常
*
* OR 逻辑:只需拥有任一权限即可
*
*
* @param permissions 需要的权限(支持多个,OR 逻辑)
* @throws BusinessException FORBIDDEN 无权限时抛出
*
* 示例:
* SecurityUtils.checkAnyPermission("content:publish", "content:review");
*/
public static void checkAnyPermission(String... permissions) {
if (!permissionHandler.hasPermission(permissions, Logical.OR)) {
Long userId = SecurityContext.getCurrentUserId();
log.warn("Permission denied for user: {}, required any of: {}", userId, permissions);
throw new BusinessException(ErrorCode.FORBIDDEN, "权限不足,无法执行此操作");
}
}
/**
* 校验用户是否拥有指定角色,无角色则抛异常
*
* AND 逻辑:必须拥有所有指定角色
*
*
* @param roles 需要的角色(支持多个,AND 逻辑)
* @throws BusinessException NO_AUTH_ERROR 角色不足时抛出
*
* 示例:
* SecurityUtils.checkRole("admin");
* SecurityUtils.checkRole("editor", "reviewer"); // 需要同时拥有两个角色
*/
public static void checkRole(String... roles) {
if (!permissionHandler.hasRole(roles, Logical.AND)) {
Long userId = SecurityContext.getCurrentUserId();
log.warn("Role denied for user: {}, required roles: {}", userId, roles);
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "角色权限不足,无法执行此操作");
}
}
/**
* 校验用户是否拥有任一角色,无角色则抛异常
*
* OR 逻辑:只需拥有任一角色即可
*
*
* @param roles 需要的角色(支持多个,OR 逻辑)
* @throws BusinessException NO_AUTH_ERROR 角色不足时抛出
*
* 示例:
* SecurityUtils.checkAnyRole("admin", "super_admin"); // 拥有其一即可
*/
public static void checkAnyRole(String... roles) {
if (!permissionHandler.hasRole(roles, Logical.OR)) {
Long userId = SecurityContext.getCurrentUserId();
log.warn("Role denied for user: {}, required any of: {}", userId, roles);
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "角色权限不足,无法执行此操作");
}
}
// ==================== 权限判断(返回 boolean)====================
/**
* 判断用户是否拥有指定权限
*
* 不抛异常,返回 boolean,用于条件判断
*
*
* @param permission 权限码
* @return true 拥有权限,false 没有权限
*
* 示例:
* if (SecurityUtils.hasPermission("user:delete")) {
* // 显示删除按钮
* }
*/
public static boolean hasPermission(String permission) {
return permissionHandler.hasPermission(new String[]{permission}, Logical.AND);
}
/**
* 判断用户是否拥有任一权限
*
* @param permissions 权限码数组
* @return true 拥有任一权限,false 全部没有
*
* 示例:
* if (SecurityUtils.hasAnyPermission("content:edit", "content:review")) {
* // 显示编辑或审核按钮
* }
*/
public static boolean hasAnyPermission(String... permissions) {
return permissionHandler.hasPermission(permissions, Logical.OR);
}
/**
* 判断用户是否拥有指定角色
*
* @param role 角色
* @return true 拥有该角色,false 没有
*
* 示例:
* if (SecurityUtils.hasRole("editor")) {
* // 显示编辑功能
* }
*/
public static boolean hasRole(String role) {
return permissionHandler.hasRole(new String[]{role}, Logical.AND);
}
/**
* 判断用户是否拥有任一角色
*
* @param roles 角色数组
* @return true 拥有任一角色,false 全部没有
*/
public static boolean hasAnyRole(String... roles) {
return permissionHandler.hasRole(roles, Logical.OR);
}
/**
* 判断用户是否为管理员
*
* 检查用户是否拥有 "admin" 角色
*
*
* @return true 是管理员,false 不是
*
* 示例:
* if (SecurityUtils.isAdmin()) {
* // 显示管理员菜单
* }
*/
public static boolean isAdmin() {
return hasRole("admin");
}
/**
* 判断用户是否为超级管理员
*
* @return true 是超级管理员,false 不是
*/
public static boolean isSuperAdmin() {
return hasRole("super_admin");
}
/**
* 判断用户是否已认证(已登录)
*
* @return true 已登录,false 未登录
*
* 示例:
* if (SecurityUtils.isAuthenticated()) {
* // 用户已登录,显示个人资料
* } else {
* // 用户未登录,显示登录按钮
* }
*/
public static boolean isAuthenticated() {
return SecurityContext.isAuthenticated();
}
/**
* 判断当前用户是否为指定用户或管理员
*
* 用于数据级权限判断,常见于个人资料、订单等场景
*
*
* @param userId 要判断的用户 ID
* @return true 是本人或管理员,false 否
*
* 示例:
* if (SecurityUtils.isSelfOrAdmin(orderId)) {
* // 可以查看/修改此订单
* }
*/
public static boolean isSelfOrAdmin(Long userId) {
if (userId == null) {
return false;
}
Long currentUserId = SecurityContext.getCurrentUserId();
return currentUserId != null && currentUserId.equals(userId) || isAdmin();
}
// ==================== 数据级权限校验(失败抛异常)====================
/**
* 校验用户是否为指定用户或管理员,否则抛异常
*
* 用于保护个人隐私数据,防止用户查看/修改他人信息
* 常见于:个人资料、账户设置、订单详情等
*
*
* @param userId 要操作的用户 ID
* @throws BusinessException FORBIDDEN 无权限时抛出
*
* 示例(Controller):
* @GetMapping("/{userId}/profile")
* public ResponseEntity> getProfile(@PathVariable Long userId) {
* SecurityUtils.checkSelfOrAdmin(userId); // 权限校验
* return ResponseEntity.ok(userService.getProfile(userId));
* }
*
* 示例(Service):
* public void updateProfile(Long userId, UpdateRequest request) {
* SecurityUtils.checkSelfOrAdmin(userId); // 权限校验
* // 执行更新逻辑...
* }
*/
public static void checkSelfOrAdmin(Long userId) {
if (!isSelfOrAdmin(userId)) {
Long currentUserId = SecurityContext.getCurrentUserId();
log.warn("User {} trying to access data of user {}", currentUserId, userId);
throw new BusinessException(ErrorCode.FORBIDDEN, "无权访问他人数据");
}
}
/**
* 校验用户是否为数据所有者,否则抛异常
*
* 通用的所有权检查方法
* 用于评论、文章、创建的任何内容等
*
*
* @param ownerId 数据所有者 ID
* @throws BusinessException FORBIDDEN 无权限时抛出
*
* 示例:
* public void deleteComment(Long commentId) {
* Comment comment = commentRepository.findById(commentId);
* SecurityUtils.checkOwner(comment.getUserId()); // 只有创建者能删除
* commentRepository.delete(comment);
* }
*/
public static void checkOwner(Long ownerId) {
if (ownerId == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "所有者ID不能为空");
}
Long currentUserId = SecurityContext.getCurrentUserId();
if (currentUserId == null || !currentUserId.equals(ownerId)) {
log.warn("User {} trying to access resource owned by {}", currentUserId, ownerId);
throw new BusinessException(ErrorCode.FORBIDDEN, "无权操作此资源");
}
}
/**
* 校验用户是否为指定用户、管理员或超级管理员
*
* @param userId 要检查的用户 ID
* @throws BusinessException FORBIDDEN 无权限时抛出
*
* 示例:
* public void resetPassword(Long userId) {
* SecurityUtils.checkSelfOrAnyAdmin(userId);
* }
*/
public static void checkSelfOrAnyAdmin(Long userId) {
Long currentUserId = SecurityContext.getCurrentUserId();
if (currentUserId == null || (!currentUserId.equals(userId) && !isAdmin() && !isSuperAdmin())) {
log.warn("User {} trying to perform operation for user {}", currentUserId, userId);
throw new BusinessException(ErrorCode.FORBIDDEN, "无权执行此操作");
}
}
/**
* 强制要求用户已登录,否则抛异常
*
* 用于需要身份验证的操作
*
*
* @throws BusinessException NOT_LOGIN_ERROR 未登录时抛出
*
* 示例:
* public void likeArticle(Long articleId) {
* SecurityUtils.requireAuthenticated();
* // 执行点赞逻辑...
* }
*/
public static void requireAuthenticated() {
if (!isAuthenticated()) {
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "请先登录");
}
}
/**
* 强制要求用户未登录,否则抛异常
*
* 用于登录、注册等接口
*
*
* @throws BusinessException 已登录时抛出
*/
public static void requireNotAuthenticated() {
if (isAuthenticated()) {
throw new BusinessException(ErrorCode.FORBIDDEN, "请先退出当前账号");
}
}
}
```
## **四、使用示例**
```java
@RestController
@RequestMapping("/api/user")
public class UserController {
/**
* 匿名访问(无需登录)
*/
@Anonymous
@GetMapping("/public/info")
public BaseResponse publicInfo() {
return ResultUtils.success("公开信息");
}
/**
* 需要登录(无注解默认需要登录)
*/
@GetMapping("/profile")
public BaseResponse getProfile() {
// 从上下文获取当前用户,无需重复查询
UserVO user = SecurityContext.getCurrentUser();
return ResultUtils.success(user);
}
/**
* 需要单个权限
*/
@RequiresPermission("system:user:list")
@GetMapping("/list")
public BaseResponse> listUsers() {
// ...
}
/**
* 需要任一权限
*/
@RequiresPermission(value = {"system:user:add", "system:user:edit"}, logical = Logical.OR)
@PostMapping("/save")
public BaseResponse saveUser() {
// ...
}
/**
* 需要特定角色
*/
@RequiresRole("admin")
@DeleteMapping("/{id}")
public BaseResponse deleteUser(@PathVariable Long id) {
// ...
}
/**
* 需要任一角色
*/
@RequiresRole(value = {"admin", "manager"}, logical = Logical.OR)
@PutMapping("/status")
public BaseResponse updateStatus() {
// ...
}
}
```
[[04-SecurityUtils 使用示例|SecurityUtils 使用示例]]
---
**项目分区导航**:⬅️ [[02-权限校验最佳实践模型-快捷使用清单|02-权限校验最佳实践模型-快捷使用清单]] | 03-权限校验最佳实践模型代码 | ➡️ [[04-SecurityUtils 使用示例|04-SecurityUtils 使用示例]]