SpringAuthorizationServer 详解

一、认证服务搭建

1. 认证服务简介

2. 服务搭建

  • pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
</dependencies>
  • SecurityConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
@Configuration
@EnableWebSecurity
public class SecurityConfig {

/**
* 配置OAuth 2.1和OpenID Connect 1.0
*/
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
// 开启OpenID Connect 1.0
.oidc(Customizer.withDefaults());
http
// 未从授权端点进行身份验证时,重定向到登录页面
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
// 使用jwt处理接收到的access token
.oauth2ResourceServer(resourceServer -> resourceServer.jwt(Customizer.withDefaults()));
return http.build();
}

/**
* 配置SpringSecurity
*/
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
return http.build();
}

@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder().username("test").password("123").roles("USER").build();
return new InMemoryUserDetailsManager(userDetails);
}

/**
* 注册客户端信息
*/
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
// `client_id`
.clientId("oidc-client")
// `client_secret`(noop表示以明文存储)
.clientSecret("{noop}secret")
// 使用Basic Auth
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// `authorization_code`
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
// `refresh_token`
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
// `redirect_uri`
.redirectUri("http://localhost:8081/test") // 客户端服务(这里随便写了个服务)
.postLogoutRedirectUri("http://127.0.0.1:8080/")
// `scope`
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
// 需要给客户端授权
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
return new InMemoryRegisteredClientRepository(oidcClient);
}

/**
* 配置JWK,加解密JWT token
*/
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
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 new ImmutableJWKSet<>(jwkSet);
}

private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}

/**
* 配置JWT解析器
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}

/**
* 配置认证服务器
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
// 使用默认EndPoints
return AuthorizationServerSettings.builder().build();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
{
"issuer": "http://127.0.0.1:8080",
"authorization_endpoint": "http://127.0.0.1:8080/oauth2/authorize", // 获取授权码
"device_authorization_endpoint": "http://127.0.0.1:8080/oauth2/device_authorization", // 获取设备授权码
"token_endpoint": "http://127.0.0.1:8080/oauth2/token", // 获取令牌
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
],
"jwks_uri": "http://127.0.0.1:8080/oauth2/jwks",
"userinfo_endpoint": "http://127.0.0.1:8080/userinfo", // 获取用户信息
"end_session_endpoint": "http://127.0.0.1:8080/connect/logout",
"response_types_supported": [
"code"
],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code"
],
"revocation_endpoint": "http://127.0.0.1:8080/oauth2/revoke",
"revocation_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
],
"introspection_endpoint": "http://127.0.0.1:8080/oauth2/introspect",
"introspection_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
],
"code_challenge_methods_supported": [
"S256"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"scopes_supported": [
"openid"
]
}

3. 认证测试

(1) 获取授权码

(2) 使用授权码获取令牌

1
2
3
4
5
6
7
8
{
"access_token": "eyJraWQiOiI3MjNlMzc4MC03MDUyLTRiZmUtYjNlMy1lMGQ4ZThlY2Y0YzMiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwiYXVkIjoib2lkYy1jbGllbnQiLCJuYmYiOjE3MDY3NzIwNjUsInNjb3BlIjpbIm9wZW5pZCIsInByb2ZpbGUiXSwiaXNzIjoiaHR0cDovLzEyNy4wLjAuMTo5MDAwIiwiZXhwIjoxNzA2NzcyMzY1LCJpYXQiOjE3MDY3NzIwNjUsImp0aSI6IjM5ZWFkZGFmLTBiYWQtNDUyMi05MmY1LTVhMTI2MGJlZTNmMyJ9.CR8CZ2NIpRQkAqBiQcWXq2qdfT_u1xxR7WV0Iuj_CPxJ9delCODdHTn7QkR0JX67hUEESuM1QhUhLDpYUSzkcCYjLOrAAyvgcI-Lphmy01pxHzpeb_dcZTBrRfGsJJOKSu5IwWMOds5XRPcp_Z68fi8L7IeANnaD1YIWukDQYgfABE98txdf0J-S3MepJu_z5rXAft8n4evYRb5L_9ryLjEjF40SD0x3ScBwuOTy6BJLevFYMg9fONHfFs3S9S3m_z1vyHTcFghYdWCMMwMTJ7QSu4hgVC_qepZyoyCMJv1W90Jc9-pK5LR8otIBtb0f5zTvM5pDTn3MfDFzy-AzdQ",
"refresh_token": "Ndg4-60wDnEXLxkvC1wf_iHU7LZ7XZvB51V_Q1_qXqZd2DJCBUAdV7sHK8Nv9nexLPKRuqtRCtI1pcnRJjkFj4KHyCcR2pK1vfVX885hW8sRq7uyqwbFe17daUQB8lwX",
"scope": "openid profile",
"id_token": "eyJraWQiOiI3MjNlMzc4MC03MDUyLTRiZmUtYjNlMy1lMGQ4ZThlY2Y0YzMiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwiYXVkIjoib2lkYy1jbGllbnQiLCJhenAiOiJvaWRjLWNsaWVudCIsImF1dGhfdGltZSI6MTcwNjc3MjA0OSwiaXNzIjoiaHR0cDovLzEyNy4wLjAuMTo5MDAwIiwiZXhwIjoxNzA2NzczODY1LCJpYXQiOjE3MDY3NzIwNjUsImp0aSI6IjFjMDdiZDZjLWRhNTQtNDE3Mi1iZDJmLTY3YTQ4ZjYyZmRhOCIsInNpZCI6ImgzZDNtbXhDR2xZMm9jenlrb01LZEpGdjUwODBpUmNTUWlXMjFLbkR4SGcifQ.bSH5g4J8uPtZmXeBBu9SVP1y9z2JN7f4oFEW5I2-R03Ap9MJoZiDNo_BPp7VLadTUVbMF9ijrf6kgveW7Zdfx6GyB_sxCpAH7Hu5S82ASNAoRCg3pz-5TmLDJHbLfqQmu12_-fpH3PK8NvSk5RmxOlFW4aDtSSYvCxPL5FV-AXCw94av4or7jdMhQOnhwCPmPhLf-fd-EzRgMjrKUQcmL2CPny7Il-mLj3sxnMO_4M1zTp2cnwlicwbJVUEVXvmokDIj6g66eksXoAaJMW3rYTwc_GEz3TuEpHx88n2p8msw0jYt7KZNs6TKpfU3bYv9pO3M023MQQwBFnAXPBQp_w",
"token_type": "Bearer",
"expires_in": 299
}

(3) 使用令牌获取用户信息

(4) 刷新令牌

二、认证服务自定义配置

1. 登录用户从数据库获取

2. 客户端从数据库获取

  • Oauth2Client
1
2
3
4
5
6
7
8
9
10
11
@Data
public class Oauth2Client {

@TableId
private String id;
private String clientId;
private String clientSecret;
private String clientName;
private String redirectUri;
private String scope;
}
  • RegisteredClientRepositoryImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Service
public class RegisteredClientRepositoryImpl implements RegisteredClientRepository {

@Resource
public Oauth2ClientService oauth2ClientService;

@Override
public void save(RegisteredClient registeredClient) {
Oauth2Client oauth2Client = new Oauth2Client();
oauth2Client.setId(registeredClient.getId());
oauth2Client.setClientId(registeredClient.getClientId());
oauth2Client.setClientSecret(registeredClient.getClientSecret());
oauth2Client.setClientName(registeredClient.getClientName());
oauth2Client.setScope(String.join(",", registeredClient.getScopes()));
oauth2Client.setRedirectUri(String.join(",", registeredClient.getRedirectUris()));
oauth2ClientService.save(oauth2Client);
}

@Override
public RegisteredClient findById(String id) {
Oauth2Client oauth2Client = oauth2ClientService.getById(id);
return toRegisteredClient(oauth2Client);
}

@Override
public RegisteredClient findByClientId(String clientId) {
Oauth2Client oauth2Client = oauth2ClientService.getOne(Wrappers.<Oauth2Client>lambdaQuery()
.eq(Oauth2Client::getClientId, clientId));
return toRegisteredClient(oauth2Client);
}

private RegisteredClient toRegisteredClient(Oauth2Client oauth2Client) {
return RegisteredClient.withId(oauth2Client.getId())
.clientId(oauth2Client.getClientId())
.clientSecret(oauth2Client.getClientSecret())
.clientName(oauth2Client.getClientName())
.scopes(set -> set.addAll(Arrays.asList(oauth2Client.getScope().split(","))))
.redirectUris(set -> set.addAll(Arrays.asList(oauth2Client.getRedirectUri().split(","))))
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofHours(2))
.refreshTokenTimeToLive(Duration.ofDays(1))
.build())
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
}
}
  • Oauth2Client-Mapper/Service/Controller:略
1
2
-- client_secret是123
INSERT INTO `oauth2_client` VALUES ('111', 'oidc-client', '$2a$10$dprC1RHQgq1uNEiKz85JbO8N5JQKHMUa8VqxIpXu4GMShEEYHh4yS', 'test', 'http://localhost:8081/test', 'profile,openid');
  • 重复 1.3 节的认证测试,注意客户端密码变了

三、客户端服务搭建

img

1. 环境准备

  • hosts
1
2
3
# 模拟多个地址
127.0.0.1 oauth2-client
127.0.0.1 oauth2-authorization-server
  • 添加客户端
1
INSERT INTO `oauth2_client` VALUES ('222', 'my-client-1', '$2a$10$dprC1RHQgq1uNEiKz85JbO8N5JQKHMUa8VqxIpXu4GMShEEYHh4yS', '客户端1', 'http://oauth2-client:8081/login/oauth2/code/messaging-client-oidc', 'profile,openid');

2. 服务搭建

  • pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>0
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
</dependencies>
  • application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
server:
port: 8081

spring:
application:
name: oauth2-client
security:
oauth2:
client:
provider:
# 认证服务器信息
my-oauth2-server:
issuer-uri: http://oauth2-authorization-server:8080
authorization-uri: http://oauth2-authorization-server:8080/oauth2/authorize
token-uri: http://oauth2-authorization-server:8080/oauth2/token
registration:
messaging-client-oidc:
# 由哪个认证服务器进行认证
provider: my-oauth2-server
# 客户端名称
client-name: 客户端1
# 客户端id
client-id: my-client-1
# 客户端秘钥
client-secret: 123
# 客户端认证方式
client-authentication-method: client_secret_basic
# 授权码模式获取令牌
authorization-grant-type: authorization_code
# 回调地址,接收认证服务器回传code的接口地址
redirect-uri: http://oauth2-client:8081/login/oauth2/code/messaging-client-oidc
scope:
- profile
- openid

logging:
level:
org.springframework.security: debug
  • AuthenticationController
1
2
3
4
5
6
7
8
@RestController
public class AuthenticationController {

@GetMapping("/token")
public OAuth2AuthorizedClient token(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {
return oAuth2AuthorizedClient;
}
}

3. 认证测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
{
"clientRegistration": {
"registrationId": "messaging-client-oidc",
"clientId": "my-client-1",
"clientSecret": "123",
"clientAuthenticationMethod": {
"value": "client_secret_basic"
},
"authorizationGrantType": {
"value": "authorization_code"
},
"redirectUri": "http://oauth2-client:8081/login/oauth2/code/messaging-client-oidc",
"scopes": [
"profile",
"openid"
],
"providerDetails": {
"authorizationUri": "http://oauth2-authorization-server:8080/oauth2/authorize",
"tokenUri": "http://oauth2-authorization-server:8080/oauth2/token",
"userInfoEndpoint": {
"uri": "http://oauth2-authorization-server:8080/userinfo",
"authenticationMethod": {
"value": "header"
},
"userNameAttributeName": "sub"
},
"jwkSetUri": "http://oauth2-authorization-server:8080/oauth2/jwks",
"issuerUri": "http://oauth2-authorization-server:8080",
"configurationMetadata": {
"authorization_endpoint": "http://oauth2-authorization-server:8080/oauth2/authorize",
"token_endpoint": "http://oauth2-authorization-server:8080/oauth2/token",
"introspection_endpoint": "http://oauth2-authorization-server:8080/oauth2/introspect",
"revocation_endpoint": "http://oauth2-authorization-server:8080/oauth2/revoke",
"device_authorization_endpoint": "http://oauth2-authorization-server:8080/oauth2/device_authorization",
"issuer": "http://oauth2-authorization-server:8080",
"jwks_uri": "http://oauth2-authorization-server:8080/oauth2/jwks",
"scopes_supported": [
"openid"
],
"response_types_supported": [
"code"
],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code"
],
"code_challenge_methods_supported": [
"S256"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
],
"introspection_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
],
"revocation_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
],
"request_uri_parameter_supported": true,
"subject_types_supported": [
"public"
],
"userinfo_endpoint": "http://oauth2-authorization-server:8080/userinfo",
"end_session_endpoint": "http://oauth2-authorization-server:8080/connect/logout",
"id_token_signing_alg_values_supported": [
"RS256"
]
}
},
"clientName": "客户端1"
},
"principalName": "admin",
"accessToken": {
"tokenValue": "eyJraWQiOiIwYWVlZGNlNy1lMTU4LTRmYjQtYWM2NS1iODI5ZjFlZmNiOWIiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6Im15LWNsaWVudC0yIiwibmJmIjoxNzA2ODU0MjE1LCJzY29wZSI6WyJvcGVuaWQiLCJwcm9maWxlIl0sImlzcyI6Imh0dHA6Ly9vYXV0aDItYXV0aG9yaXphdGlvbi1zZXJ2ZXI6ODA4MCIsImV4cCI6MTcwNjg2MTQxNSwiaWF0IjoxNzA2ODU0MjE1LCJqdGkiOiI5OTJmYzk5Mi1jNTc2LTQ1ODQtOWVjMC1lZDIxNDFlMDM4MzkifQ.m2gk8W8kIQxt7ilk4cemAr_L9qk8RoZ5UrOVrIIzu1M3VQTs-KUh5qHyzrCtrqO5So04lXR-4r-w97qeDSoPgAaDfTzPD2eDdUyvaBJU2gFBexKhmKoLMDIDfoNHxX2v-yZ2k5o1MZtu_hZtFj_9vogusjIX1Z5jeM6XupmL44DohDgIstqOMG8kVOBanDgmtRkbhq7hfkBhdUvo64GLMPEHLYORnI0GGxa43ZvSYivN3-kNPsll0KRyaDBDfHWBu3l0SW6AEErdYCtttmiZ01_ma7PTwCR9Pcdgtw6qfDHWJRr-Ud-Q_XkF-Ctqm1hhfFRc1aU7qKq0FK1AgObR3Q",
"issuedAt": "2024-02-02T06:10:15.239316700Z",
"expiresAt": "2024-02-02T08:10:15.239316700Z",
"tokenType": {
"value": "Bearer"
},
"scopes": [
"openid",
"profile"
]
},
"refreshToken": {
"tokenValue": "5GojNIG0yJsIgFvBteoshc2atUwBHU-7jcpKI6iPFBsYqPu97lOMcusBa320YuCn0C5mtz8qtUcbju77pTMbpE_CYk92OmYhXdeRJfXo63sCOr-jbXtibNXYUKupSNHn",
"issuedAt": "2024-02-02T06:10:15.239316700Z",
"expiresAt": null
}
}

四、资源服务搭建

1. 服务搭建

  • pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
</dependencies>
  • application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 8083

spring:
application:
name: oauth2-resource-server
security:
oauth2:
resource-server:
jwt:
issuer-uri: http://oauth2-authorization-server:8080

logging:
level:
org.springframework.security: debug
  • ResourceServerConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class ResourceServerConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();

}
}
  • TestController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
public class TestController {

@PreAuthorize("hasAuthority('SCOPE_profile')")
@GetMapping("/profile")
public String profile() {
return "profile";
}

@PreAuthorize("hasAuthority('SCOPE_test')")
@GetMapping("/test")
public String test() {
return "test";
}

@GetMapping("/none")
public String none() {
return "none";
}
}

2. 认证测试

参考