依赖

只需要下面的这一个依赖,springboot 版本为 3.3

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>

授权服务AuthorizationServerConfig配置

spring 官方在快速开始里面给出了下面的默认最小配置Spring Authorization Server

AuthorizationServerConfig

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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
@Configuration
public class AuthorizationServerConfig {

// 定义自定义同意页面 URI
private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";

// 配置授权服务器安全过滤器链
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
// 针对 Spring Authorization Server 最佳实践配置
// 应用默认的 Spring 授权服务器安全配置
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.authorizationEndpoint(authorizationEndpoint ->
authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI))
.oidc(oidc -> oidc
.userInfoEndpoint(userInfo -> userInfo
.userInfoResponseHandler(this::onAuthenticationSuccess)
)
); // 启用 OpenID Connect 1.0
http
// Redirect to the login page when not authenticated from the
// authorization endpoint
// 未经身份验证时重定向到登录页面
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"), // 配置登录地址
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
// Accept access tokens for User Info and/or Client Registration
// 接受用户信息和/或客户端注册的访问令牌
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()))
.cors(Customizer.withDefaults());

return http.build();
}

// 注册客户端存储库
@Bean
public RegisteredClientRepository registeredClientRepository() {
// 注册用户平台客户端
RegisteredClient wordPress = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("kil-test") // 客户端ID
.clientName("TOY") // 客户端名称
.clientSecret("{noop}test") // 客户端密钥,{noop}表示明文存储
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) // 客户端认证方法
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) // 授权类型:授权码模式
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) // 刷新令牌授权类型
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) // 客户端凭据授权类型
.redirectUri("https://blog.122345.xyz") // 授权码回调地址
.postLogoutRedirectUri("https://blog.122345.xyz/") // 注销后重定向地址
.scope("profile") // 客户端作用域
// 令牌设置:访问令牌的生存期为1天
.tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofDays(1L)).build())
// 客户端设置:需要用户授权同意
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build(); // 构建注册的客户端

return new InMemoryRegisteredClientRepository(wordPress);
}

// 定义一个名为jwtTokenCustomizer的Bean,用于定制JWT令牌
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer() {
// 返回一个Lambda表达式,定制JWT令牌的内容
return (context) -> {
// 检查令牌类型是否为访问令牌
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
// 获取JWT令牌的声明部分
context.getClaims().claims((claims) -> {
// 获取用户权限列表并转换为Set集合
Set<String> roles = AuthorityUtils.authorityListToSet(context.getPrincipal().getAuthorities())
.stream()
// 将权限中的"ROLE_"前缀去除
.map(c -> c.replaceFirst("^ROLE_", ""))
// 转换为不可变的Set集合
.collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet));

// 将角色信息放入JWT令牌的声明中的"roles"字段
claims.put("roles", roles);
});
}
};
}

// 定义一个名为jwkSource的Bean,用于提供JSON Web Key (JWK)
@Bean
public JWKSource<SecurityContext> jwkSource() {
// 生成RSA密钥对
KeyPair keyPair = generateRsaKey();
// 获取公钥
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
// 获取私钥
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// 创建RSAKey对象
RSAKey rsaKey = new RSAKey.Builder(publicKey)
// 设置私钥
.privateKey(privateKey)
// 设置Key ID
.keyID(UUID.randomUUID().toString())
// 构建RSAKey对象
.build();
// 创建JWKSet对象,包含上面创建的RSAKey
JWKSet jwkSet = new JWKSet(rsaKey);
// 返回一个不可变的JWKSet
return new ImmutableJWKSet<>(jwkSet);
}

// 定义一个静态方法用于生成RSA密钥对
private static KeyPair generateRsaKey() {
// 声明KeyPair对象
KeyPair keyPair;
try {
// 获取RSA算法的密钥对生成器实例
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
// 初始化密钥对生成器,指定密钥长度为2048位
keyPairGenerator.initialize(2048);
// 生成RSA密钥对
keyPair = keyPairGenerator.generateKeyPair();
// 捕获可能的异常
} catch (Exception ex) {
// 抛出异常
throw new IllegalStateException(ex);
}
// 返回生成的RSA密钥对
return keyPair;
}

// 定义一个名为jwtDecoder的Bean,接受一个JWKSource<SecurityContext>作为参数
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
// 调用OAuth2AuthorizationServerConfiguration类的jwtDecoder方法,并传入jwkSource作为参数
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}

// 定义一个名为authorizationServerSettings的Bean
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
// 使用AuthorizationServerSettings的构建器模式创建一个实例并返回
return AuthorizationServerSettings.builder().build();
}

// 授权成功返回信息
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
// 当认证成功时执行的方法,接受HttpServletRequest、HttpServletResponse和Authentication作为参数
// 将Authentication转换为OidcUserInfoAuthenticationToken类型,以便获取用户信息

// 设置响应的Content-Type为application/json,并指定字符集为UTF-8
response.setContentType("application/json;charset=UTF-8");
// 设置响应的状态码为200(HttpStatus.OK.value()返回的是200)
response.setStatus(HttpStatus.OK.value());
// 将request中的用户主体信息转换为JSON字符串并写入响应的输出流中
response.getWriter().write(JSON.toJSONString(request.getUserPrincipal()));
// 刷新响应的输出流
response.getWriter().flush();
}

}
权限范围 声明
openid sub
profile Name、family_name、given_name、middle_name、nickname、preferred_username、profile、 picture、website、gender、birthdate、zoneinfo、locale、updated_at
email email、email_verified
address address,是一个 JSON 对象、包含 formatted、street_address、locality、region、postal_code、country
phone phone_number、phone_number_verified

DefaultSecurityConfig

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
@EnableWebSecurity
@Configuration(proxyBeanMethods = false) //不使用代理来优化 @Configuration 类中的 @Bean 方法
public class DefaultSecurityConfig {

@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
// 定义一个安全过滤器链,指定顺序为2
http
.authorizeHttpRequests((authorize) -> authorize
// 配置不需要验证的地址
.requestMatchers(new AntPathRequestMatcher("/actuator/**"),
new AntPathRequestMatcher("/oauth2/**"),
new AntPathRequestMatcher("/login"),
new AntPathRequestMatcher("/**/*.json"),
new AntPathRequestMatcher("/**/*.html"),
// 对指定的地址不进行认证,允许所有访问
new AntPathRequestMatcher("/**/*.png")).permitAll()
// 其他地址需要认证
.anyRequest().authenticated()
)
// 启用默认的跨域配置
.cors(Customizer.withDefaults())
// 禁用CSRF保护
.csrf(AbstractHttpConfigurer::disable)
// .httpBasic(Customizer.withDefaults())
// Form login handles the redirect to the login page from the
// authorization server filter chain
// 启用HTTP基本认证,使用默认配置
// 登录的url
.formLogin(form -> form
// 登录的url
.loginPage("/login")
// 指定用于处理登录的URL
.loginProcessingUrl("/login"))
;

// 构建并返回SecurityFilterChain
return http.build();
}

// 跨域请求配置
@Bean
public CorsConfigurationSource corsConfigurationSource() {
// 定义跨域配置源
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 允许所有头部
config.addAllowedHeader("*");
// 允许所有方法
config.addAllowedMethod("*");
// 允许所有来源
config.addAllowedOriginPattern("*");
// 允许发送身份验证信息
config.setAllowCredentials(true);
// 预检请求的有效期,单位为秒
config.setMaxAge(3600L);
// 对所有路径应用上述配置
source.registerCorsConfiguration("/**", config);
// 返回配置源
return source;
}
}

持久化客户端

AuthorizationServerConfig类添加

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
    // 注册客户端存储库
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {

/*
// 注册用户平台客户端
RegisteredClient wordPress = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("kil-blog") // 客户端ID
.clientName("TOY论坛") // 客户端名称
.clientSecret("{noop}kilCat") // 客户端密钥,{noop}表示明文存储
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) // 客户端认证方法
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) // 授权类型:授权码模式
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) // 刷新令牌授权类型
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) // 客户端凭据授权类型
.redirectUri("https://blog.122345.xyz") // 授权码回调地址
.postLogoutRedirectUri("https://blog.122345.xyz") // 注销后重定向地址
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("message.read")
.scope("message.write")
.scope("all") // 客户端作用域
// 令牌设置:访问令牌的生存期为1天
.tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofDays(1L)).build())
// 客户端设置:需要用户授权同意
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build(); // 构建注册的客户端
*/

// Save registered client's in db as if in-memory
// registeredClientRepository.save(wordPress);

return new JdbcRegisteredClientRepository(jdbcTemplate);
}

@Bean
public JdbcOAuth2AuthorizationService auth2AuthorizationService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
// 这个 Bean 用于管理 OAuth2 授权。
// 它需要两个参数:jdbcTemplate 和 registeredClientRepository。
// jdbcTemplate 是 Spring JDBC 模板,用于执行 SQL 查询和更新操作。
// registeredClientRepository 是一个接口,用于管理 OAuth2 客户端的注册信息。
// 这个 Bean 将 OAuth2 授权信息存储到数据库中,并提供查询和删除授权信息的方法。
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}

@Bean
public JdbcOAuth2AuthorizationConsentService auth2AuthorizationConsentService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
// 这个 Bean 用于管理 OAuth2 授权同意。
// 它也需要 jdbcTemplate 和 registeredClientRepository 作为参数。
// 这个 Bean 将 OAuth2 授权同意信息存储到数据库中,并提供查询和删除授权同意信息的方法。
// 它将被 ConsentController 使用。
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}

sql参考官方项目 spring-authorization-server

官方dome demo-authorizationserver

修改pom

需要额外引入spring security cas包原因是启动时(logging等级:org.springframework.security: trace)会报错:java.lang.ClassNotFoundException:org.springframework.security.cas.jackson2.CasJackson2Module错误。

1
2
3
4
5
6
7
8
<!-- 添加spring security cas支持
这里需添加spring-security-cas依赖,
否则启动时报java.lang.ClassNotFoundException: org.springframework.security.cas.jackson2.CasJackson2Module错误。
-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>