一、背景
在 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、引入授权服务器依赖
1 | xml复制代码<dependency> |
2、创建授权服务器用户
张三
通过QQ登录
的方式来登录CSDN
网站。
此处完成用户张三
的创建,这个张三是授权服务器的用户,此处即QQ服务器的用户。
1 | java复制代码@EnableWebSecurity |
3、创建授权服务器和客户端
张三
通过QQ登录
的方式来登录CSDN
网站。
此处完成QQ授权服务器和客户端CSDN的创建。
1 | java复制代码package com.huan.study.authorization.config; |
/**
* 个性化 JWT token
*/
class CustomOAuth2TokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {
@Override
public void customize(JwtEncodingContext context) {
// 添加一个自定义头
context.getHeaders().header("client-id", context.getRegisteredClient().getClientId());
}
}
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
/**
* 定义 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.comclientSecret
的值不可泄漏给客户端,必须保存在服务器端。
1、授权码流程
1、获取授权码
qq.com:8080/oauth2/auth… user.userInfo
client_id=csdn:表示客户端是谁
response_type=code:表示返回授权码
scope=user.userInfo user.userInfo:获取多个权限以空格分开
redirect_uri=www.baidu.com:跳转请求,用户同意或拒绝后
2、根据授权码获取token
1 | shell复制代码 curl -i -X POST \ |
Authorization: 携带具体的 clientId 和 clientSecret 的base64的值
grant_type=authorization_code 表示采用的方式是授权码
code=xxx:上一步获取到的授权码
3、流程演示
2、根据刷新令牌获取token
1 | shell复制代码curl -i -X POST \ |
3、客户端模式
此模式下,没有用户的参与,只有客户端和授权服务器之间的参与。
1 | shell复制代码curl -i -X POST \ |
4、撤销令牌
1 | shell复制代码curl -i -X POST \ |
5、查看token 的信息
1 | shell复制代码curl -i -X POST \ |
6、查看JWK信息
1 | shell复制代码curl -i -X GET \ |
六、完整代码
七、参考地址
本文转载自: 掘金