基于JWT实现SSO单点登录
# 实现原理
用户访问应用A 时,需要到认证服务上进行认证。认证成功后,再去访问应用B,仍然需要到认证服务器认证,只不过这次不用再重新登录了,只需要授一下权就可以了。
# 代码结构
sso-server :认证服务器 sso-client1:应用A sso-client2:应用B
<groupId>com.imooc.sso</groupId>
<artifactId>sso-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>sso-server</module>
<module>sso-client1</module>
<module>sso-client2</module>
</modules>
<packaging>pom</packaging>
<description>单点登录demo</description>
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 认证服务器开发
1.配置客户端的 clientId 和 clientSecert, 设置JwtToekenStore
2.配置用户密码
@Configuration
@EnableAuthorizationServer
public class SsoAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("imooc1")
.secret("imoocsecrect1")
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.and()
.withClient("imooc2")
.secret("imoocsecrect2")
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(jwtTokenStore()).accessTokenConverter(jwtAccessTokenConverter());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("isAuthenticated()");//访问需认证,默认denyAll
}
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("imooc");
return converter;
}
}
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
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
server.port = 9999
server.context-path = /server
security.user.password = 123456
1
2
3
2
3
# 应用A
1、@EnableOAuth2Sso
2、添加配置文件
3、写一个简单的前端
@SpringBootApplication
@RestController
@EnableOAuth2Sso
public class SsoClient1Application {
@GetMapping("/user")
public Authentication user(Authentication user) {
return user;
}
public static void main(String[] args) {
SpringApplication.run(SsoClient1Application.class, args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
security.oauth2.client.clientId = imooc1
security.oauth2.client.clientSecret = imoocsecrect1
security.oauth2.client.user-authorization-uri = http://127.0.0.1:9999/server/oauth/authorize
security.oauth2.client.access-token-uri = http://127.0.0.1:9999/server/oauth/token
security.oauth2.resource.jwt.key-uri = http://127.0.0.1:9999/server/oauth/token_key
server.port = 8080
server.context-path = /client1
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SSO Client1</title>
</head>
<body>
<h1>SSO Demo Client1</h1>
<a href="http://127.0.0.1:8060/client2/index.html">访问Client2</a>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
应用B和应用A类似,这样一个简单的SSO Demo就搭建好了。启动来看一下
我们先来访问应用A
会直接引导我们跳转到认证服务器上,输入用户名和密码后,跳转到授权界面
点击授权,完成登录
此时我们点击 访问Client2,同样会跳转到认证服务器,但是这次不用登录,直接授权就可以了
# 改善代码
- 登录方法改为表单登录
- 省去用户去授权,默认直接授权
# 1.设置表单登录
@Configuration
public class SsoSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and().authorizeRequests().anyRequest().authenticated();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 2.重写授权的逻辑
隐藏授权界面,并直接提交,参考源码:WhitelabelApprovalEndpoint
@FrameworkEndpoint // 相当于restController
@SessionAttributes("authorizationRequest")
public class WhitelabelApprovalEndpoint {
@RequestMapping("/oauth/confirm_access")
public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
}
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
@RestController
@SessionAttributes("authorizationRequest")
public class SsoApprovalEndpoint {
@RequestMapping("/oauth/confirm_access")
public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
String template = createTemplate(model, request);
if (request.getAttribute("_csrf") != null) {
model.put("_csrf", request.getAttribute("_csrf"));
}
return new ModelAndView(new SsoSpelView(template), model);
}
protected String createTemplate(Map<String, Object> model, HttpServletRequest request) {
String template = TEMPLATE;
if (model.containsKey("scopes") || request.getAttribute("scopes") != null) {
template = template.replace("%scopes%", createScopes(model, request)).replace("%denial%", "");
}
else {
template = template.replace("%scopes%", "").replace("%denial%", DENIAL);
}
if (model.containsKey("_csrf") || request.getAttribute("_csrf") != null) {
template = template.replace("%csrf%", CSRF);
}
else {
template = template.replace("%csrf%", "");
}
return template;
}
private CharSequence createScopes(Map<String, Object> model, HttpServletRequest request) {
StringBuilder builder = new StringBuilder("<ul>");
@SuppressWarnings("unchecked")
Map<String, String> scopes = (Map<String, String>) (model.containsKey("scopes") ? model.get("scopes") : request
.getAttribute("scopes"));
for (String scope : scopes.keySet()) {
String approved = "true".equals(scopes.get(scope)) ? " checked" : "";
String denied = !"true".equals(scopes.get(scope)) ? " checked" : "";
String value = SCOPE.replace("%scope%", scope).replace("%key%", scope).replace("%approved%", approved)
.replace("%denied%", denied);
builder.append(value);
}
builder.append("</ul>");
return builder.toString();
}
private static String CSRF = "<input type='hidden' name='${_csrf.parameterName}' value='${_csrf.token}' />";
private static String DENIAL = "<form id='denialForm' name='denialForm' action='${path}/oauth/authorize' method='post'><input name='user_oauth_approval' value='false' type='hidden'/>%csrf%<label><input name='deny' value='Deny' type='submit'/></label></form>";
private static String TEMPLATE = "<html><body><div style='display:none;'><h1>OAuth Approval</h1>"
+ "<p>Do you authorize '${authorizationRequest.clientId}' to access your protected resources?</p>"
+ "<form id='confirmationForm' name='confirmationForm' action='${path}/oauth/authorize' method='post'><input name='user_oauth_approval' value='true' type='hidden'/>%csrf%%scopes%<label><input name='authorize' value='Authorize' type='submit'/></label></form>"
+ "%denial%</div><script>document.getElementById('confirmationForm').submit()</script></body></html>";
private static String SCOPE = "<li><div class='form-group'>%scope%: <input type='radio' name='%key%'"
+ " value='true'%approved%>Approve</input> <input type='radio' name='%key%' value='false'%denied%>Deny</input></div></li>";
}
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
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
参考文章:
基于JWT实现SSO单点登录_暴走猿人的博客-CSDN博客_jwt认证实现单点登录原理 (opens new window)