单点登录原理
何谓单点登录? 请看下面这篇博客
首先创建我们的项目架构,不在原来的项目上进行了,便于理解分析,项目架构如下
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();
-
}
-
}
经测试我们上面的三个期望都已成功实现。
文章转载至:[https://blog.csdn.net/ahcr1026212/article/details/89846168](https://blog.csdn.net/ahcr1026212/article/details/89846168)