--- 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 使用示例]]