oauth2.0实现sso单点登录的方式和相关代码
SSO介绍
什么是SSO
百科:SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的机制。它是目前比较流行的企业业务整合的解决方案之一。
简单来说,SSO出现的目的在于解决同一产品体系中,多应用共享用户session的需求。SSO通过将用户登录信息映射到浏览器cookie中,解决其它应用免登获取用户session的问题。
为什么需要SSO
开放平台业务本身不需要SSO,但是如果平台的普通用户也可以在申请后成为一个应用开发者,那么就需要将平台加入到公司的整体账号体系中去,另外,对于企业级场景来说,一般都会有SSO系统,充当统一的账号校验入口。
CAS协议中概念介绍
SSO单点登录只是一个方案,而目前市面上最流行的单端登录系统是由耶鲁大学开发的CAS系统,而由其实现的CAS协议,也成为目前SSO协议中的既定协议,下文中的单点登录协议及结构,均为CAS中的体现结构
CAS协议中有以下几个概念:
1.CAS Client:需要集成单点登录的应用,称为单点登录客户端
2.CAS Server:单点登录服务器,用户登录鉴权、凭证下发及校验等操作
3.TGT:ticker granting ticket,用户凭证票据,用以标记用户凭证,用户在单点登录系统中登录一次后,再其有效期内,TGT即代表用户凭证,用户在其它client中无需再进行二次登录操作,即可共享单点登录系统中的已登录用户信息
4.ST:service ticket,服务票据,服务可以理解为客户端应用的一个业务模块,体现为客户端回调url,CAS用以进行服务权限校验,即CAS可以对接入的客户端进行管控
5.TGC:ticket granting cookie,存储用户票据的cookie,即用户登录凭证最终映射的cookies
CAS核心协议介绍

1.用户在浏览器中访问应用 2.应用发现需要索要用户信息,跳转至SSO服务器 3.SSO服务器向用户展示登录界面,用户进行登录操作,SSO服务器进行用户校验后,映射出TGC 4.SSO服务器向回调应用服务url,返回ST 5.应用去SSO服务器校验ST权限及合法性 6.SSO服务器校验成功后,返回用户信息
CAS基本流程介绍
以下为基本的CAS协议流程,图一为初次登录时的流程,图二为已进行过一次登录后的流程


以上是oauth的单点登录的流程,下面我们来看下应该如何配置单点登录:
继承了WebSecurityConfigurerAdapter的类上加@EnableOAuth2Sso注解来表示支持单点登录:
@Configuration
@EnableOAuth2Sso
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {}
另外还需要在应用中添加如下的两个类:
SsoApprovalEndpoint:
package urity.demo.sso;
import org.apache.catalina.util.ParameterMap;
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 javax.servlet.http.HttpServletRequest;
import java.util.Map;
@RestController
@SessionAttributes("authorizationRequest")
public class SsoApprovalEndpoint {
@RequestMapping("/oauth/confirm_access")
public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
String template = createTemplate(model, request);
if (request.getAttribute("_csrf") != null) {
model.put("_csrf", request.getAttribute("_csrf"));
}
return new ModelAndView(new SsoSpelView(template), model);
}
protected String createTemplate(Map<String, Object> model, HttpServletRequest request) {
//解决从登录跳转到授权 和 应用之间跳转授权 form表单内action值相同 导致无法完成授权的问题
if((request.getParameterMap()) instanceof ParameterMap){
this.DENIAL="<form id='denialForm' name='denialForm' action='${path}/oauth/authorize' method='post' style='display: inline-block;margin-left: 15px; ' ><input name='user_oauth_approval' value='false' type='hidden'/>%csrf%<label><input class='sub' name='deny' value='取消' type='submit' /></label></form>";
this.TEMPLATE="<html><head><style type='text/css'>.sub{width: 100px;background: grey;color: #fff;transition: all 1s;}.sub:hover{background-color: #FFF;color: black;transition: all 1s;transition: all 1s;}"
+"</style></head><body style='background-color: #eee;'><div style='text-align: center; margin-top: 35px;'><h1>认证授权</h1>"
+ "<p>你确定授权应用 '【${authorizationRequest.clientId}】' 登录并访问你的信息?</p>"
+ "<form id='confirmationForm' name='confirmationForm' action='${path}/oauth/authorize' method='post' style='display: inline-block;margin-right: 15px; ' ><input name='user_oauth_approval' value='true' type='hidden'/>%csrf%%scopes%<label><input class='sub' name='authorize' value='确定' type='submit' /></label></form>"
+ "%denial%</div></body></html>";
}else{
this.DENIAL="<form id='denialForm' name='denialForm' action='/oauth/authorize' method='post' style='display: inline-block;margin-left: 15px; ' ><input name='user_oauth_approval' value='false' type='hidden'/>%csrf%<label><input class='sub' name='deny' value='取消' type='submit' /></label></form>";
this.TEMPLATE="<html><head><style type='text/css'>.sub{width: 100px;background: grey;color: #fff;transition: all 1s;}.sub:hover{background-color: #FFF;color: black;transition: all 1s;}"
+"</style></head><body style='background-color: #eee;'><div style='text-align: center; margin-top: 35px;'><h1>认证授权</h1>"
+ "<p>你确定授权应用 '【${authorizationRequest.clientId}】' 登录并访问你的信息?</p>"
+ "<form id='confirmationForm' name='confirmationForm' action='/oauth/authorize' method='post' style='display: inline-block;margin-right: 15px; ' ><input name='user_oauth_approval' value='true' type='hidden'/>%csrf%%scopes%<label><input class='sub' name='authorize' value='确定' type='submit' /></label></form>"
+ "%denial%</div></body></html>";
}
String template = TEMPLATE;
if (model.containsKey("scopes") || request.getAttribute("scopes") != null) {
template = template.replace("%scopes%", createScopes(model, request)).replace("%denial%", "");
}
else {
template = template.replace("%scopes%", "").replace("%denial%", DENIAL);
}
if (model.containsKey("_csrf") || request.getAttribute("_csrf") != null) {
template = template.replace("%csrf%", CSRF);
}
else {
template = template.replace("%csrf%", "");
}
return template;
}
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" : "";
String value = SCOPE.replace("%scope%", scope).replace("%key%", scope).replace("%approved%", approved)
.replace("%denied%", denied);
builder.append(value);
}
builder.append("</ul>");
return builder.toString();
}
private static String CSRF = "<input type='hidden' name='${_csrf.parameterName}' value='${_csrf.token}' />";
private String DENIAL = "<form id='denialForm' name='denialForm' action='/oauth/authorize' method='post' style='display: inline-block;margin-left: 15px; ' ><input name='user_oauth_approval' value='false' type='hidden'/>%csrf%<label><input class='sub' name='deny' value='取消' type='submit' /></label></form>";
// private static String TEMPLATE = "<html><body><div style='display:none;'><h1>OAuth Approval</h1>"
// + "<p>Do you authorize '${authorizationRequest.clientId}' to access your protected resources?</p>"
// + "<form id='confirmationForm' name='confirmationForm' action='${path}/oauth/authorize' method='post'><input name='user_oauth_approval' value='true' type='hidden'/>%csrf%%scopes%<label><input name='authorize' value='Authorize' type='submit'/></label></form>"
// + "%denial%</div><script>document.getElementById('confirmationForm').submit()</script></body></html>";
private String TEMPLATE = "<html><head><style type='text/css'>.sub{width: 100px;background: grey;color: #fff;}.sub:hover{background-color: #FFF;color: black;}"
+"</style></head><body style='background-color: #eee;'><div style='text-align: center; margin-top: 35px;'><h1>认证授权</h1>"
+ "<p>你确定授权应用 '【${authorizationRequest.clientId}】' 登录并访问你的信息?</p>"
+ "<form id='confirmationForm' name='confirmationForm' action='/oauth/authorize' method='post' style='display: inline-block;margin-right: 15px; ' ><input name='user_oauth_approval' value='true' type='hidden'/>%csrf%%scopes%<label><input class='sub' name='authorize' value='确定' type='submit' /></label></form>"
+ "%denial%</div></body></html>";
private static String SCOPE = "<li><div class='form-group'>%scope%: <input type='radio' name='%key%'"
+ " value='true'%approved%>Approve</input> <input type='radio' name='%key%' value='false'%denied%>Deny</input></div></li>";
}
SsoSpelView:
package urity.demo.sso;
import org.springframework.context.expression.MapAccessor;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.util.PropertyPlaceholderHelper;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
public class SsoSpelView implements View {
private final String template;
private final String prefix;
private final SpelExpressionParser parser = new SpelExpressionParser();
private final StandardEvaluationContext context = new StandardEvaluationContext();
private PropertyPlaceholderHelper.PlaceholderResolver resolver;
public SsoSpelView(String template) {
this.template = template;
this.prefix = new RandomValueStringGenerator().generate() + "{";
this.context.addPropertyAccessor(new MapAccessor());
this.resolver = new PropertyPlaceholderHelper.PlaceholderResolver() {
public String resolvePlaceholder(String name) {
Expression expression = parser.parseExpression(name);
Object value = expression.getValue(context);
return value == null ? null : value.toString();
}
};
}
public String getContentType() {
return "text/html";
}
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
Map<String, Object> map = new HashMap<String, Object>(model);
String path = ServletUriComponentsBuilder.fromContextPath(request).build()
.getPath();
map.put("path", (Object) path==null ? "" : path);
context.setRootObject(map);
String maskedTemplate = template.replace("${", prefix);
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper(prefix, "}");
String result = helper.replacePlaceholders(maskedTemplate, resolver);
result = result.replace(prefix, "${");
response.setContentType(getContentType());
response.getWriter().append(result);
}
}
分析:SsoApprovalEndpoint这个类的来源于WhitelabelApprovalEndpoint这个类,主要用于单点登录是否授权进入用的,默认会有有个白色的授权页面出现让客户选择是否授权登录,看下源码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.oauth2.provider.endpoint;
import java.util.Iterator;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;
@FrameworkEndpoint
@SessionAttributes({"authorizationRequest"})
public class WhitelabelApprovalEndpoint {
private static String CSRF = "<input type='hidden' name='${_csrf.parameterName}' value='${_csrf.token}' />";
private static String DENIAL = "<form id='denialForm' name='denialForm' action='${path}/oauth/authorize' method='post'><input name='user_oauth_approval' value='false' type='hidden'/>%csrf%<label><input name='deny' value='Deny' type='submit'/></label></form>";
private static String TEMPLATE = "<html><body><h1>OAuth Approval</h1><p>Do you authorize '${authorizationRequest.clientId}' to access your protected resources?</p><form id='confirmationForm' name='confirmationForm' action='${path}/oauth/authorize' method='post'><input name='user_oauth_approval' value='true' type='hidden'/>%csrf%%scopes%<label><input name='authorize' value='Authorize' type='submit'/></label></form>%denial%</body></html>";
private static String SCOPE = "<li><div class='form-group'>%scope%: <input type='radio' name='%key%' value='true'%approved%>Approve</input> <input type='radio' name='%key%' value='false'%denied%>Deny</input></div></li>";
public WhitelabelApprovalEndpoint() {
}
@RequestMapping({"/oauth/confirm_access"})
public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
String template = this.createTemplate(model, request);
if (request.getAttribute("_csrf") != null) {
model.put("_csrf", request.getAttribute("_csrf"));
}
return new ModelAndView(new SpelView(template), model);
}
protected String createTemplate(Map<String, Object> model, HttpServletRequest request) {
String template = TEMPLATE;
if (!model.containsKey("scopes") && request.getAttribute("scopes") == null) {
template = template.replace("%scopes%", "").replace("%denial%", DENIAL);
} else {
template = template.replace("%scopes%", this.createScopes(model, request)).replace("%denial%", "");
}
if (!model.containsKey("_csrf") && request.getAttribute("_csrf") == null) {
template = template.replace("%csrf%", "");
} else {
template = template.replace("%csrf%", CSRF);
}
return template;
}
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" : "";
String value = SCOPE.replace("%scope%", scope).replace("%key%", scope).replace("%approved%", approved).replace("%denied%", denied);
builder.append(value);
}
builder.append("</ul>");
return builder.toString();
}
}
TEMPLATE这里面的网页代码字符串就是相关的授权页面,显示是否授权或者拒绝授权的页面,当我们选择授权后我们会跳转到另一个服务器的页面.
如果我们不想让它显示出来授权页面(因为这样会影响用户体验),我们可以在原始的文档中写<scripts>代码让它自动提交,如下所示:
private static String TEMPLATE = "<html><body><div style='display:none;'><h1>OAuth Approval</h1>"
+ "<p>Do you authorize '${authorizationRequest.clientId}' to access your protected resources?</p>"
+ "<form id='confirmationForm' name='confirmationForm' action='${path}/oauth/authorize' method='post'><input name='user_oauth_approval' value='true' type='hidden'/>%csrf%%scopes%<label><input name='authorize' value='Authorize' type='submit'/></label></form>"
+ "%denial%</div>
<script>document.getElementById('confirmationForm').submit()</script></body></html>";
我们用<div style='display:none;'>来表示这个页面是空白的,然后我们加上<script>的编写来自动提交,形成一个空白页面一闪而过的效果(不需要再手动点击授权)
遇到的坑总结:
时常配置好后报出如下错误:
***************************
APPLICATION FAILED TO START
***************************
Description:
Method springSecurityFilterChain in org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration required a single bean, but 4 were found:
- remoteTokenServices: defined by method 'remoteTokenServices' in class path resource [org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration$RemoteTokenServicesConfiguration$TokenInfoServicesConfiguration.class]
- consumerTokenServices: defined by method 'consumerTokenServices' in class path resource [org/springframework/security/oauth2/config/annotation/web/configuration/AuthorizationServerEndpointsConfiguration.class]
- defaultAuthorizationServerTokenServices: defined by method 'defaultAuthorizationServerTokenServices' in class path resource [org/springframework/security/oauth2/config/annotation/web/configuration/AuthorizationServerEndpointsConfiguration.class]
- defaultTokenServices: defined by method 'defaultTokenServices' in class path resource [urity/demo/oauth2/AuthorizationServerConfiguration.class]
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
这个是security找不到使用哪个类报错的问题,这里是资源类不明,所以我们在资源的相关配置上加上@Primary注解来解决:
/**
* 创建一个默认的资源服务token
*
* @return
*/
@Bean
@Primary
public ResourceServerTokenServices defaultTokenServices() {
final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenEnhancer(accessTokenConverter());
defaultTokenServices.setTokenStore(jwtStore());
return defaultTokenServices;
}
项目git地址
(喜欢记得点星支持哦,谢谢!)
https://github.com/fengcharly/springOauth-sso-CAS
oauth2.0实现sso单点登录的方式和相关代码的更多相关文章
- (十一) 整合spring cloud云架构 - SSO单点登录之OAuth2.0登录流程(2)
上一篇是站在巨人的肩膀上去研究OAuth2.0,也是为了快速帮助大家认识OAuth2.0,闲话少说,我根据框架中OAuth2.0的使用总结,画了一个简单的流程图(根据用户名+密码实现OAuth2.0的 ...
- Spring Cloud云架构 - SSO单点登录之OAuth2.0登录流程(2)
上一篇是站在巨人的肩膀上去研究OAuth2.0,也是为了快速帮助大家认识OAuth2.0,闲话少说,我根据框架中OAuth2.0的使用总结,画了一个简单的流程图(根据用户名+密码实现OAuth2.0的 ...
- Spring Cloud云架构 - SSO单点登录之OAuth2.0 根据token获取用户信息(4)
上一篇我根据框架中OAuth2.0的使用总结,画了SSO单点登录之OAuth2.0 登出流程,今天我们看一下根据用户token获取yoghurt信息的流程: /** * 根据token获取用户信息 * ...
- .NET Core2.0+MVC 用Redis/Memory+cookie实现的sso单点登录
之前发布过使用session+cookie实现的单点登录,博主个人用的很不舒服,为什么呢,博主自己测试的时候,通过修改host的方法,在本机发布了三个站点,但是,经过测试,发现,三个站点使用的sess ...
- SSO单点登录三种情况的实现方式详解
单点登录(SSO——Single Sign On)对于我们来说已经不陌生了.对于大型系统来说使用单点登录可以减少用户很多的麻烦.就拿百度来说吧,百度下面有很多的子系统——百度经验.百度知道.百度文库等 ...
- Spring Security OAuth2 SSO 单点登录
基于 Spring Security OAuth2 SSO 单点登录系统 SSO简介 单点登录(英语:Single sign-on,缩写为 SSO),又译为单一签入,一种对于许多相互关连,但是又是各自 ...
- SSO单点登录三种情况的实现方式详解(转载)
单点登录(SSO——Single Sign On)对于我们来说已经不陌生了.对于大型系统来说使用单点登录可以减少用户很多的麻烦.就拿百度来说吧,百度下面有很多的子系统——百度经验.百度知道.百度文库等 ...
- NET Core 2.0使用Cookie认证实现SSO单点登录
NET Core 2.0使用Cookie认证实现SSO单点登录 之前写了一个使用ASP.NET MVC实现SSO登录的Demo,https://github.com/bidianqing/SSO.Sa ...
- 【SpringSecurityOAuth2】源码分析@EnableOAuth2Sso在Spring Security OAuth2 SSO单点登录场景下的作用
目录 一.从Spring Security OAuth2官方文档了解@EnableOAuth2Sso作用 二.源码分析@EnableOAuth2Sso作用 @EnableOAuth2Client OA ...
随机推荐
- Tomcat 环境变量配置
1.变量和常量 i 和 0 2.环境变量 cmd >set 查看所有环境变量 %PATH% 系统指定可执行文件的搜索路径,可以是 .exe .bat String path=“C:\WINDOW ...
- 红楼梦3d游戏
1. 红楼梦大观园2d图 2. 红楼梦3d图 潇湘馆 注册机:根据电脑名和时间生成一个id,然后根据注册机生成注册码.
- pl/sql 语言设置
1.select * from v$nls_parameters 查询nls的参数,获得数据库服务器端的字符编码 NLS_LANGUAGE NLS_CHARACTERSET 2.修改本地环境变量 NL ...
- 在Sublime中集成Team Foundation Server (TFS),实现版本管理
Sublime是一款具有代码高亮.语法提示.自动完成且反应快速的编辑器软件,由于它开发的技术架构.丰富的插件,和轻盈而快速的编程响应,Sublime广受程序员的爱好.在C, C++, Javascri ...
- 《Are your lights on?》读后感
楔子(看过某类小说的孩纸对此应该不陌生...): <你的灯亮着吗?>讲了些什么?它为我们总结了解决问题的一般方法?不,它只是建议我们遇到问题后应该怎么做(绝对不等于解决问题的方法).这些建 ...
- C#内存释放(垃圾回收)
今天写了个很小的程序,程序的功能仅仅是截图,但是如果长时间开启并截图的时候,程序会变的很大,从刚开始的运行在任务管理器中只有十几K大小,运行一段时间后在任务管理器中看到程序可以达到1G或2G甚至更大: ...
- Dalsa线扫相机SDK下载和安装
1.首先去官方网站下载SDK Support Downloads - Teledyne DALSA http://www.teledynedalsa.com/imaging/support/downl ...
- nodejs vinyl-fs 处理文件时输入问题
使用 nodejs vinyl-fs 复制文件时输出路径不对,还是会有原来的相对路径,原因是用了反斜杠“\”,正斜杠“/”没问题 测试过程 node版本: v9.3.0 系统:win10 步骤: 得到 ...
- 在循环中使用鼠标悬停时表示当前悬停选中,传入this关键字即可
在前端循环中使用鼠标悬停事件 <div class="message-widget contact-widget"> <!-- Message --> {% ...
- ASP.NET Core 2 学习笔记(七)路由
ASP.NET Core通过路由(Routing)设定,将定义的URL规则找到相对应行为:当使用者Request的URL满足特定规则条件时,则自动对应到相符合的行为处理.从ASP.NET就已经存在的架 ...