傳統 RBAC(Role-Based Access Control)以「用戶-角色-權限」三元組為核心,但在 B/S 架構下面臨挑戰:
if (hasPerm(...)),違反開閉原則。本文提出「角色-表單」權限模型,結合 AspectJ 編譯期織入,實現零侵入、細顆粒度、熱生效的權限控制。
User (用戶) ←N:M→ Role (角色) ←N:M→ Permission (權限/表單功能點)
↑
Session (會話上下文,持有當前激活角色集)
| 表名 | 關鍵欄位 | 說明 |
|---|---|---|
| sys_user | id, username, password_hash, status | 用戶主檔 |
| sys_role | id, code, name, description | 角色定義 |
| sys_permission | id, form_code, action_code, description | 表單功能點,如 "ORDER_FORM:SUBMIT" |
| sys_role_perm | role_id, perm_id | 角色-權限關聯 |
| sys_user_role | user_id, role_id | 用戶-角色關聯 |
關鍵設計:Permission 綁定 form_code + action_code,對應前端具體表單提交/按鈕操作,而非模糊的菜單 URL。
| 特性 | Spring AOP (代理) | AspectJ (編譯期/類載入期) |
|---|---|---|
| 攔截範圍 | 僅 public 方法、外部調用 | 任意方法、構造函數、欄位存取、異常拋出 |
| 性能 | 代理鏈開銷 | 零運行時開銷,直接內聯字節碼 |
| 依賴 | Spring 容器管理 Bean | 無容器依賴,POJO 即可織入 |
| 熱部署 | 支援 | 需 Load-Time Weaving (LTW) 或重新編譯 |
權限屬橫切關注點,需攔截 Controller 內部私有方法、Service 異常流等,AspectJ 更適合。
@Aspect
public class FormPermissionAspect {
@Autowired private PermissionService permService; // 注入權限查詢服務
/**
* 切點:所有標註 @RequiresFormPerm 的方法執行
* 也可用 execution(* com.example.web..*Controller.*(..)) 捕獲整層
*/
@Pointcut("@annotation(com.example.annotation.RequiresFormPerm)")
public void formPermPointcut() {}
/**
* 環繞通知:權限校驗 + 原方法執行
*/
@Around("formPermPointcut()")
public Object checkPermission(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature sig = (MethodSignature) pjp.getSignature();
RequiresFormPerm ann = sig.getMethod().getAnnotation(RequiresFormPerm.class);
String formCode = ann.formCode();
String actionCode = ann.actionCode();
// 從 ThreadLocal/Session 獲取當前用戶上下文
UserContext ctx = UserContextHolder.get();
if (ctx == null || !permService.hasFormPermission(ctx.getUserId(), formCode, actionCode)) {
throw new AccessDeniedException("無權限操作表單 [" + formCode + "] 動作 [" + actionCode + "]");
}
return pjp.proceed(); // 通過校驗,執行原方法
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME) // AspectJ 需保留至運行期讀取註解屬性
public @interface RequiresFormPerm {
String formCode(); // 表單代碼,如 "PURCHASE_ORDER"
String actionCode(); // 動作代碼,如 "APPROVE", "REJECT", "SAVE_DRAFT"
}
// Controller 使用
@Controller
@RequestMapping("/purchase")
public class PurchaseOrderController {
@RequiresFormPerm(formCode = "PURCHASE_ORDER", actionCode = "APPROVE")
@PostMapping("/approve/{id}")
public Result approve(@PathVariable Long id) { ... }
@RequiresFormPerm(formCode = "PURCHASE_ORDER", actionCode = "SAVE_DRAFT")
@PostMapping("/draft")
public Result saveDraft(@RequestBody PurchaseOrderDTO dto) { ... }
}
對於遵循命名慣例的專案,可用一個切面覆蓋整個包:
@Pointcut("execution(* com.example.web..*Controller.*(..))")
public void controllerLayer() {}
@Around("controllerLayer()")
public Object globalFormPermCheck(ProceedingJoinPoint pjp) throws Throwable {
// 從方法名/類名推導 formCode/actionCode,或讀取方法級註解
// 若無註解則放行(預設公開)或攔截(預設拒絕),視安全策略而定
...
}
Maven 專案引入 aspectj-maven-plugin:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.14.0</version>
<configuration>
<complianceLevel>17</complianceLevel>
<source>17</source>
<target>17</target>
<showWeaveInfo>true</showWeaveInfo>
<verbose>true</verbose>
<Xlint>ignore</Xlint>
</configuration>
<executions>
<execution>
<goals><goal>compile</goal></goals>
</execution>
</executions>
</plugin>
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public Result handleAccessDenied(AccessDeniedException e) {
return Result.fail(403, e.getMessage());
}
}
| 維度 | 傳統硬編碼 | 本方案 (AspectJ + RBAC) |
|---|---|---|
| 代碼侵入性 | 高,業務邏輯混雜權限判斷 | 零侵入,註解聲明式 |
| 顆粒度 | 通常僅到 URL/Action | 表單功能點級 (按鈕/欄位/操作) |
| 維護性 | 修改權限需改代碼重啟 | 資料庫配置即時生效(配合緩存版本號) |
| 複用性 | 難以跨模塊復用 | 切面集中管理,自動應用於所有受管方法 |
| 測試性 | 需 Mock 權限上下文 | 單元測試可直接調用方法,整合測試驗證切面織入 |
架構師建議:中小專案優先用 Spring Security + @PreAuthorize,架構簡單、生態完善。大型系統、需極致效能、細顆粒度欄位級控制、或非 Spring 容器環境時,AspectJ 編譯期織入是更專業的選擇。