SpringBoot:使用AOP對API請求授權驗證 - George
在今天的文章中,我將討論如何利用 Spring AOP 在端點級別授權 API 請求。
假設我們構建了一個 API 來跟蹤啟用了基本身份驗證的 Spring Security 的每月費用,並且我們希望根據經過身份驗證的使用者的許可權來授權請求。
簡而言之,身份驗證是驗證使用者身份以確定他們聲稱的身份的過程,授權 是驗證使用者的許可權/角色/許可權以訪問特定資源的過程。
為簡單起見,我們 只有兩個許可權:USER並且ADMIN我們可以考慮身份驗證過程已經根據使用者名稱/密碼組合使用正確的授予許可權填充 Spring Security Context。
public enum SecurityAuthorities { USER, ADMIN } |
我們的一些 API 端點需要USER許可權,而其他端點需要許可權ADMIN(用於使用者管理和其他管理要求)。
我們如何獲得自定義許可權的授權?
使用 Spring,我們可以@PreAuthorize在端點級別使用眾所周知的註釋:
@PreAuthorize("hasAuthority('USER')") @GetMapping("/api/v1/expenses", produces = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(OK) public ResponseEntity<GetExpensesResponseDto> getExpenses(){ // irrelevant code here } |
正如您所看到的,我們hasAuthority('USER')為@PreAuthorize註釋指定了 值,它轉換為:經過身份驗證的使用者必須具有USER訪問此端點的許可權。如果缺少此許可權,則將返回 403 Forbidden。
將許可權值指定為純文字的主要問題是可維護性以及容易出現拼寫錯誤和破壞性更改的事實。想象一下,我們在SecurityAuthorities想將許可權名稱從USER 重構為CUSTOMER.
這意味著您需要確保找到所有找到'USER'字串的位置並將其替換為'CUSTOMER'; 再加上你需要在 25 個不同的地方做這件事,這很快就會變得很痛苦。那麼為什麼不使用我們的列舉類呢?
@PreAuthorize("hasAuthority(T(com.example.SecurityAuthorities).USER)") @GetMapping("/api/v1/expenses", produces = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(OK) public ResponseEntity<GetExpensesResponseDto> getExpenses(){ // irrelevant code here } |
使用 enum 類比純文字更容易一些,因為在編譯時,您可以在完全限定的包名稱之後放置正確的值,而無需擔心拼寫錯誤。此外,如果您重新命名許可權名稱或將列舉移動到另一個包中,更改將反映在此處......但是如果您刪除列舉,編譯器根本不會抱怨,這是一個大問題,因為它隱藏了您的端點期望的授權事實上不再存在。
即使@PreAuthorize註釋解決了授權過程並且使用起來非常簡單,我們仍然需要一個更清晰、更易於維護的解決方案,以便直接使用我們的列舉值,而無需限定包名稱或硬編碼字串,同時確保編譯時安全。AOP 來拯救你了!
AOP 解決方案
長話短說AOP 允許您向現有程式碼新增額外的行為,而無需修改程式碼本身。
我們要做的基本上是實現一個方法,該方法將在新請求到達我們的端點時由 AOP 自動呼叫,並接收一組許可權以檢查經過身份驗證的使用者 (the Principal) 以決定授權的結果. 請記住,我們端點的現有程式碼只會發生一個小改動:用@PreAuthorize自定義的註釋替換註釋。讓我們繼續…
首先,我們需要一種方法來指定端點需要檢查自定義許可權的哪個子集。為此,我們將建立一個自定義註釋以直接使用我們的列舉而不是字串:
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Order(Ordered.HIGHEST_PRECEDENCE) public @interface HasEndpointAuthorities { SecurityAuthorities[] authorities(); } |
接下來,我們需要更新我們的端點,如下所示:
@HasEndpointAuthorities(authorities = { SecurityAuthorities.USER }) @GetMapping("/api/v1/expenses", produces = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(OK) public ResponseEntity<GetExpensesResponseDto> getExpenses(){ // irrelevant code here } |
這個自定義註解的好處是,如果我們重構SecurityAuthorities列舉,所有更改都會立即反映,如果我們刪除它,編譯器會尖叫報錯。
在我們定義了我們的自定義註釋之後,我們需要建立一個方法,該方法將在針對我們用@HasEndpointAuthorities註釋的端點之一的每個請求上呼叫。
@Aspect @Component @Slf4j public class HasEndpointAuthoritiesAspect { @Before("within(@org.springframework.web.bind.annotation.RestController *) && @annotation(authorities)") public void hasAuthorities(final JoinPoint joinPoint, final HasEndpointAuthorities authorities) { final SecurityContext securityContext = SecurityContextHolder.getContext(); if (!Objects.isNull(securityContext)) { final Authentication authentication = securityContext.getAuthentication(); if (!Objects.isNull(authentication)) { final String username = authentication.getName(); final Collection<? extends GrantedAuthority> userAuthorities = authentication.getAuthorities(); if (Stream.of(authorities.authorities()).noneMatch(authorityName -> userAuthorities.stream().anyMatch(userAuthority -> authorityName.name().equals(userAuthority.getAuthority())))) { log.error("User {} does not have the correct authorities required by endpoint", username); throw new ApiException(DefaultExceptionReason.FORBIDDEN); } } else { log.error("The authentication is null when checking endpoint access for user request"); throw new ApiException(DefaultExceptionReason.UNAUTHORIZED); } } else { log.error("The security context is null when checking endpoint access for user request"); throw new ApiException(DefaultExceptionReason.FORBIDDEN); } } } |
注意類別的的註釋@Aspect和方法級別的@Before註釋:第一個註釋了 Spring AOP 將使用的類,第二個解釋如下:
@Before註釋是在被@hasAuthorities註釋了的方法倍呼叫之前呼叫, /api/v1/expenses端點如果被訪問,在真正呼叫該API對應的方法getExpenses 之前hasAuthorities 必須首先被呼叫。如果hasAuthorities 拋錯如403, 真正業務方法getExpenses方法也不會被呼叫,403將作為響應返回。
就是這樣!您現在可以通過自定義註釋使用一行程式碼來控制檢查每個端點的許可權,並且您還可以充分利用列舉類。
相關文章
- 使用請求頭認證來測試需要授權的 API 介面API
- 驗證與授權
- OAuth 2.0 授權碼請求OAuth
- 網路驗證之授權碼使用
- 通過 Passport 實現 API 請求認證(移動端的密碼授權令牌)PassportAPI密碼
- 透過 Passport 實現 API 請求認證(移動端的密碼授權令牌)PassportAPI密碼
- 使用 Laravel 請求類來驗證表單請求Laravel
- Django(59)驗證和授權Django
- 細說API - 認證、授權和憑證API
- SpringBoot--- 使用SpringSecurity進行授權認證Spring BootGse
- 利用JAAS對使用者進行驗證和授權遇到的問題
- .Net Core官方的 JWT 授權驗證JWT
- 搞定了!OAuth2使用驗證碼進行授權OAuth
- GOLANG Web請求引數驗證GolangWeb
- 使用總結:Laravel 的 passport 密碼授權模式完成 API 認證LaravelPassport密碼模式API
- Asp.net中基於Forms驗證的角色驗證授權ASP.NETORM
- 使用 gorilla/mux 進行 HTTP 請求路由和驗證GoUXHTTP路由
- 認證授權
- Laravel POST 請求 API 介面,使用自定義表單驗證,驗證失敗跳轉回首頁的問題記錄LaravelAPI
- 認證授權方案之授權初識
- 驗證LOCK請求的FIFO機制
- 認證授權方案之授權揭祕 (上篇)
- 【Azure API 管理】在APIM中使用客戶端證書驗證API的請求,但是一直提示錯誤"No client certificate received."API客戶端client
- 對列授權
- .NET Core使用 CancellationToken 取消API請求API
- Laravel 自定義表單請求驗證忽略某些欄位驗證Laravel
- Laravel 5 API 服務端支援簽名授權認證LaravelAPI服務端
- Welcome to YARP - 5.身份驗證和授權
- CORS跨域限制以及預請求驗證CORS跨域
- 【認證與授權】Spring Security的授權流程Spring
- Laravel 使用 ApiToken 認證請求LaravelAPI
- 瞭解如何使用JSON Web令牌(JWT)實現訪問授權驗證JSONWebJWT
- Springboot中Rest風格請求對映如何開啟並使用Spring BootREST
- 鴻蒙Next許可權申請全攻略:系統授權與使用者授權之道鴻蒙
- SpringBoot使用AOPSpring Boot
- 對api請求封裝的探索和總結API封裝
- 使用MITMProxy轉發請求到本地、儲存鑑權給本地請求MIT
- 關於httpclient 請求https (如何繞過證書驗證)HTTPclient