Spring Authorization Server的使用
一、背景
在 Spring Security 5
中,現在已經不提供了 授權服務器
的配置,但是 授權服務器 在我們平時的開發過程中用的還是比較多的。不過 Spring 官方提供了一個 由Spring官方主導,社區驅動的授權服務 spring-authorization-server
,目前已經到了 0.1.2 的版本,不過該項目還是一個實驗性的項目,不可在生產環境中使用,此處來使用項目搭建一個簡單的授權服務器。
二、前置知識
1、了解 oauth2 協議、流程。可以參考阮一峰的這篇文章
2、JWT、JWS、JWK的概念
JWT:
指的是 JSON Web Token,由 header.payload.signture 組成。不存在簽名的JWT是不安全的,存在簽名的JWT是不可竄改的。
JWS:
指的是簽過名的JWT,即擁有簽名的JWT。
JWK:
既然涉及到簽名,就涉及到簽名算法,對稱加密還是非對稱加密,那么就需要加密的 密鑰或者公私鑰對。此處我們將 JWT的密鑰或者公私鑰對統一稱為 JSON WEB KEY,即 JWK。
三、需求
1、 完成授權碼(authorization-code
)流程。
最安全的流程,需要用戶的參與。
2、 完成客戶端(client credentials
)流程。
沒有用戶的參與,一般可以用於內部系統之間的訪問,或者系統間不需要用戶的參與。
3、簡化模式在新的 spring-authorization-server 項目中已經被棄用了。
4、刷新令牌。
5、撤銷令牌。
6、查看頒發的某個token信息。
7、查看JWK信息。
8、個性化JWT token,即給JWT token中增加額外信息。
完成案例:
張三
通過QQ登錄
的方式來登錄CSDN
網站。
登錄后,CSDN就可以獲取到QQ頒發的token
,CSDN網站拿着token就可以獲取張三在QQ資源服務器上的 個人信息
了。
角色分析
張三:
用戶即資源擁有者
CSDN:
客戶端
QQ:
授權服務器
個人信息:
即用戶的資源,保存在資源服務器中
四、核心代碼編寫
1、引入授權服務器依賴
<dependency>
<groupId>org.springframework.security.experimental</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.1.2</version>
</dependency>
2、創建授權服務器用戶
張三
通過QQ登錄
的方式來登錄CSDN
網站。
此處完成用戶張三
的創建,這個張三是授權服務器的用戶,此處即QQ服務器的用戶。
@EnableWebSecurity
public class DefaultSecurityConfig {
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.formLogin();
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 此處創建用戶,張三。
@Bean
UserDetailsService users() {
UserDetails user = User.builder()
.username("zhangsan")
.password(passwordEncoder().encode("zhangsan123"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
3、創建授權服務器和客戶端
張三
通過QQ登錄
的方式來登錄CSDN
網站。
此處完成QQ授權服務器和客戶端CSDN的創建。
package com.huan.study.authorization.config;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.RequestMatcher;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.UUID;
/** * 認證服務器配置 * * @author huan.fu 2021/7/12 - 下午2:08 */
@Configuration
public class AuthorizationConfig {
@Autowired
private PasswordEncoder passwordEncoder;
/** * 個性化 JWT token */
class CustomOAuth2TokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {
@Override
public void customize(JwtEncodingContext context) {
// 添加一個自定義頭
context.getHeaders().header("client-id", context.getRegisteredClient().getClientId());
}
}
/** * 定義 Spring Security 的攔截器鏈 */
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// 設置jwt token個性化
http.setSharedObject(OAuth2TokenCustomizer.class, new CustomOAuth2TokenCustomizer());
// 授權服務器配置
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
return http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer)
.and()
.formLogin()
.and()
.build();
}
/** * 創建客戶端信息,可以保存在內存和數據庫,此處保存在數據庫中 */
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
// 客戶端id 需要唯一
.clientId("csdn")
// 客戶端密碼
.clientSecret(passwordEncoder.encode("csdn123"))
// 可以基於 basic 的方式和授權服務器進行認證
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
// 授權碼
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
// 刷新token
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
// 客戶端模式
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// 密碼模式
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
// 簡化模式,已過時,不推薦
.authorizationGrantType(AuthorizationGrantType.IMPLICIT)
// 重定向url
.redirectUri("https://www.baidu.com")
// 客戶端申請的作用域,也可以理解這個客戶端申請訪問用戶的哪些信息,比如:獲取用戶信息,獲取用戶照片等
.scope("user.userInfo")
.scope("user.photos")
.clientSettings(clientSettings -> {
// 是否需要用戶確認一下客戶端需要獲取用戶的哪些權限
// 比如:客戶端需要獲取用戶的 用戶信息、用戶照片 但是此處用戶可以控制只給客戶端授權獲取 用戶信息。
clientSettings.requireUserConsent(true);
})
.tokenSettings(tokenSettings -> {
// accessToken 的有效期
tokenSettings.accessTokenTimeToLive(Duration.ofHours(1));
// refreshToken 的有效期
tokenSettings.refreshTokenTimeToLive(Duration.ofDays(3));
// 是否可重用刷新令牌
tokenSettings.reuseRefreshTokens(true);
})
.build();
JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
if (null == jdbcRegisteredClientRepository.findByClientId("csdn")) {
jdbcRegisteredClientRepository.save(registeredClient);
}
return jdbcRegisteredClientRepository;
}
/** * 保存授權信息,授權服務器給我們頒發來token,那我們肯定需要保存吧,由這個服務來保存 */
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
class CustomOAuth2AuthorizationRowMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper {
public CustomOAuth2AuthorizationRowMapper(RegisteredClientRepository registeredClientRepository) {
super(registeredClientRepository);
getObjectMapper().configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
this.setLobHandler(new DefaultLobHandler());
}
}
CustomOAuth2AuthorizationRowMapper oAuth2AuthorizationRowMapper =
new CustomOAuth2AuthorizationRowMapper(registeredClientRepository);
authorizationService.setAuthorizationRowMapper(oAuth2AuthorizationRowMapper);
return authorizationService;
}
/** * 如果是授權碼的流程,可能客戶端申請了多個權限,比如:獲取用戶信息,修改用戶信息,此Service處理的是用戶給這個客戶端哪些權限,比如只給獲取用戶信息的權限 */
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
/** * 對JWT進行簽名的 加解密密鑰 */
@Bean
public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
/** * jwt 解碼 */
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/** * 配置一些斷點的路徑,比如:獲取token、授權端點 等 */
@Bean
public ProviderSettings providerSettings() {
return new ProviderSettings()
// 配置獲取token的端點路徑
.tokenEndpoint("/oauth2/token")
// 發布者的url地址,一般是本系統訪問的根路徑
// 此處的 qq.com 需要修改我們系統的 host 文件
.issuer("http://qq.com:8080");
}
}
注意⚠️:
1、需要將 qq.com 在系統的 host 文件中與 127.0.0.1 映射起來。
2、因為客戶端信息、授權信息(token信息等)保存到數據庫,因此需要將表建好。
3、詳細信息看上方代碼的注釋
五、測試
從上方的代碼中可知:
資源所有者:
張三 用戶名和密碼為:zhangsan/zhangsan123
客戶端信息:
CSDN clientId和clientSecret:csdn/csdn123
授權服務器地址:
qq.com
clientSecret
的值不可泄漏給客戶端,必須保存在服務器端。
1、授權碼流程
1、獲取授權碼
http://qq.com:8080/oauth2/authorize?client_id=csdn&response_type=code&redirect_uri=https://www.baidu.com&scope=user.userInfo user.userInfo
client_id=csdn:表示客戶端是誰
response_type=code:表示返回授權碼
scope=user.userInfo user.userInfo:獲取多個權限以空格分開
redirect_uri=https://www.baidu.com:跳轉請求,用戶同意或拒絕后
2、根據授權碼獲取token
curl -i -X POST \
-H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
'http://qq.com:8080/oauth2/token?grant_type=authorization_code&code=tDrZ-LcQDG0julJBcGY5mjtXpE04mpmXjWr9vr0-rQFP7UuNFIP6kFArcYwYo4U-iZXFiDcK4p0wihS_iUv4CBnlYRt79QDoBBXMmQBBBm9jCblEJFHZS-WalCoob6aQ&redirect_uri=https%3A%2F%2Fwww.baidu.com'
Authorization: 攜帶具體的 clientId 和 clientSecret 的base64的值
grant_type=authorization_code 表示采用的方式是授權碼
code=xxx:上一步獲取到的授權碼
3、流程演示
2、根據刷新令牌獲取token
curl -i -X POST \
-H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
'http://qq.com:8080/oauth2/token?grant_type=refresh_token&refresh_token=Wpu3ruj8FhI-T1pFmnRKfadOrhsHiH1JLkVg2CCFFYd7bYPN-jICwNtPgZIXi3jcWqR6FOOBYWo56W44B5vm374nvM8FcMzTZaywu-pz3EcHvFdFmLJrqAixtTQZvMzx'
3、客戶端模式
此模式下,沒有用戶的參與,只有客戶端和授權服務器之間的參與。
curl -i -X POST \
-H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
'http://qq.com:8080/oauth2/token?grant_type=client_credentials'
4、撤銷令牌
curl -i -X POST \
'http://qq.com:8080/oauth2/revoke?token=令牌'
5、查看token 的信息
curl -i -X POST \
-H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
'http://qq.com:8080/oauth2/introspect?token=XXX'
6、查看JWK信息
curl -i -X GET \
'http://qq.com:8080/oauth2/jwks'
六、完整代碼
https://gitee.com/huan1993/spring-cloud-parent/tree/master/security/authorization-server
七、參考地址
1、https://github.com/spring-projects-experimental/spring-authorization-server