单点登录原理
何谓单点登录? 请看下面这篇博客
首先创建我们的项目架构,不在原来的项目上进行了,便于理解分析,项目架构如下
tiger-sso 是pom项目,同时也是tiger-auth的子模块
SSO认证服务器
项目pom文件
<?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>tiger-sso</artifactId>
<groupId>com.rui.tiger</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.rui.tiger</groupId>
<artifactId>sso-server</artifactId>
<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</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
</dependency>
</dependencies>
</project>
配置相关
package com.rui.tiger.sso.server;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
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;
/**
* @author CaiRui
* @Date 2019-05-03 14:29
*/
@Configuration
@EnableAuthorizationServer
public class SsoAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client1")
.secret("client1s")
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.redirectUris(
"http://localhost:8080/client1/login")
.and()
.withClient("client2")
.secret("client2s")
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.redirectUris(
"http://localhost:8060/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.passwordEncoder(NoOpPasswordEncoder.getInstance());
security.tokenKeyAccess("isAuthenticated()");
}
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("tiger");//秘钥
return converter;
}
}
修改成默认basic登录
package com.rui.tiger.sso.server;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @author CaiRui
* @Date 2019-05-03 14:41
*/
@Configuration
public class SsoServerSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//security5+ 认证默认为表单了也就是http.formLogin()
http.httpBasic();
}
}
服务启动类及配置文件
package com.rui.tiger.sso.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.test.context.junit4.SpringRunner;
/**
* @author CaiRui
* @Date 2019-05-03 14:45
*/
@SpringBootApplication
public class SsoServerApplication {
public static void main(String[] args) {
SpringApplication.run(SsoServerApplication.class,args);
}
}
application.yml
server:
port: 9999
servlet:
context-path: /server
spring:
security:
user:
password: 123456
SSO客户端
client1和client2配置
客户端1和客户端2配置基本都一样,只是端口号和项目路径不一样,可自行配置
pom文件依赖
<?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>tiger-sso</artifactId>
<groupId>com.rui.tiger</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.rui.tiger</groupId>
<artifactId>sso-client1</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- @EnableOAuth2Sso 单点登录注解 -->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
</dependency>
</dependencies>
</project>
服务启动类
package com.rui.tiger.sso.client1;
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;
/**
* @author CaiRui
* @date 2019-05-05 14:03
*/
@SpringBootApplication
@EnableOAuth2Sso
@RestController
public class SsoClient1Application {
public static void main(String[] args) {
SpringApplication.run(SsoClient1Application.class, args);
}
//编写一个获取当前服务器的用户信息控制器
@GetMapping("/user")
public Authentication user(Authentication user){
return user;
}
}
application.yml配置文件
security:
oauth2:
client:
clientId: client1
clientSecret: client1s
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
添加项目首页文件 /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:8060/client2/index.html">访问Client2</a>
</body>
</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. 不是自定义的用户
期望:可以自定义用户
认证服务器自定义登录界面和用户
自定义用户认证
package com.rui.tiger.sso.server.service;
import lombok.extern.slf4j.Slf4j;
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;
/**
* @author CaiRui
* @date 2019-05-05 15:42
*/
@Component
@Slf4j
public class SsoUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String password = passwordEncoder.encode("123456");
//TODO 这里实际是查询数据库获取
log.info("用户名 {},数据库密码{}", username, password);
User admin = new User(username,
// "{noop}123456",
password,
true, true, true, true,
AuthorityUtils.commaSeparatedStringToAuthorityList(""));
return admin;
}
}
表单权限配置
package com.rui.tiger.sso.server;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author CaiRui
* @Date 2019-05-03 14:41
*/
@Configuration
public class SsoServerSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// .httpBasic()
.formLogin() // 更改为form表单登录
.and()
// 所有的请求都必须授权后才能访问
.authorizeRequests()
.anyRequest()
.authenticated();
;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
}
自动授权
通过授权界面关键字OAuth Approval全局搜索定位到如下方法:
org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint#createTemplate
改下此段逻辑,隐藏自动授权和自动提交表单,主要是加注释的地方进行修改。
package com.rui.tiger.sso.server;
import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint;
import org.springframework.security.web.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.Map;
/**
* 自动授权修改
* 授权确认服务:不能继承 WhitelabelApprovalEndpoint,因为FrameworkEndpoint会被扫描,就会存在两个一样的地址;报错
*
* @author CaiRui
* @date 2019-05-05 15:59
* @see WhitelabelApprovalEndpoint
*/
@RestController
@SessionAttributes("authorizationRequest")
public class MyWhitelabelApprovalEndpoint {
@RequestMapping("/oauth/confirm_access")
public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
final String approvalContent = 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(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();
// 让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) (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(createScopes(model, request));
builder.append(authorizeInputTemplate);
} else {
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>");
}
// 添加自动提交操作
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>");
@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" : "";
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();
}
}
经测试我们上面的三个期望都已成功实现。