1. 新建sso父工程 pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.security</groupId>
<artifactId>sso</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring.security.oauth2.autoconfigure.version>2.1.6.RELEASE</spring.security.oauth2.autoconfigure.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${spring.security.oauth2.autoconfigure.version}</version>
</dependency>
</dependencies>
<modules>
<module>sso-server</module>
<module>sso-client_a</module>
<module>sso-client_b</module>
</modules>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
2. 新建sso-server工程 pom.xml如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>sso</artifactId>
<groupId>com.security</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sso-server</artifactId>
</project>
3. 分别建立sso-client_a和sso-client_b子模块pom.xml如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>sso</artifactId>
<groupId>com.security</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sso-client_a</artifactId>
<!-- 单独建立 -->
<!-- <artifactId>sso-client_b</artifactId> -->
</project>
4. sso-service编写
package com.security.sso.authentication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* 认证相关的扩展点配置。配置在这里的bean,业务系统都可以通过声明同类型或同名的bean来覆盖安全
* 模块默认的配置。
*/
@Configuration
public class AuthenticationBeanConfig {
/**
* 默认密码处理器
*
* @return
*/
@Bean
@ConditionalOnMissingBean(PasswordEncoder.class)
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
package com.security.sso.authentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component(value = "userDetailsService")
public class DefaultUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
/* (non-Javadoc)
* @see org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername(java.lang.String)
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String password = passwordEncoder.encode("123456");
System.out.println("用户名: ["+ username +"],数据库密码: ["+ password +"]");
User admin = new User(username, password,
true, true, true, true,
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
return admin;
}
}
package com.security.sso.authentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
@EnableAuthorizationServer
public class SsoAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("healthy")
.secret(passwordEncoder.encode("healthysecrect"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.redirectUris(
"http://localhost:8080/client1/login")
.and()
.withClient("demo")
.secret(passwordEncoder.encode("demosecrect"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.redirectUris(
"http://localhost:8081/client2/login");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(jwtTokenStore()).accessTokenConverter(jwtAccessTokenConverter());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 客户端来向认证服务器获取签名的时候需要登录认证身份才能获取
// 因为客户端需要用密钥解密jwt字符串
security
.tokenKeyAccess("isAuthenticated()")
.passwordEncoder(passwordEncoder); // 密码加密策略;
}
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("imooc");
return converter;
}
}
package com.security.sso.authentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SsoSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
// .httpBasic()
.formLogin() // 更改为form表单登录
.and()
// 所有的请求都必须授权后才能访问
.authorizeRequests()
.antMatchers("/error").permitAll()
.anyRequest()
.authenticated();
}
}
package com.security.sso.authentication;
import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.web.server.csrf.CsrfToken;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.HtmlUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Iterator;
import java.util.Map;
/**
* 自定义的授权页面
*/
@RestController
@SessionAttributes({"authorizationRequest"})
public class SsoWhitelabelApprovalEndpoint {
public SsoWhitelabelApprovalEndpoint() {
}
@RequestMapping({"/oauth/confirm_access"})
public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
final String approvalContent = this.createTemplate(model, request);
if (request.getAttribute("_csrf") != null) {
model.put("_csrf", request.getAttribute("_csrf"));
}
View approvalView = new View() {
@Override
public String getContentType() {
return "text/html";
}
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setContentType(this.getContentType());
response.getWriter().append(approvalContent);
}
};
return new ModelAndView(approvalView, model);
}
protected String createTemplate(Map<String, Object> model, HttpServletRequest request) {
AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get("authorizationRequest");
String clientId = authorizationRequest.getClientId();
StringBuilder builder = new StringBuilder();
// style='display:none;'设置body不显示
builder.append("<html><body style='display:none;'><h1>OAuth Approval</h1>");
builder.append("<p>Do you authorize \"").append(HtmlUtils.htmlEscape(clientId));
builder.append("\" to access your protected resources?</p>");
builder.append("<form id=\"confirmationForm\" name=\"confirmationForm\" action=\"");
String requestPath = ServletUriComponentsBuilder.fromContextPath(request).build().getPath();
if (requestPath == null) {
requestPath = "";
}
builder.append(requestPath).append("/oauth/authorize\" method=\"post\">");
builder.append("<input name=\"user_oauth_approval\" value=\"true\" type=\"hidden\"/>");
String csrfTemplate = null;
CsrfToken csrfToken = (CsrfToken) ((CsrfToken) (model.containsKey("_csrf") ? model.get("_csrf") : request.getAttribute("_csrf")));
if (csrfToken != null) {
csrfTemplate = "<input type=\"hidden\" name=\"" + HtmlUtils.htmlEscape(csrfToken.getParameterName()) + "\" value=\"" + HtmlUtils.htmlEscape(csrfToken.getToken()) + "\" />";
}
if (csrfTemplate != null) {
builder.append(csrfTemplate);
}
String authorizeInputTemplate = "<label><input name=\"authorize\" value=\"Authorize\" type=\"submit\"/></label></form>";
if (!model.containsKey("scopes") && request.getAttribute("scopes") == null) {
builder.append(authorizeInputTemplate);
builder.append("<form id=\"denialForm\" name=\"denialForm\" action=\"");
builder.append(requestPath).append("/oauth/authorize\" method=\"post\">");
builder.append("<input name=\"user_oauth_approval\" value=\"false\" type=\"hidden\"/>");
if (csrfTemplate != null) {
builder.append(csrfTemplate);
}
builder.append("<label><input name=\"deny\" value=\"Deny\" type=\"submit\"/></label></form>");
} else {
builder.append(this.createScopes(model, request));
builder.append(authorizeInputTemplate);
}
// 添加提交表单的事件
builder.append("<script>document.getElementById('confirmationForm').submit()</script>");
builder.append("</body></html>");
return builder.toString();
}
private CharSequence createScopes(Map<String, Object> model, HttpServletRequest request) {
StringBuilder builder = new StringBuilder("<ul>");
Map<String, String> scopes = (Map) ((Map) (model.containsKey("scopes") ? model.get("scopes") : request.getAttribute("scopes")));
Iterator var5 = scopes.keySet().iterator();
while (var5.hasNext()) {
String scope = (String) var5.next();
String approved = "true".equals(scopes.get(scope)) ? " checked" : "";
String denied = !"true".equals(scopes.get(scope)) ? " checked" : "";
scope = HtmlUtils.htmlEscape(scope);
builder.append("<li><div class=\"form-group\">");
builder.append(scope).append(": <input type=\"radio\" name=\"");
builder.append(scope).append("\" value=\"true\"").append(approved).append(">Approve</input> ");
builder.append("<input type=\"radio\" name=\"").append(scope).append("\" value=\"false\"");
builder.append(denied).append(">Deny</input></div></li>");
}
builder.append("</ul>");
return builder.toString();
}
}
yml
server:
port: 9999
servlet:
context-path: /server
spring:
security:
user:
password: 123456
package com.security.sso;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SsoServerApplication {
/**
* 官网文档: https://docs.spring.io/spring-security-oauth2-boot/docs/current/reference/html5/#boot-features-security-oauth2-single-sign-on
* 客户端application.yml 需配置:user-info-uri 和 token-info-uri 不配置就会报错;可以参考官网文档
*
* OAuth Error error="invalid_grant", error_description="Invalid redirect: http://xxxx/login does not match one of the registered values: xxxx"
* 如上错误:请检查com.security.sso.authentication.SsoAuthorizationServerConfig中的redirectUris()
*
* 自定义授权页面
* @FrameworkEndpoint注解 的优先级没有@RestController的优先级高,直接定义同样的配置控制器路径即可,会优先使用自定义的处理;
* 默认处理:org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint
* 自定义处理(默认直接提交表单):com.security.sso.authentication.SsoWhitelabelApprovalEndpoint
*
* Error creating bean with name 'jwtTokenServices'
* 如上错误请检查:com.security.sso.authentication.SsoAuthorizationServerConfig&configure(ClientDetailsServiceConfigurer clients)中的secret(passwordEncoder.encode("xxx"))是否使用了加密;
*
*/
public static void main(String[] args) {
SpringApplication.run(SsoServerApplication.class, args);
}
}
5. 编写sso-client_a项目配置
package com.security.sso.client;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
@EnableOAuth2Sso // 开启单点登录
public class SsoClient1Application {
public static void main(String[] args) {
SpringApplication.run(SsoClient1Application.class, args);
}
//编写一个获取当前服务器的用户信息控制器
@GetMapping("/user")
public Authentication user(Authentication user){
return user;
}
}
security:
oauth2:
client:
clientId: healthy
clientSecret: healthysecrect
user-authorization-uri: http://127.0.0.1:9999/server/oauth/authorize
access-token-uri: http://127.0.0.1:9999/server/oauth/token
resource:
jwt:
key-uri: http://127.0.0.1:9999/server/oauth/token_key
user-info-uri: http://127.0.0.1:9999/server/user
token-info-uri: http://127.0.0.1:9999/server/oauth/check_token
preferTokenInfo: false
server:
port: 8080
servlet:
context-path: /client1
sso-client_b ---> resource ----> static ----> index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SSO Client1</title>
</head>
<body>
<h1>SSO Demo Client1</h1>
<a href="http://localhost:8081/client2/index.html">访问Client2</a>
</body>
</html>
6. 编写sso-client_b
package com.security.sso.client;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
@EnableOAuth2Sso // 开启单点登录
public class SsoClient2Application {
public static void main(String[] args) {
SpringApplication.run(SsoClient2Application.class, args);
}
//编写一个获取当前服务器的用户信息控制器
@GetMapping("/user")
public Authentication user(Authentication user){
return user;
}
}
security:
oauth2:
client:
clientId: demo
clientSecret: demosecrect
user-authorization-uri: http://127.0.0.1:9999/server/oauth/authorize
access-token-uri: http://127.0.0.1:9999/server/oauth/token
resource:
jwt:
key-uri: http://127.0.0.1:9999/server/oauth/token_key
user-info-uri: http://127.0.0.1:9999/server/user
token-info-uri: http://127.0.0.1:9999/server/oauth/check_token
preferTokenInfo: false
server:
port: 8081
servlet:
context-path: /client2
sso-client_b ---> resource ----> static ----> index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SSO Client2</title>
</head>
<body>
<h1>SSO Demo Client2</h1>
<a href="http://localhost:8080/client1/index.html">访问Client1</a>
</body>
</html>
7. 测试:
http://localhost:8080/client1/user
http://localhost:8081/client2/user
8. 官方文档:
https://docs.spring.io/spring-security-oauth2-boot/docs/current/reference/html5/#boot-features-security-oauth2-single-sign-on