首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Spring AOP 實現權限控制:從切面設計到註解驅動的完整方案

     編輯:Java 綜合教學

前言

權限控制是企業級應用的核心橫切關注點。傳統在 Controller/Service 中硬編碼 if (hasPermission) 導致代碼分散、難維護。Spring AOP 通過切面將權限校驗與業務邏輯解耦,配合自定義註解實現聲明式權限控制,是成熟架構的標準實踐。本文提供從核心概念到生產級代碼的完整落地指南。


一、核心架構設計

1.1 權限模型抽象


// 權限標識:資源:動作 (如 "order:create", "user:delete")
public class Permission {
    private final String resource;
    private final String action;
    // 構造、equals/hashCode、toString
    public static Permission of(String resource, String action) { ... }
    public static Permission parse(String expression) { ... } // "order:create"
}

// 用戶上下文:ThreadLocal 存儲當前登錄用戶
public class SecurityContext {
    private static final ThreadLocal LOCAL = new ThreadLocal<>();
    public static void setPrincipal(UserPrincipal principal) { LOCAL.set(principal); }
    public static UserPrincipal getPrincipal() { return LOCAL.get(); }
    public static void clear() { LOCAL.remove(); }
}

// 用戶主體接口
public interface UserPrincipal {
    Long getUserId();
    Set getRoles();      // 角色集合
    Set getPermissions(); // 權限集合
    default boolean hasPermission(Permission p) { return getPermissions().contains(p); }
    default boolean hasRole(String role) { return getRoles().contains(role); }
}

1.2 自定義權限註解


@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited // 類註解繼承給方法
public @interface RequiresPermission {
    /** 權限表達式,支持 SpEL:#{@permissionEvaluator.hasPermission(#order.id, 'order:update')} */
    String[] value() default {};
    
    /** 邏輯關係:AND(默認) 需全部擁有,OR 任一擁有即可 */
    Logical logical() default Logical.AND;
    
    /** 拒絕訪問時的處理策略 */
    DenyStrategy denyStrategy() default DenyStrategy.THROW_EXCEPTION;
    
    public enum Logical { AND, OR }
    public enum DenyStrategy { THROW_EXCEPTION, RETURN_DEFAULT, CUSTOM_HANDLER }
}

1.3 異常體系


public class AccessDeniedException extends RuntimeException {
    private final Permission requiredPermission;
    public AccessDeniedException(String message, Permission requiredPermission) {
        super(message);
        this.requiredPermission = requiredPermission;
    }
}

// 全局異常處理器
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(AccessDeniedException.class)
    public R handleAccessDenied(AccessDeniedException e) {
        log.warn("權限不足: user={}, required={}", SecurityContext.getPrincipal().getUserId(), e.getRequiredPermission());
        return R.fail(403, "權限不足: " + e.getMessage());
    }
}

二、切面實現核心邏輯


@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 10) // 優先於事務切面執行
@Slf4j
public class PermissionAspect {
    
    private final PermissionEvaluator permissionEvaluator; // SpEL 權限評估器
    
    public PermissionAspect(PermissionEvaluator permissionEvaluator) {
        this.permissionEvaluator = permissionEvaluator;
    }
    
    /** 
     * 切點:所有標註 @RequiresPermission 的方法(類或方法級別)
     * 支持類級別註解作為默認,方法級別覆蓋/追加
     */
    @Pointcut("@annotation(com.example.security.RequiresPermission) " +
              "|| @within(com.example.security.RequiresPermission)")
    public void permissionCheckPointcut() {}
    
    @Around("permissionCheckPointcut()")
    public Object checkPermission(ProceedingJoinPoint pjp) throws Throwable {
        // 1. 獲取註解(方法優先,其次類)
        RequiresPermission methodAnn = getAnnotation(pjp, RequiresPermission.class);
        RequiresPermission classAnn = getClassAnnotation(pjp, RequiresPermission.class);
        
        // 合併註解屬性(方法覆蓋類)
        RequiresPermission effectiveAnn = mergeAnnotations(classAnn, methodAnn);
        if (effectiveAnn == null || effectiveAnn.value().length == 0) {
            return pjp.proceed(); // 無權限要求,直接放行
        }
        
        // 2. 獲取當前用戶
        UserPrincipal principal = SecurityContext.getPrincipal();
        if (principal == null) {
            throw new AccessDeniedException("未登錄或會話失效", null);
        }
        
        // 3. 解析權限表達式(支持 SpEL 動態參數)
        String[] expressions = effectiveAnn.value();
        List required = new ArrayList<>(expressions.length);
        for (String expr : expressions) {
            Permission perm = resolvePermissionExpression(expr, pjp);
            if (perm != null) required.add(perm);
        }
        
        // 4. 校驗權限
        boolean hasPermission = evaluatePermissions(principal, required, effectiveAnn.logical());
        if (!hasPermission) {
            Permission denied = required.stream()
                .filter(p -> !principal.hasPermission(p))
                .findFirst().orElse(required.get(0));
            throw new AccessDeniedException("缺少權限: " + denied, denied);
        }
        
        // 5. 校驗通過,執行目標方法
        return pjp.proceed();
    }
    
    /** 解析 SpEL 表達式:#{@bean.method(#arg)} 或 靜態 "resource:action" */
    private Permission resolvePermissionExpression(String expr, ProceedingJoinPoint pjp) {
        if (expr.startsWith("#{") && expr.endsWith("}")) {
            // SpEL 表達式:#{@permissionService.check(#order.id, 'order:update')}
            String spel = expr.substring(2, expr.length() - 1);
            StandardEvaluationContext context = new StandardEvaluationContext();
            // 綁定方法參數為 SpEL 變量
            String[] paramNames = getParameterNames(pjp);
            Object[] args = pjp.getArgs();
            for (int i = 0; i < paramNames.length; i++) {
                context.setVariable(paramNames[i], args[i]);
            }
            // 註冊 Spring Bean 解析器
            context.setBeanResolver(new BeanFactoryResolver(applicationContext));
            return new SpelExpressionParser().parseExpression(spel).getValue(context, Permission.class);
        }
        return Permission.parse(expr); // 靜態權限字符串
    }
    
    private boolean evaluatePermissions(UserPrincipal principal, List required, RequiresPermission.Logical logical) {
        if (logical == RequiresPermission.Logical.OR) {
            return required.stream().anyMatch(principal::hasPermission);
        }
        return required.stream().allMatch(principal::hasPermission); // AND
    }
    
    // 輔助方法:獲取方法/類註解、合併屬性、獲取參數名(需編譯參數 -parameters 或用 LocalVariableTableParameterNameDiscoverer)
}

三、SpEL 動態權限表達式進階用法

3.1 方法參數綁定


@Service
public class OrderService {
    // 動態權限:只有訂單創建者或管理員可取消
    @RequiresPermission("#{@orderPermissionEvaluator.canCancel(#orderId, principal)}")
    public void cancelOrder(Long orderId) { ... }
}

@Component
public class OrderPermissionEvaluator {
    public boolean canCancel(Long orderId, UserPrincipal principal) {
        Order order = orderRepo.findById(orderId).orElseThrow();
        return order.getUserId().equals(principal.getUserId()) || principal.hasRole("ADMIN");
    }
}

3.2 返回值校驗(後置權限)


@AfterReturning(pointcut = "@annotation(requiresPermission)", returning = "result")
public void checkReturnPermission(JoinPoint jp, RequiresPermission requiresPermission, Object result) {
    // 例如:查詢敏感數據後,檢查用戶是否有導出權限
    if (result instanceof List && requiresPermission.value().length > 0) {
        // 可在此處做數據脫敏或二次校驗
    }
}

四、與 Spring Security 整合(生產推薦)

手寫切面適合輕量級項目,企業級建議接入 Spring Security,復用其完整認證授權體系:


@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/login", "/public/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
    
    // 自定義 PermissionEvaluator 接入 SpEL
    @Bean
    public PermissionEvaluator permissionEvaluator() {
        return new CustomPermissionEvaluator();
    }
}

public class CustomPermissionEvaluator implements PermissionEvaluator {
    @Override
    public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) {
        UserPrincipal principal = (UserPrincipal) auth.getPrincipal();
        return principal.hasPermission(Permission.parse(permission.toString()));
    }
    @Override
    public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission) {
        return hasPermission(auth, null, permission);
    }
}

使用 Spring Security 註解:


@PreAuthorize("hasPermission(#orderId, 'order:update')")           // SpEL
@PreAuthorize("@orderPermissionEvaluator.canCancel(#orderId)")     // Bean 方法
@Secured("ROLE_ADMIN")                                             // 角色
@RolesAllowed({"USER", "ADMIN"})                                   // JSR-250
public void updateOrder(Long orderId, OrderDTO dto) { ... }

五、最佳實踐與避坑指南

場景建議方案關鍵點
靜態權限 (CRUD)@RequiresPermission("order:create")簡單、可讀、易審計
動態權限 (資源所有者)@PreAuthorize("@evaluator.check(#id, principal)")SpEL 表達式、Bean 方法、參數綁定
方法級覆蓋類級註解合併邏輯:方法 value 替換類 value,logical/denyStrategy 方法優先切面中正確合併屬性
異步/線程池調用手動傳遞 SecurityContext 或用 TransmittableThreadLocalThreadLocal 不自動傳遞到子線程
WebFlux 響應式ReactiveSecurityContextHolder + Mono.deferContextual非阻塞上下文傳遞
性能熱點權限緩存 (Caffeine/Redis)、編譯期 SpEL 解析緩存避免每次請求解析 SpEL、查庫
測試@WithMockUser / @WithUserDetails 單元測試切面模擬不同角色權限

六、總結

  1. 切面 + 註解:核心模式,將橫切邏輯與業務解耦
  2. SpEL 表達式:支持動態參數綁定、Bean 方法調用,極大增強表達力
  3. Spring Security 整合:生產首選,復用認證授權、會話管理、CSRF、OAuth2 等完整體系
  4. 權限模型:資源:動作 粒度,支持角色繼承、動態計算、ABAC 屬性訪問控制
  5. 工程化:緩存、異步傳遞、審計日誌、測試覆蓋、文檔自動生成

權限控制不是「加個 if」,而是領域模型、安全架構、基礎設施的系統工程。掌握 AOP 切面設計、SpEL 動態表達、Spring Security 整合三層能力,才能構建既安全又靈活的授權體系。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved