SpringSecurityOAuth使用JWT Token实现SSO单点登录
⒈认证服务器
1.添加pom依赖
1 <dependency> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-web</artifactId> 4 </dependency> 5 6 <dependency> 7 <groupId>org.springframework.cloud</groupId> 8 <artifactId>spring-cloud-starter-oauth2</artifactId> 9 <version>2.1.2.RELEASE</version> 10 </dependency> 11 12 <dependency> 13 <groupId>org.springframework.boot</groupId> 14 <artifactId>spring-boot-starter-test</artifactId> 15 <scope>test</scope> 16 </dependency> 17 <dependency> 18 <groupId>org.springframework.security</groupId> 19 <artifactId>spring-security-test</artifactId> 20 <scope>test</scope> 21 </dependency>
2.配置文件相关配置
1 server.port=10086 2 server.servlet.context-path=/server
3.Security配置
1 package cn.coreqi.ssoserver.config; 2 3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.beans.factory.annotation.Qualifier; 5 import org.springframework.context.annotation.Bean; 6 import org.springframework.security.authentication.AuthenticationManager; 7 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 8 import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 11 import org.springframework.security.core.userdetails.UserDetailsService; 12 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 13 import org.springframework.security.crypto.factory.PasswordEncoderFactories; 14 import org.springframework.security.crypto.password.NoOpPasswordEncoder; 15 import org.springframework.security.crypto.password.PasswordEncoder; 16 17 @EnableWebSecurity 18 public class SsoWebSecurityConfig extends WebSecurityConfigurerAdapter { 19 20 @Override 21 @Bean 22 public AuthenticationManager authenticationManagerBean() throws Exception { 23 return super.authenticationManagerBean(); 24 } 25 26 @Qualifier("ssoUserDetailsService") 27 @Autowired 28 private UserDetailsService userDetailsService; 29 30 @Bean 31 public PasswordEncoder passwordEncoder() 32 { 33 //return NoOpPasswordEncoder.getInstance(); 34 //return new BCryptPasswordEncoder(); 35 return PasswordEncoderFactories.createDelegatingPasswordEncoder(); 36 } 37 38 @Override 39 protected void configure(HttpSecurity http) throws Exception { 40 http.formLogin() 41 .and() 42 .authorizeRequests() 43 .antMatchers("/oauth/*","/login/*").permitAll() 44 .anyRequest().authenticated() //任何请求都需要身份认证 45 .and().csrf().disable(); //禁用CSRF 46 } 47 48 @Override 49 protected void configure(AuthenticationManagerBuilder auth) throws Exception { 50 // auth.inMemoryAuthentication() 51 // .withUser("fanqi").password("admin").roles("admin"); 52 auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); 53 } 54 }
⒋用户登录逻辑
1 package cn.coreqi.ssoserver.service; 2 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 @Component 13 public class SsoUserDetailsService implements UserDetailsService { 14 15 @Autowired 16 private PasswordEncoder passwordEncoder; 17 18 @Override 19 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 20 return new User(username,passwordEncoder.encode("admin"), 21 AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN")); 22 } 23 }
5.认证服务器配置
1 package cn.coreqi.ssoserver.config; 2 3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.context.annotation.Bean; 5 import org.springframework.context.annotation.Configuration; 6 import org.springframework.security.authentication.AuthenticationManager; 7 import org.springframework.security.crypto.password.PasswordEncoder; 8 import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; 9 import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; 10 import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; 11 import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; 12 import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; 13 import org.springframework.security.oauth2.provider.token.TokenStore; 14 import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; 15 import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; 16 17 @Configuration 18 @EnableAuthorizationServer //声明当前应用为认证服务器 19 public class SsoAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { 20 21 @Autowired 22 private AuthenticationManager authenticationManagerBean; 23 24 @Autowired 25 private PasswordEncoder passwordEncoder; 26 27 /** 28 * Token生成过程处理 29 * @return 30 */ 31 @Bean 32 public JwtAccessTokenConverter jwtAccessTokenConverter(){ 33 JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); 34 accessTokenConverter.setSigningKey("fanqi"); //Token签名用的密钥 35 //发出去的令牌需要密钥签名,验令牌的时候也需要令牌来验签,如果他人获知了我们的密钥 36 //就可以用我们的密钥来签发我们的JWT令牌,JWT唯一的安全性就是密钥 37 //别人用我们的密钥来签发我们的JWT令牌就可以随意进入我们的系统 38 return accessTokenConverter; 39 } 40 41 @Bean 42 public TokenStore jwtTokenStore(){ 43 return new JwtTokenStore(jwtAccessTokenConverter()); 44 } 45 46 /** 47 * 针对端点的配置 48 * @param endpoints 49 * @throws Exception 50 */ 51 @Override 52 public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { 53 endpoints 54 .tokenStore(jwtTokenStore()) 55 .accessTokenConverter(jwtAccessTokenConverter()) 56 .authenticationManager(authenticationManagerBean); 57 } 58 59 /** 60 * 配置当前认证服务器可以给那些应用发令牌 61 * @param clients 62 * @throws Exception 63 */ 64 @Override 65 public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 66 67 clients.inMemory() 68 .withClient("coreqi1") 69 .secret(passwordEncoder.encode("coreqisecret1")) 70 .authorizedGrantTypes("authorization_code","refresh_token") 71 .redirectUris("http://localhost:10010/client1/login","http://127.0.0.1:10010/client1/login") 72 .scopes("all") 73 .and() 74 .withClient("coreqi2") 75 .secret(passwordEncoder.encode("coreqisecret2")) 76 .authorizedGrantTypes("authorization_code","refresh_token") 77 .redirectUris("http://localhost:10000/client2/login","http://127.0.0.1:10000/client2/login") 78 .scopes("all"); 79 } 80 81 /** 82 * 针对安全性有关的配置 83 * @param security 84 * @throws Exception 85 */ 86 @Override 87 public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { 88 security 89 .tokenKeyAccess("isAuthenticated()"); //访问认证服务器的tokenKey(Token签名密钥)时需要身份认证 90 } 91 }
6.覆写登录授权页面,直接跳过
1 package cn.coreqi.ssoserver.controller; 2 3 import org.springframework.security.oauth2.provider.AuthorizationRequest; 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 13 import javax.servlet.http.HttpServletRequest; 14 import javax.servlet.http.HttpServletResponse; 15 import java.util.Iterator; 16 import java.util.Map; 17 18 /** 19 * 登录完不授权直接进入网站 20 * 授权的页面根据OAuth协议是无法直接跳过去的 21 * 因此,我们模仿WhitelabelApprovalEndpoint类,在表单逻辑处直接提交 22 */ 23 @RestController 24 @SessionAttributes({"authorizationRequest"}) 25 public class SsoApprovalEndpoint { 26 @RequestMapping({"/oauth/confirm_access"}) 27 public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception { 28 final String approvalContent = this.createTemplate(model, request); 29 if (request.getAttribute("_csrf") != null) { 30 model.put("_csrf", request.getAttribute("_csrf")); 31 } 32 33 View approvalView = new View() { 34 public String getContentType() { 35 return "text/html"; 36 } 37 38 public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { 39 response.setContentType(this.getContentType()); 40 response.getWriter().append(approvalContent); 41 } 42 }; 43 return new ModelAndView(approvalView, model); 44 } 45 46 protected String createTemplate(Map<String, Object> model, HttpServletRequest request) { 47 AuthorizationRequest authorizationRequest = (AuthorizationRequest)model.get("authorizationRequest"); 48 String clientId = authorizationRequest.getClientId(); 49 StringBuilder builder = new StringBuilder(); 50 builder.append("<html><body><div 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 59 builder.append(requestPath).append("/oauth/authorize\" method=\"post\">"); 60 builder.append("<input name=\"user_oauth_approval\" value=\"true\" type=\"hidden\"/>"); 61 String csrfTemplate = null; 62 CsrfToken csrfToken = (CsrfToken)((CsrfToken)(model.containsKey("_csrf") ? model.get("_csrf") : request.getAttribute("_csrf"))); 63 if (csrfToken != null) { 64 csrfTemplate = "<input type=\"hidden\" name=\"" + HtmlUtils.htmlEscape(csrfToken.getParameterName()) + "\" value=\"" + HtmlUtils.htmlEscape(csrfToken.getToken()) + "\" />"; 65 } 66 67 if (csrfTemplate != null) { 68 builder.append(csrfTemplate); 69 } 70 71 String authorizeInputTemplate = "<label><input name=\"authorize\" value=\"Authorize\" type=\"submit\"/></label></form>"; 72 if (!model.containsKey("scopes") && request.getAttribute("scopes") == null) { 73 builder.append(authorizeInputTemplate); 74 builder.append("<form id=\"denialForm\" name=\"denialForm\" action=\""); 75 builder.append(requestPath).append("/oauth/authorize\" method=\"post\">"); 76 builder.append("<input name=\"user_oauth_approval\" value=\"false\" type=\"hidden\"/>"); 77 if (csrfTemplate != null) { 78 builder.append(csrfTemplate); 79 } 80 81 builder.append("<label><input name=\"deny\" value=\"Deny\" type=\"submit\"/></label></form>"); 82 } else { 83 builder.append(this.createScopes(model, request)); 84 builder.append(authorizeInputTemplate); 85 } 86 87 builder.append("</div><script>document.getElementById('confirmationForm').submit()</script></body></html>"); 88 return builder.toString(); 89 } 90 91 private CharSequence createScopes(Map<String, Object> model, HttpServletRequest request) { 92 StringBuilder builder = new StringBuilder("<ul>"); 93 Map<String, String> scopes = (Map)((Map)(model.containsKey("scopes") ? model.get("scopes") : request.getAttribute("scopes"))); 94 Iterator var5 = scopes.keySet().iterator(); 95 96 while(var5.hasNext()) { 97 String scope = (String)var5.next(); 98 String approved = "true".equals(scopes.get(scope)) ? " checked" : ""; 99 String denied = !"true".equals(scopes.get(scope)) ? " checked" : ""; 100 scope = HtmlUtils.htmlEscape(scope); 101 builder.append("<li><div class=\"form-group\">"); 102 builder.append(scope).append(": <input type=\"radio\" name=\""); 103 builder.append(scope).append("\" value=\"true\"").append(approved).append(">Approve</input> "); 104 builder.append("<input type=\"radio\" name=\"").append(scope).append("\" value=\"false\""); 105 builder.append(denied).append(">Deny</input></div></li>"); 106 } 107 108 builder.append("</ul>"); 109 return builder.toString(); 110 } 111 112 }
⒉应用A
1.pom依赖
1 <dependency> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-web</artifactId> 4 </dependency> 5 6 <dependency> 7 <groupId>org.springframework.cloud</groupId> 8 <artifactId>spring-cloud-starter-oauth2</artifactId> 9 <version>2.1.2.RELEASE</version> 10 </dependency> 11 12 <dependency> 13 <groupId>org.springframework.boot</groupId> 14 <artifactId>spring-boot-starter-test</artifactId> 15 <scope>test</scope> 16 </dependency> 17 <dependency> 18 <groupId>org.springframework.security</groupId> 19 <artifactId>spring-security-test</artifactId> 20 <scope>test</scope> 21 </dependency>
2.配置文件相关配置
1 server.port=10010 2 server.servlet.context-path=/client1 3 security.oauth2.client.client-id=coreqi1 4 security.oauth2.client.client-secret=coreqisecret1 5 security.oauth2.client.scope=all 6 security.oauth2.client.user-authorization-uri=http://127.0.0.1:10086/server/oauth/authorize 7 security.oauth2.client.access-token-uri=http://127.0.0.1:10086/server/oauth/token 8 security.oauth2.resource.jwt.key-uri=http://127.0.0.1:10086/server/oauth/token_key
3.主程序类添加@EnableOAuth2Sso注解使之生效
1 package cn.coreqi; 2 3 import org.springframework.boot.SpringApplication; 4 import org.springframework.boot.autoconfigure.SpringBootApplication; 5 import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; 6 7 @SpringBootApplication 8 @EnableOAuth2Sso //使sso生效 9 public class SsoClient1Application { 10 11 public static void main(String[] args) { 12 SpringApplication.run(SsoClient1Application.class, args); 13 } 14 15 }
4.编写Action接口用于查看授权信息
1 package cn.coreqi.sso_client1.controller; 2 3 import org.springframework.security.core.Authentication; 4 import org.springframework.web.bind.annotation.GetMapping; 5 import org.springframework.web.bind.annotation.RequestMapping; 6 import org.springframework.web.bind.annotation.RestController; 7 8 @RestController 9 @RequestMapping("/user") 10 public class UserController { 11 12 @GetMapping 13 public Authentication user(Authentication user){ 14 return user; 15 } 16 }
5.编写resources/static/index.html文件,用于跳转测试
1 <!DOCTYPE html> 2 <html lang="en"> 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://127.0.0.1:10000/client2/index.html">访问Client2</a> 10 </body> 11 </html>
⒊应用B
1.添加pom依赖
1 <dependency> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-web</artifactId> 4 </dependency> 5 6 <dependency> 7 <groupId>org.springframework.cloud</groupId> 8 <artifactId>spring-cloud-starter-oauth2</artifactId> 9 <version>2.1.2.RELEASE</version> 10 </dependency> 11 12 <dependency> 13 <groupId>org.springframework.boot</groupId> 14 <artifactId>spring-boot-starter-test</artifactId> 15 <scope>test</scope> 16 </dependency> 17 <dependency> 18 <groupId>org.springframework.security</groupId> 19 <artifactId>spring-security-test</artifactId> 20 <scope>test</scope> 21 </dependency>
2.配置文件相关配置
1 server.port=10000 2 server.servlet.context-path=/client2 3 security.oauth2.client.client-id=coreqi2 4 security.oauth2.client.client-secret=coreqisecret2 5 security.oauth2.client.scope=all 6 security.oauth2.client.user-authorization-uri=http://127.0.0.1:10086/server/oauth/authorize 7 security.oauth2.client.access-token-uri=http://127.0.0.1:10086/server/oauth/token 8 security.oauth2.resource.jwt.key-uri=http://127.0.0.1:10086/server/oauth/token_key
3.主程序类添加@EnableOAuth2Sso注解使之生效
1 package cn.coreqi; 2 3 import org.springframework.boot.SpringApplication; 4 import org.springframework.boot.autoconfigure.SpringBootApplication; 5 import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; 6 7 @SpringBootApplication 8 @EnableOAuth2Sso //使sso生效 9 public class SsoClient2Application { 10 11 public static void main(String[] args) { 12 SpringApplication.run(SsoClient2Application.class, args); 13 } 14 15 }
4.编写Action接口用于查看授权信息
1 package cn.coreqi.sso_client2.controller; 2 3 import org.springframework.security.core.Authentication; 4 import org.springframework.web.bind.annotation.GetMapping; 5 import org.springframework.web.bind.annotation.RequestMapping; 6 import org.springframework.web.bind.annotation.RestController; 7 8 @RestController 9 @RequestMapping("/user") 10 public class UserController { 11 12 @GetMapping 13 public Authentication user(Authentication user){ 14 return user; 15 } 16 }
5.编写resources/static/index.html文件,用于跳转测试
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>SSO Client2</title> 6 </head> 7 <body> 8 <h1> SSO Demo Client2</h1> 9 <a href="http://127.0.0.1:10010/client1/index.html">访问Client1</a> 10 </body> 11 </html>