【转载】spring-security-oauth2(二十四) 基于JWT的SSO单点登录

单点登录原理

何谓单点登录? 请看下面这篇博客

单点登录与简单实现

 

 首先创建我们的项目架构,不在原来的项目上进行了,便于理解分析,项目架构如下

 tiger-sso 是pom项目,同时也是tiger-auth的子模块

SSO认证服务器

项目pom文件


 
 
  1. <?xml version= "1.0" encoding= "UTF-8"?>
  2. <project xmlns= "http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation= "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>tiger-sso</artifactId>
  7. <groupId>com.rui.tiger</groupId>
  8. <version> 1.0-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion> 4.0.0</modelVersion>
  11. <groupId>com.rui.tiger</groupId>
  12. <artifactId>sso-server</artifactId>
  13. <dependencies>
  14. <dependency>
  15. <groupId>org.springframework.boot</groupId>
  16. <artifactId>spring-boot-starter-security</artifactId>
  17. </dependency>
  18. <dependency>
  19. <groupId>org.springframework.boot</groupId>
  20. <artifactId>spring-boot-starter-web</artifactId>
  21. </dependency>
  22. <dependency>
  23. <groupId>org.springframework.security.oauth</groupId>
  24. <artifactId>spring-security-oauth2</artifactId>
  25. </dependency>
  26. <dependency>
  27. <groupId>org.springframework.security</groupId>
  28. <artifactId>spring-security-jwt</artifactId>
  29. </dependency>
  30. <dependency>
  31. <groupId>org.springframework.boot</groupId>
  32. <artifactId>spring-boot-starter-test</artifactId>
  33. </dependency>
  34. <dependency>
  35. <groupId>org.springframework.security</groupId>
  36. <artifactId>spring-security-test</artifactId>
  37. </dependency>
  38. </dependencies>
  39. </project>

配置相关


 
 
  1. package com.rui.tiger.sso.server;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.security.crypto.password.NoOpPasswordEncoder;
  5. import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
  6. import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
  7. import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
  8. import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
  9. import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
  10. import org.springframework.security.oauth2.provider.token.TokenStore;
  11. import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
  12. import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
  13. /**
  14. * @author CaiRui
  15. * @Date 2019-05-03 14:29
  16. */
  17. @Configuration
  18. @EnableAuthorizationServer
  19. public class SsoAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
  20. @Override
  21. public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  22. clients.inMemory()
  23. .withClient( "client1")
  24. .secret( "client1s")
  25. .authorizedGrantTypes( "authorization_code", "refresh_token")
  26. .scopes( "all")
  27. .redirectUris(
  28. "http://localhost:8080/client1/login")
  29. .and()
  30. .withClient( "client2")
  31. .secret( "client2s")
  32. .authorizedGrantTypes( "authorization_code", "refresh_token")
  33. .scopes( "all")
  34. .redirectUris(
  35. "http://localhost:8060/client2/login");
  36. }
  37. @Override
  38. public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  39. endpoints.tokenStore(jwtTokenStore()).accessTokenConverter(jwtAccessTokenConverter());
  40. }
  41. @Override
  42. public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
  43. // 客户端来向认证服务器获取签名的时候需要登录认证身份才能获取 因为客户端需要用密钥解密jwt字符串
  44. security.passwordEncoder(NoOpPasswordEncoder.getInstance());
  45. security.tokenKeyAccess( "isAuthenticated()");
  46. }
  47. @Bean
  48. public TokenStore jwtTokenStore() {
  49. return new JwtTokenStore(jwtAccessTokenConverter());
  50. }
  51. @Bean
  52. public JwtAccessTokenConverter jwtAccessTokenConverter() {
  53. JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
  54. converter.setSigningKey( "tiger"); //秘钥
  55. return converter;
  56. }
  57. }

修改成默认basic登录


 
 
  1. package com.rui.tiger.sso.server;
  2. import org.springframework.context.annotation.Configuration;
  3. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  4. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  5. /**
  6. * @author CaiRui
  7. * @Date 2019-05-03 14:41
  8. */
  9. @Configuration
  10. public class SsoServerSecurityConfig extends WebSecurityConfigurerAdapter {
  11. @Override
  12. protected void configure(HttpSecurity http) throws Exception {
  13. //security5+ 认证默认为表单了也就是http.formLogin()
  14. http.httpBasic();
  15. }
  16. }

服务启动类及配置文件


 
 
  1. package com.rui.tiger.sso.server;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. import org.springframework.test.context.junit4.SpringRunner;
  5. /**
  6. * @author CaiRui
  7. * @Date 2019-05-03 14:45
  8. */
  9. @SpringBootApplication
  10. public class SsoServerApplication {
  11. public static void main(String[] args) {
  12. SpringApplication.run(SsoServerApplication.class,args);
  13. }
  14. }

application.yml


 
 
  1. server:
  2. port: 9999
  3. servlet:
  4. context-path: /server
  5. spring:
  6. security:
  7. user:
  8. password: 123456

SSO客户端

client1和client2配置

客户端1和客户端2配置基本都一样,只是端口号和项目路径不一样,可自行配置

pom文件依赖


 
 
  1. <?xml version= "1.0" encoding= "UTF-8"?>
  2. <project xmlns= "http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation= "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>tiger-sso</artifactId>
  7. <groupId>com.rui.tiger</groupId>
  8. <version> 1.0-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion> 4.0 .0</modelVersion>
  11. <groupId>com.rui.tiger</groupId>
  12. <artifactId>sso-client1</artifactId>
  13. <dependencies>
  14. <dependency>
  15. <groupId>org.springframework.boot</groupId>
  16. <artifactId>spring-boot-starter-security</artifactId>
  17. </dependency>
  18. <!-- @EnableOAuth2Sso 单点登录注解 -->
  19. <dependency>
  20. <groupId>org.springframework.security.oauth.boot</groupId>
  21. <artifactId>spring-security-oauth2-autoconfigure</artifactId>
  22. </dependency>
  23. <dependency>
  24. <groupId>org.springframework.boot</groupId>
  25. <artifactId>spring-boot-starter-web</artifactId>
  26. </dependency>
  27. <dependency>
  28. <groupId>org.springframework.security.oauth</groupId>
  29. <artifactId>spring-security-oauth2</artifactId>
  30. </dependency>
  31. <dependency>
  32. <groupId>org.springframework.security</groupId>
  33. <artifactId>spring-security-jwt</artifactId>
  34. </dependency>
  35. <dependency>
  36. <groupId>org.springframework.boot</groupId>
  37. <artifactId>spring-boot-starter-test</artifactId>
  38. </dependency>
  39. <dependency>
  40. <groupId>org.springframework.security</groupId>
  41. <artifactId>spring-security-test</artifactId>
  42. </dependency>
  43. </dependencies>
  44. </project>

服务启动类


 
 
  1. package com.rui.tiger.sso.client1;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
  5. import org.springframework.security.core.Authentication;
  6. import org.springframework.web.bind.annotation.GetMapping;
  7. import org.springframework.web.bind.annotation.RestController;
  8. /**
  9. * @author CaiRui
  10. * @date 2019-05-05 14:03
  11. */
  12. @SpringBootApplication
  13. @EnableOAuth2Sso
  14. @RestController
  15. public class SsoClient1Application {
  16. public static void main(String[] args) {
  17. SpringApplication.run(SsoClient1Application.class, args);
  18. }
  19. //编写一个获取当前服务器的用户信息控制器
  20. @GetMapping( "/user")
  21. public Authentication user(Authentication user){
  22. return user;
  23. }
  24. }

application.yml配置文件


 
 
  1. security:
  2. oauth2:
  3. client:
  4. clientId: client1
  5. clientSecret: client1s
  6. user-authorization-uri: http://127.0.0.1:9999/server/oauth/authorize
  7. access-token-uri: http://127.0.0.1:9999/server/oauth/token
  8. resource:
  9. jwt:
  10. key-uri: http://127.0.0.1:9999/server/oauth/token_key
  11. user-info-uri: http://127.0.0.1:9999/server/user
  12. token-info-uri: http://127.0.0.1:9999/server/oauth/check_token
  13. preferTokenInfo: false
  14. server:
  15. port: 8080
  16. servlet:
  17. context-path: /client1

添加项目首页文件 /static/index.html 


 
 
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>SSO Client1 </title>
  6. </head>
  7. <body>
  8. <h1>SSO Demo Client1 </h1>
  9. <a href="http://localhost:8060/client2/index.html">访问Client2 </a>
  10. </body>
  11. </html>

client2配置同上面类似,注意更换端口号和首页跳转地址即可。

单点跳转测试

分别启动server,client1,client2项目

1.访问 http://localhost:8080/client1

2.这个时候会跳转到 端口9999的认证服务器,点击同意授权后,输入用户(默认用户user)和密码(123456),跳转到client1首页

3.点击跳转到client2,授权后跳转到client2首页

4.最后2个客户端之间就可以互相跳转

原理:

org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint#authorize

具体原理等有时间再看源码分析

测试问题

然后这里体验之后会发现3个问题: 
1. 认证服务器的登录页面 是basci

期望效果:使用自定义的登录页面 
2. 每次都需要授权

期望效果:第一次登录的时候授权,后面跳转到其他应用不需要手动点击授权了 
3. 不是自定义的用户

期望:可以自定义用户

认证服务器自定义登录界面和用户

自定义用户认证


 
 
  1. package com.rui.tiger.sso.server.service;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.security.core.authority.AuthorityUtils;
  5. import org.springframework.security.core.userdetails.User;
  6. import org.springframework.security.core.userdetails.UserDetails;
  7. import org.springframework.security.core.userdetails.UserDetailsService;
  8. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  9. import org.springframework.security.crypto.password.PasswordEncoder;
  10. import org.springframework.stereotype.Component;
  11. /**
  12. * @author CaiRui
  13. * @date 2019-05-05 15:42
  14. */
  15. @Component
  16. @Slf4j
  17. public class SsoUserDetailsService implements UserDetailsService {
  18. @Autowired
  19. private PasswordEncoder passwordEncoder;
  20. @Override
  21. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  22. String password = passwordEncoder.encode( "123456");
  23. //TODO 这里实际是查询数据库获取
  24. log.info( "用户名 {},数据库密码{}", username, password);
  25. User admin = new User(username,
  26. // "{noop}123456",
  27. password,
  28. true, true, true, true,
  29. AuthorityUtils.commaSeparatedStringToAuthorityList( ""));
  30. return admin;
  31. }
  32. }

表单权限配置


 
 
  1. package com.rui.tiger.sso.server;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
  6. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  7. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  8. import org.springframework.security.core.userdetails.UserDetailsService;
  9. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  10. import org.springframework.security.crypto.password.PasswordEncoder;
  11. /**
  12. * @author CaiRui
  13. * @Date 2019-05-03 14:41
  14. */
  15. @Configuration
  16. public class SsoServerSecurityConfig extends WebSecurityConfigurerAdapter {
  17. @Autowired
  18. private UserDetailsService userDetailsService;
  19. @Bean
  20. public PasswordEncoder passwordEncoder() {
  21. return new BCryptPasswordEncoder();
  22. }
  23. @Override
  24. protected void configure(HttpSecurity http) throws Exception {
  25. http
  26. // .httpBasic()
  27. .formLogin() // 更改为form表单登录
  28. .and()
  29. // 所有的请求都必须授权后才能访问
  30. .authorizeRequests()
  31. .anyRequest()
  32. .authenticated();
  33. ;
  34. }
  35. @Override
  36. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  37. auth.userDetailsService(userDetailsService)
  38. .passwordEncoder(passwordEncoder());
  39. }
  40. }

自动授权

通过授权界面关键字OAuth Approval全局搜索定位到如下方法:

org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint#createTemplate

改下此段逻辑,隐藏自动授权和自动提交表单,主要是加注释的地方进行修改。


 
 
  1. package com.rui.tiger.sso.server;
  2. import org.springframework.security.oauth2.provider.AuthorizationRequest;
  3. import org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint;
  4. import org.springframework.security.web.csrf.CsrfToken;
  5. import org.springframework.web.bind.annotation.RequestMapping;
  6. import org.springframework.web.bind.annotation.RestController;
  7. import org.springframework.web.bind.annotation.SessionAttributes;
  8. import org.springframework.web.servlet.ModelAndView;
  9. import org.springframework.web.servlet.View;
  10. import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
  11. import org.springframework.web.util.HtmlUtils;
  12. import javax.servlet.http.HttpServletRequest;
  13. import javax.servlet.http.HttpServletResponse;
  14. import java.util.Map;
  15. /**
  16. * 自动授权修改
  17. * 授权确认服务:不能继承 WhitelabelApprovalEndpoint,因为FrameworkEndpoint会被扫描,就会存在两个一样的地址;报错
  18. *
  19. * @author CaiRui
  20. * @date 2019-05-05 15:59
  21. * @see WhitelabelApprovalEndpoint
  22. */
  23. @RestController
  24. @SessionAttributes( "authorizationRequest")
  25. public class MyWhitelabelApprovalEndpoint {
  26. @RequestMapping( "/oauth/confirm_access")
  27. public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
  28. final String approvalContent = createTemplate(model, request);
  29. if (request.getAttribute( "_csrf") != null) {
  30. model.put( "_csrf", request.getAttribute( "_csrf"));
  31. }
  32. View approvalView = new View() {
  33. @Override
  34. public String getContentType() {
  35. return "text/html";
  36. }
  37. @Override
  38. public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
  39. response.setContentType(getContentType());
  40. response.getWriter().append(approvalContent);
  41. }
  42. };
  43. return new ModelAndView(approvalView, model);
  44. }
  45. protected String createTemplate(Map<String, Object> model, HttpServletRequest request) {
  46. AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get( "authorizationRequest");
  47. String clientId = authorizationRequest.getClientId();
  48. StringBuilder builder = new StringBuilder();
  49. // 让body不显示
  50. builder.append( "<html><body style='display:none;'><h1>OAuth Approval</h1>");
  51. builder.append( "<p>Do you authorize \"").append(HtmlUtils.htmlEscape(clientId));
  52. builder.append( "\" to access your protected resources?</p>");
  53. builder.append( "<form id=\"confirmationForm\" name=\"confirmationForm\" action=\"");
  54. String requestPath = ServletUriComponentsBuilder.fromContextPath(request).build().getPath();
  55. if (requestPath == null) {
  56. requestPath = "";
  57. }
  58. builder.append(requestPath).append( "/oauth/authorize\" method=\"post\">");
  59. builder.append( "<input name=\"user_oauth_approval\" value=\"true\" type=\"hidden\"/>");
  60. String csrfTemplate = null;
  61. CsrfToken csrfToken = (CsrfToken) (model.containsKey( "_csrf") ? model.get( "_csrf") : request.getAttribute( "_csrf"));
  62. if (csrfToken != null) {
  63. csrfTemplate = "<input type=\"hidden\" name=\"" + HtmlUtils.htmlEscape(csrfToken.getParameterName()) +
  64. "\" value=\"" + HtmlUtils.htmlEscape(csrfToken.getToken()) + "\" />";
  65. }
  66. if (csrfTemplate != null) {
  67. builder.append(csrfTemplate);
  68. }
  69. String authorizeInputTemplate = "<label><input name=\"authorize\" value=\"Authorize\" type=\"submit\"/></label></form>";
  70. if (model.containsKey( "scopes") || request.getAttribute( "scopes") != null) {
  71. builder.append(createScopes(model, request));
  72. builder.append(authorizeInputTemplate);
  73. } else {
  74. builder.append(authorizeInputTemplate);
  75. builder.append( "<form id=\"denialForm\" name=\"denialForm\" action=\"");
  76. builder.append(requestPath).append( "/oauth/authorize\" method=\"post\">");
  77. builder.append( "<input name=\"user_oauth_approval\" value=\"false\" type=\"hidden\"/>");
  78. if (csrfTemplate != null) {
  79. builder.append(csrfTemplate);
  80. }
  81. builder.append( "<label><input name=\"deny\" value=\"Deny\" type=\"submit\"/></label></form>");
  82. }
  83. // 添加自动提交操作
  84. builder.append( "<script>document.getElementById('confirmationForm').submit()</script>");
  85. builder.append( "</body></html>");
  86. return builder.toString();
  87. }
  88. private CharSequence createScopes(Map<String, Object> model, HttpServletRequest request) {
  89. StringBuilder builder = new StringBuilder( "<ul>");
  90. @SuppressWarnings( "unchecked")
  91. Map<String, String> scopes = (Map<String, String>) (model.containsKey( "scopes") ?
  92. model.get( "scopes") : request.getAttribute( "scopes"));
  93. for (String scope : scopes.keySet()) {
  94. String approved = "true".equals(scopes.get(scope)) ? " checked" : "";
  95. String denied = ! "true".equals(scopes.get(scope)) ? " checked" : "";
  96. scope = HtmlUtils.htmlEscape(scope);
  97. builder.append( "<li><div class=\"form-group\">");
  98. builder.append(scope).append( ": <input type=\"radio\" name=\"");
  99. builder.append(scope).append( "\" value=\"true\"").append(approved).append( ">Approve</input> ");
  100. builder.append( "<input type=\"radio\" name=\"").append(scope).append( "\" value=\"false\"");
  101. builder.append(denied).append( ">Deny</input></div></li>");
  102. }
  103. builder.append( "</ul>");
  104. return builder.toString();
  105. }
  106. }

经测试我们上面的三个期望都已成功实现。

文章转载至:[https://blog.csdn.net/ahcr1026212/article/details/89846168](https://blog.csdn.net/ahcr1026212/article/details/89846168)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值