您現在的位置是:首頁 > 籃球

用於 Spring RESTful API 的 OAuth2

  • 由 skysevenqi 發表于 籃球
  • 2022-10-17
簡介您可以使用Postman從 Keycloak 獲取訪問令牌,然後傳送測試請求:建立一個新集合在Authorization選項卡中,選擇 OAuth2 並單擊“Edit token configuration”以輸入以下值:授權型別:授權碼(

授權伺服器怎麼啟用

學習基本的 OAuth2 概念,如何在桌面上設定完整的測試環境,並深入瞭解為 Spring RESTful API 配置安全性。

在本文中,我將首先回顧 OAuth2 的基本概念,然後幫助您在桌面上設定一個完整的測試環境,最後,深入探討為 Spring RESTful API配置安全性。

Google、Facebook、GitHub、Office365 和許多其他公司都使用 OAuth2。為什麼它在每個應用程式中都如此受歡迎和優於普通的舊登入名/密碼?

透過一次身份驗證的單點登入使用者可以無縫訪問您的所有服務,就像在 Gmail、Drive 和 YouTube 之間切換一樣。

使用者憑據在一個地方進行管理,從而減少了攻擊面並減少了使用者資料庫洩露的機會。

登入、登出和使用者管理只需開發一次,節省時間和金錢。

SaaS或本地解決方案已經提供了比您的團隊可以合理實現的更多功能:多因素身份驗證、大多數常見提供商(Google、Facebook、GitHub 等)的身份聯合、LDAP聯結器、OIDC 合規性等。

Keycloak 當然可以做到所有這些,但是在您最喜歡的搜尋引擎中輸入“OIDC SaaS”並檢查彈出了多少結果。

足夠的 OAuth2 背景

OAuth2 定義了

4 個參與者。

在編寫 Spring 配置時,必須清楚地瞭解誰是誰:

資源所有者

:將其視為終端使用者。最常見的是,這是一個自然人,但可以是使用客戶端憑據進行身份驗證的 Web 服務(見下文)。在 Spring security 中,它由

Authentication

security-context 中的 表示。

資源伺服器

:負責服務資源的 API(最常見的是 REST)

Client

:需要訪問一個或多個資源伺服器上資源的軟體;它以自己的名義(使用客戶端憑據)或代表擁有訪問令牌的終端使用者執行此操作。

授權伺服器

:釋出和驗證身份和身份委託的伺服器(終端使用者允許客戶端代表他執行的操作)

OAuth2 流程

有很多,但有兩個是我們感興趣的:

授權碼

這可能是最有用的一個。它用於對終端使用者(自然人)進行身份驗證。

用於 Spring RESTful API 的 OAuth2

在 OpenID 環境中,資源伺服器在啟動時或在處理第一個請求之前從標準路徑獲取授權伺服器配置。

授權伺服器處理身份驗證(使用表單、cookie、生物識別或任何它喜歡的東西),然後使用一次使用的程式碼將使用者重定向到客戶端。

客戶端聯絡授權伺服器以交換訪問令牌的程式碼(以及可選的重新整理和 ID 令牌)。

客戶端使用標頭中的訪問令牌向資源伺服器傳送請求

Authorization

令牌驗證和詳細資訊檢索的兩種情況,具體取決於資源伺服器配置,如下所示:

JWT 解碼器讀取令牌並使用授權伺服器公鑰(在啟動時下載一次)對其進行驗證。

請求被髮送到授權伺服器自省端點(對於每個請求)。

客戶憑證

客戶端將客戶端 ID 和機密傳送到授權伺服器,授權伺服器返回一個訪問令牌以用於驗證客戶端本身(無使用者上下文)。這必須限於在您信任的伺服器上執行的客戶端(能夠保守秘密實際上是“秘密”),並且不包括在瀏覽器或移動應用程式中執行的所有服務(可以對程式碼進行逆向工程以讀取秘密)。

Tokens

令牌代表資源所有者的身份以及客戶可以代表他做什麼,就像你可以給別人投票給你的紙質代理。它至少包含以下屬性:

發行

者:發出令牌的授權伺服器(警察或類似的人,他們證明了提供和接受代理的人的身份)

主題

:資源所有者唯一識別符號(授予代理的人)

範圍

:此令牌可用於什麼(資源所有者是否授予代理投票、管理銀行賬戶、在郵局獲取包裹等)

Expiry

: 直到何時可以使用此令牌

JWT

JWT

是JSON Web Token。它主要用作 OAuth2 的訪問或 ID 令牌。

JWT 可以由JWT 解碼器

自行驗證,它只需要一個授權伺服器公共簽名金鑰。

Opaque Tokens

可以使用不透明令牌(任何格式,包括 JWT),但它需要

自省

:客戶端和資源伺服器必須向授權伺服器傳送請求以確保令牌有效並獲取令牌“屬性”(相當於 JWT“宣告” )。與 JWT 解碼相比,此過程可能會對效能產生嚴重影響。

Access Token

這是客戶端在其對資源伺服器的請求中作為承載授權標頭髮送的令牌。訪問令牌內容應該只關注授權和資源伺服器(客戶端不應嘗試讀取訪問令牌)。

Refresh Token

該令牌將由客戶端傳送到授權伺服器,以在它到期時(或者最好是之前)獲得一個新的訪問令牌。

身份令牌

ID 令牌是 OAuth2 的 OpenID 擴充套件的一部分,是客戶端用來獲取使用者資訊的令牌。

範圍

範圍定義了使用者允許客戶以他的名義做什麼(而不是允許使用者在系統中做什麼)。您可能會將其視為在客戶端訪問資源所有者資源之前應用到資源所有者資源的掩碼。

OpenID

這是 OAuth2 之上的標準,其中包括標準宣告。

授權伺服器要求

要繼續下面的教程,我們需要一個 OpenID 授權伺服器,其中包含一些已宣告的客戶端和資源所有者。為簡單起見,我們將使用由 Quarkus 提供支援的獨立 Keycloak 發行版。

伺服器配置

如果您的主機還沒有 SSL 證書,請生成一個。

解壓 Keycloak 存檔後,編輯

conf/keycloak。conf

檔案:

屬性檔案

http-port=8442https-key-store-file=/path/to/self_signed。jkshttps-key-store-password=change-mehttps-port=8443

一切就緒:只需從

bin/kc。bat start-dev

or開始

bin/kc。sh start-dev

並連線到 https://localhost:8443。

客戶

那麼需要兩個不同的客戶:

spring-addons-confidential

將在啟用“客戶端身份驗證”和“服務帳戶角色”的情況下建立。我們信任的應用程式將使用此客戶端,在我們信任的伺服器上執行,以使用客戶端憑據流聯絡授權伺服器。

用於 Spring RESTful API 的 OAuth2

spring-addons-public

將在沒有客戶端身份驗證和“標準流程”的情況下建立。Web/移動應用程式和 REST 客戶端(如 Postman)將使用此客戶端對使用者進行身份驗證。

用於 Spring RESTful API 的 OAuth2

您必須為公共客戶端定義一些有效的重定向 URL:

用於 Spring RESTful API 的 OAuth2

新增 URL 後不要忘記儲存。

角色

我們將建立一個名為

NICE

或者,您可以在客戶端級別定義角色。如果您這樣做,還請從客戶端詳細資訊

-> 客戶端範圍 -> spring-addons-[public|confidential]-dedicated -> 新增對映器 -> 從預定義的對映器中啟用“客戶端角色”對映器。

使用者

讓我們建立兩個使用者:

brice

NICE

角色(應該被允許得到問候)

igor

無角色(應禁止打招呼)

Spring Boot 資源伺服器安全配置

我們將看到,藉助Spring Boot,我們可以在幾分鐘內構建一個安全的資源伺服器,包括安全規則單元測試。

要求

Spring Boot 2。7 及更高版本:不推薦使用

WebSecurityConfigurerAdapter

使用者許可權應對映自Keycloak 放置使用者角色的宣告

realm_access。roles

resource-access。spring-addons-confidential。roles

宣告

resource-access。spring-addons-public。roles

具有 GET 端點的控制器僅在授予使用者

NICE

許可權時才返回問候語(如果身份驗證丟失/無效,則返回 401,如果缺少

NICE

角色,則返回 403)

CORS:“純”資源伺服器所必需的;從另一個套接字、主機或域提供的 UI 元件;需要跨域訪問

“無狀態”會話管理:無 servlet 會話;客戶端狀態由 URI 和訪問令牌管理

CSRF 保護(由於無狀態會話管理而使用 cookie 儲存庫)

OpenAPI 規範(來自)

spring-doc-openapi

以及探針應該可供匿名訪問

readinessliveness

@Controllers

所有其他路由都僅限於經過身份驗證的使用者(使用 註釋的方法上的細粒度安全規則

@PreAuthorize

401 未授權(而不是 302 重定向到登入)當向受保護資源發出請求且授權標頭丟失或無效時

如果啟用 SSL,則強制使用 HTTPS

spring-boot-starter-oauth2-資源伺服器

Spring Boot 提供了一個庫來簡化資源伺服器的安全配置:

spring-boot-starter-oauth2-resource-server

。 讓我們用它來實現上述要求。

開啟Spring initializr並生成具有以下依賴項的專案:

Spring Web

OAuth2 Resource Server

Spring Boot Actuator

Lombok

用於 Spring RESTful API 的 OAuth2

下載並解壓後,新增以下依賴項:

XML

org。springdoc springdoc-openapi-security 1。6。9 org。springdoc springdoc-openapi-ui 1。6。9 org。springframework。boot spring-boot-starter-test test

現在,我們可以透過 Spring Boot 2。7+ 的方式配置安全性:提供一個

SecurityFilterChain

bean 而不是擴充套件 deprecated

WebSecurityConfigurerAdapter

爪哇

@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)public class WebSecurityConfig { @Bean public SecurityFilterChain filterChain( HttpSecurity http, Converter authenticationConverter, ServerProperties serverProperties) throws Exception { // Enable OIDC http。oauth2ResourceServer()。jwt()。jwtAuthenticationConverter(authenticationConverter); // Enable anonymous http。anonymous(); // Enable and configure CORS http。cors()。configurationSource(corsConfigurationSource()); // State-less session (client state in JWT token only) http。sessionManagement()。sessionCreationPolicy(SessionCreationPolicy。STATELESS); // Enable CSRF with cookie repo because of state-less session-management http。csrf()。csrfTokenRepository(new CookieCsrfTokenRepository()); // Return 401 instead of redirect to login page http。exceptionHandling()。authenticationEntryPoint((request, response, authException) -> { response。addHeader(HttpHeaders。WWW_AUTHENTICATE, “Basic realm=\”Restricted Content\“”); response。sendError(HttpStatus。UNAUTHORIZED。value(), HttpStatus。UNAUTHORIZED。getReasonPhrase()); }); // If SSL enabled, disable http (https only) if (serverProperties。getSsl() != null && serverProperties。getSsl()。isEnabled()) { http。requiresChannel()。anyRequest()。requiresSecure(); } else { http。requiresChannel()。anyRequest()。requiresInsecure(); } // Route security: authenticated to all routes but actuator and Swagger-UI // @formatter:off http。authorizeRequests() 。antMatchers(“/actuator/health/readiness”, “/actuator/health/liveness”, “/v3/api-docs/**”)。permitAll() 。anyRequest()。authenticated(); // @formatter:on return http。build(); } public interface Jw2tAuthoritiesConverter extends Converter> { } public interface Jwt2AuthenticationConverter extends Converter { } @Bean public Jwt2AuthenticationConverter authenticationConverter(Jw2tAuthoritiesConverter authoritiesConverter) { return jwt -> new JwtAuthenticationToken(jwt, authoritiesConverter。convert(jwt)); } @SuppressWarnings(“unchecked”) @Bean public Jw2tAuthoritiesConverter authoritiesConverter() { // This is a converter for roles as embedded in the JWT by a Keycloak server // Roles are taken from both realm_access。roles & resource_access。{client}。roles return jwt -> { final var realmAccess = (Map) jwt。getClaims()。getOrDefault(“realm_access”, Map。of()); final var realmRoles = (Collection) realmAccess。getOrDefault(“roles”, List。of()); final var resourceAccess = (Map) jwt。getClaims()。getOrDefault(“resource_access”, Map。of()); // We assume here you have a “spring-addons-public” client configure in your Keycloak realm final var publicClientAccess = (Map) resourceAccess。getOrDefault(“spring-addons-public”, Map。of()); final var publicClientRoles = (Collection) clientAccess。getOrDefault(“roles”, List。of()); final var confidentialClientAccess = (Map) resourceAccess。getOrDefault(“spring-addons-confidential”, Map。of()); final var confidentialClientRoles = (Collection) clientAccess。getOrDefault(“roles”, List。of()); return Stream。concat(realmRoles。stream(), Stream。concat(publicClientRoles。stream(), confidentialClientRoles。stream())) 。map(SimpleGrantedAuthority::new) 。toList(); }; } private CorsConfigurationSource corsConfigurationSource() { // Very permissive CORS config。。。 final var configuration = new CorsConfiguration(); configuration。setAllowedOrigins(Arrays。asList(“*”)); configuration。setAllowedMethods(Arrays。asList(“*”)); configuration。setAllowedHeaders(Arrays。asList(“*”)); configuration。setExposedHeaders(Arrays。asList(“*”)); // Limited to API routes (neither actuator nor Swagger-UI) final var source = new UrlBasedCorsConfigurationSource(); source。registerCorsConfiguration(“/greet/**”, configuration); return source; }}

當然,我們還需要一個安全的 REST

@Controller

爪哇

@RestController@RequestMapping(“/greet”)@PreAuthorize(“isAuthenticated()”)public class GreetingController { @GetMapping() @PreAuthorize(“hasAuthority(‘NICE’)”) public String getGreeting(JwtAuthenticationToken auth) { return String 。format( “Hi %s! You are granted with: %s。”, auth。getToken()。getClaimAsString(StandardClaimNames。PREFERRED_USERNAME), auth。getAuthorities()。stream()。map(GrantedAuthority::getAuthority)。collect(Collectors。joining(“, ”, “[”, “]”))); }}

最後,我們需要一些條目

application。properties

屬性檔案

spring。security。oauth2。resourceserver。jwt。issuer-uri=https://localhost:8443/realms/mastermanagement。endpoint。health。probes。enabled=truemanagement。health。readinessstate。enabled=truemanagement。health。livenessstate。enabled=truemanagement。endpoints。web。exposure。include=*spring。lifecycle。timeout-per-shutdown-phase=30slogging。level。org。springframework。security。web。csrf=DEBUG

該應用程式現在應該在埠 8080 上執行,並公開一個

brice

只能訪問的安全端點。

您可以使用Postman從 Keycloak 獲取訪問令牌,然後傳送測試請求:

建立一個新集合

Authorization

選項卡中,選擇 OAuth2 並單擊“

Edit token configuration

”以輸入以下值:授權型別:授權碼(帶 PKCSE)回撥地址:https://localhost:4200(或者你在 Keycloak 中配置 spring-addons-public 客戶端時設定的)認證網址:https://localhost:8443/realms/master/protocol/openid-connect/auth訪問令牌網址:https://localhost:8443/realms/master/protocol/openid-connect/token客戶 ID:spring-addons-public範圍:OpenID spring-addons-public-dedicated profile email offline_access

在您進行身份驗證後

brice

,單擊“

使用令牌

”並將新的 GET 請求新增到 http://localhost:8080/greet。您應該會收到一個問候語(但如果您以 身份驗證,則不會

igor

)。

配置縮減

我們在 web-security 配置中實現的功能列表是我們在大多數資源伺服器中需要的非常通用的東西。我們可以使用替代的spring-addons啟動器之一,而不是複製此程式碼,它在 4 個變體中被拒絕,用於 Web MVC 與 WebFlux 和 JWT 解碼器與內省的組合。

當我們使用 JWT 解碼器編寫 servlet 應用程式時,我們將替換

spring-boot-starter-oauth2-resource-server

spring-addons-webmvc-jwt-resource-server

XML

com。c4-soft。springaddons spring-addons-webmvc-jwt-resource-server 5。1。3

我們現在可以刪除幾乎所有的網路安全配置:

@EnableGlobalMethodSecurity(prePostEnabled = true)public class WebSecurityConfig {}

只需提供一些屬性:

屬性檔案

# shoud be set to where your authorization-server iscom。c4-soft。springaddons。security。issuers[0]。location=https://localhost:8443/realms/master# shoud be configured with a list of private-claims this authorization-server puts user roles into# below is default Keycloak conf for a `spring-addons` client with client roles mapper enabledcom。c4-soft。springaddons。security。issuers[0]。authorities。claims=realm_access。roles,resource-access。spring-addons-confidential。roles,resource_access。spring-addons-public。rolescom。c4-soft。springaddons。security。permit-all=/actuator/health/readiness,/actuator/health/liveness,/v3/api-docs/**# use IDE auto-completion or see SpringAddonsSecurityProperties javadoc for complete configuration properties listmanagement。endpoint。health。probes。enabled=truemanagement。health。readinessstate。enabled=truemanagement。health。livenessstate。enabled=truemanagement。endpoints。web。exposure。include=*spring。lifecycle。timeout-per-shutdown-phase=30s

Bootiful,不是嗎?

這裡沒有什麼神奇之處:它只是一個帶有一些@ConditionalOnMissingBean 定義的Spring Boot 模組,它們提供了可以輕鬆覆蓋的合理預設值。

由於 spring-addons 不包含

JwtAuthenticationToken

,而是一個通用的

OAthentication

,因此需要對我們的控制器進行輕微修改:

@RestController@RequestMapping(“/greet”)@PreAuthorize(“isAuthenticated()”)public class GreetingController { @GetMapping() @PreAuthorize(“hasAuthority(‘NICE’)”) public String getGreeting(OAuthentication auth) { return String 。format( “Hi %s! You are granted with: %s。”, auth。getClaims()。getPreferredUsername(), auth。getAuthorities()。stream()。map(GrantedAuthority::getAuthority)。collect(Collectors。joining(“, ”, “[”, “]”))); }}

單元測試

我們看到了如何使用類似的表示式將

R

ole

B

ased

A

ccess Control新增到我們的 Spring 方法中,這些表示式

基於

@PreAuthorize(“hasAuthority(‘NICE’)”)

JWT 訪問令牌中包含的身份和角色進行斷言(或暴露在授權伺服器自省端點上)。

現在讓我們看看如何對這些安全規則進行單元測試。

spring-security-test

提供

MockMvc

後處理器和

WebTestClient

修改器來填充測試安全上下文,

JwtAuthenticationToken

或者

BearerTokenAuthentication

它們是

Authentication

分別具有 JWT 解碼器或令牌自省的應用程式的預設設定。

import static org。springframework。security。test。web。servlet。request。SecurityMockMvcRequestPostProcessors。jwt;@WebMvcTest(GreetingController。class)@Import(WebSecurityConfig。class)class GreetingControllerTest { @MockBean JwtDecoder jwtDecoder; @Autowired MockMvc mockMvc; @Test void greetBrice() throws Exception { mockMvc。perform(get(“/greet”)。with(jwt()。jwt(jwt -> { jwt。claim(“preferred_username”, “Brice”); })。authorities(List。of(new SimpleGrantedAuthority(“NICE”), new SimpleGrantedAuthority(“AUTHOR”))))) 。andExpect(status()。isOk()) 。andExpect(content()。string(“Hi Brice! You are granted with: [NICE, AUTHOR]。”)); }}

但是,這有一些限制:

只能

@Controller

測試安全性(其他

@Component

單元測試不在 MockMvc 或 WebTestClient 請求的上下文中執行)。

它在測試請求定義中造成了一些混亂。

作為替代方案,我們可以新增對

spring-addons-webmvc-jwt-test

XML

com。c4-soft。springaddons spring-addons-webmvc-jwt-test 5。1。3 test

它包含類似於 的測試註釋

@WithMockUser

,注入其他型別的

Authentication

@WithMockJwtAuth

構建一個

JwtAuthenticationToken

(JWT 解碼器的預設值)。

@WithMockBearerTokenAuthentication

構建一個

BearerTokenAuthentication

(令牌自省的預設值)。

@OpenId

構建一個

OAuthentication

(spring-addons 啟動器的預設值)。

上面的測試變成:

@WebMvcTest(GreetingController。class)@AutoConfigureAddonsSecurityWebmvcIntrospecting@Import({ WebSecurityConfig。class })class GreetingControllerTest { @Autowired MockMvcSupport api; @Test @OpenId( authorities = { “NICE”, “AUTHOR” }, claims = @OpenIdClaims(preferredUsername = “Brice”) void greetBrice() throws Exception { api。get(“/greet”) 。andExpect(status()。isOk()) 。andExpect(content()。string(“Hi Brice! You are granted with: [NICE, AUTHOR]。”)); }}

如果您還沒有應用上面的“配置縮減”:

spring-addons-webmvc-jwt-test

依賴項替換為

spring-addons-oauth2-test

替換

@OpenId

@WithMockJwtAuth

替換

MockMvcSupport

MockMvc

替換

get(“/greet”)

perform(get(“/greet”))

刪除

@AutoConfigureSecurityAddons

新增

@MockBean JwtDecoder jwtDecoder;

請務必匯入所有安全配置檔案。

仔細檢查您是否將 。secure(true) 和 。csrf() 新增到每個請求構建器,如果它在您的安全配置中被啟用(MockMvcSupport 會自動執行它)。

所以是的,Spring 外掛也可以讓你的單元測試變得輕鬆。

走得更遠

我省略了客戶端的配置。您應該從經過認證的實現中選擇一個庫。我個人對 Angular 更有經驗,並且更喜歡angular-auth-oidc-client。

要了解如何從 spring-addons 覆蓋預設 @Beans,您可以參考這個高階教程,其中包括:

解析授權伺服器提供的私有宣告

擴充套件

Authentication

實施以實施不僅僅基於角色的安全規則

豐富 Spring 安全 DSL(

@PreAuthorize

@PostFilter

表示式)

謝謝大家閱讀,喜歡的朋友請關注點贊轉發,帶你瞭解最新技術趨勢。

Top