背景
8月17日,Spring官方宣布 Spring Authorization Server 已正式脫離實驗狀态,并進入Spring-Project家族!
Spring Authorization Server (以下簡稱 SAS)是 Spring 團隊最新開發适配 OAuth 協定的授權伺服器項目,旨在替代原有的 Spring Security OAuth Server。
經過半年的開發和孵化,目前已經釋出了 0.2.0 版本,已支援授權碼、用戶端、重新整理、登出等 OAuth 協定。
目前 SAS 項目已經遷移至官方正式倉庫維護,成為官方的正式子項目。
前置知識
Oauth 2
OAuth 2.0 規定了四種獲得令牌的流程。你可以選擇最适合自己的那一種,向第三方應用頒發令牌。下面就是這四種授權方式。
- 授權碼(authorization-code)
- 隐藏式(implicit)
- 密碼式(password):
- 用戶端憑證(client credentials)
注意,不管哪一種授權方式,第三方應用申請令牌之前,都必須先到系統備案,說明自己的身份,然後會拿到兩個身份識别碼:用戶端 ID(client ID)和用戶端密鑰(client secret)。這是為了防止令牌被濫用,沒有備案過的第三方應用,是不會拿到令牌的。
JWT、JWS、JWK的概念
JWT:指的是 JSON Web Token,由 header.payload.signture 組成。不存在簽名的JWT是不安全的,存在簽名的JWT是不可竄改的。JWS:指的是簽過名的JWT,即擁有簽名的JWT。JWK:既然涉及到簽名,就涉及到簽名算法,對稱加密還是非對稱加密,那麼就需要加密的 密鑰或者公私鑰對。此處我們将 JWT的密鑰或者公私鑰對統一稱為 JSON WEB KEY,即 JWK。
maven 依賴
<!--oauth2 server-->
<dependency>
<groupId>org.springframework.security.experimental</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.1.0</version>
</dependency>
<!--security dependency-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
初始化配置
- 由于官方還未提供對應的 Spring Boot Starter 自動化配置,需要自己配置相關的 @Bean
- 本配置基于 Spring Boot 2.4.2
@Configuration
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
public class AuthServerConfiguration {
// 定義 spring security 攔擊鍊規則
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.formLogin(withDefaults());
return http.build();
}
// 建立預設登入使用者 lengleng / 123456
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.builder()
.username("lengleng")
.password("{noop}123456")
.authorities("ROLE_USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
// 建立預設的bean 登入用戶端,基于 授權碼、 重新整理令牌的能力
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient client = RegisteredClient.withId("pig")
.clientId("pig")
.clientSecret("pig")
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.authorizationGrantTypes(authorizationGrantTypes -> {
authorizationGrantTypes.add(AuthorizationGrantType.AUTHORIZATION_CODE);
authorizationGrantTypes.add(AuthorizationGrantType.REFRESH_TOKEN);
})
.redirectUri("https://pig4cloud.com")
.build();
return new InMemoryRegisteredClientRepository(client);
}
// 指定token 生成的加解密密鑰
@Bean
@SneakyThrows
public JWKSource<SecurityContext> jwkSource() {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// @formatter:off
RSAKey rsaKey= new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
}
測試
- 浏覽器通路如下連結,會重定向至登入頁
- 輸入賬号密碼後,會攜帶 code 自動回調至目标頁面
授權碼認證
curl --location --request GET 'http://localhost:3000/oauth2/authorize?client_id=pig&client_secret=pig&response_type=code&redirect_uri=https://pig4cloud.com'
擷取令牌
curl --location --request POST 'http://localhost:3000/oauth2/token' \
--header 'Authorization: Basic cGlnOnBpZw==' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code={code}' \
--data-urlencode 'redirect_uri=https://pig4cloud.com'
重新整理令牌
curl --location --request POST 'http://localhost:3000/oauth2/token' \
--header 'Authorization: Basic cGlnOnBpZw==' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token={refresh_token}' \
撤銷令牌
- 通過 access_token
curl --location --request POST 'http://localhost:3000/oauth2/revoke' \
--header 'Authorization: Basic cGlnOnBpZw==' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'token={access_token}' \
--data-urlencode 'token_type_hint=access_token'
- 通過 refresh_token
curl --location --request POST 'http://localhost:3000/oauth2/revoke' \
--header 'Authorization: Basic cGlnOnBpZw==' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'token={refresh_token}' \
--data-urlencode 'token_type_hint=refresh_token'