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单点登录的方式和相关代码的更多相关文章

  1. (十一) 整合spring cloud云架构 - SSO单点登录之OAuth2.0登录流程(2)

    上一篇是站在巨人的肩膀上去研究OAuth2.0,也是为了快速帮助大家认识OAuth2.0,闲话少说,我根据框架中OAuth2.0的使用总结,画了一个简单的流程图(根据用户名+密码实现OAuth2.0的 ...

  2. Spring Cloud云架构 - SSO单点登录之OAuth2.0登录流程(2)

    上一篇是站在巨人的肩膀上去研究OAuth2.0,也是为了快速帮助大家认识OAuth2.0,闲话少说,我根据框架中OAuth2.0的使用总结,画了一个简单的流程图(根据用户名+密码实现OAuth2.0的 ...

  3. Spring Cloud云架构 - SSO单点登录之OAuth2.0 根据token获取用户信息(4)

    上一篇我根据框架中OAuth2.0的使用总结,画了SSO单点登录之OAuth2.0 登出流程,今天我们看一下根据用户token获取yoghurt信息的流程: /** * 根据token获取用户信息 * ...

  4. .NET Core2.0+MVC 用Redis/Memory+cookie实现的sso单点登录

    之前发布过使用session+cookie实现的单点登录,博主个人用的很不舒服,为什么呢,博主自己测试的时候,通过修改host的方法,在本机发布了三个站点,但是,经过测试,发现,三个站点使用的sess ...

  5. SSO单点登录三种情况的实现方式详解

    单点登录(SSO——Single Sign On)对于我们来说已经不陌生了.对于大型系统来说使用单点登录可以减少用户很多的麻烦.就拿百度来说吧,百度下面有很多的子系统——百度经验.百度知道.百度文库等 ...

  6. Spring Security OAuth2 SSO 单点登录

    基于 Spring Security OAuth2 SSO 单点登录系统 SSO简介 单点登录(英语:Single sign-on,缩写为 SSO),又译为单一签入,一种对于许多相互关连,但是又是各自 ...

  7. SSO单点登录三种情况的实现方式详解(转载)

    单点登录(SSO——Single Sign On)对于我们来说已经不陌生了.对于大型系统来说使用单点登录可以减少用户很多的麻烦.就拿百度来说吧,百度下面有很多的子系统——百度经验.百度知道.百度文库等 ...

  8. NET Core 2.0使用Cookie认证实现SSO单点登录

    NET Core 2.0使用Cookie认证实现SSO单点登录 之前写了一个使用ASP.NET MVC实现SSO登录的Demo,https://github.com/bidianqing/SSO.Sa ...

  9. 【SpringSecurityOAuth2】源码分析@EnableOAuth2Sso在Spring Security OAuth2 SSO单点登录场景下的作用

    目录 一.从Spring Security OAuth2官方文档了解@EnableOAuth2Sso作用 二.源码分析@EnableOAuth2Sso作用 @EnableOAuth2Client OA ...

随机推荐

  1. 20155327 2016-2017-4 《Java程序设计》第6周学习总结

    20155327 2016-2017-4 <Java程序设计>第6周学习总结 教材学习内容总结 理解流与IO 流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象.即数据在两 ...

  2. 11) 生成可执行jar文件 maven-shade-plugin

    搜索 site:maven.apache.org maven-assembly-plugin http://maven.apache.org/plugins/maven-assembly-plugin ...

  3. This problem will occur when running in 64 bit mode with the 32 bit Oracle client components installed(在64位模式下运行安装了32位的Oracle客户端组件时,会发生此问题)

    部署win服务时出现下面的问题: 在事件查看器中看到如下错误: 日志名称: Application来源: ***调度服务日期: 2014/5/21 12:53:21事件 ID: 0任务类别: 无级别: ...

  4. UT源码+019

    设计三角形问题的程序 输入三个整数a.b.c,分别作为三角形的三条边,现通过程序判断由三条边构成的三角形的类型为等边三角形.等腰三角形.一般三角形(特殊的还有直角三角形),以及不构成三角形.(等腰直角 ...

  5. 集合(五)不正确地使用HashMap引发死循环及元素丢失

    前一篇文章讲解了HashMap的实现原理,讲到了HashMap不是线程安全的.那么HashMap在多线程环境下又会有什么问题呢? 几个月前,公司项目的一个模块在线上运行的时候出现了死循环,死循环的代码 ...

  6. dubbo在eclipse中无法读取到dubbo.xsd

    报错信息如下: Multiple annotations found at this line:– cvc-complex-type.2.4.c: The matching wildcard is s ...

  7. windows下redis安装及应用

    一.下载安装Redis(windows版本) 1.下载地址:https://github.com/MicrosoftArchive/redis/releases 2.安装: 1)打开运行窗口,输出cm ...

  8. 深入学习c++--左值引用和右值引用

    #include <iostream> #include <string> #include <vector> using namespace std; int m ...

  9. 835. Image Overlap

    Two images A and B are given, represented as binary, square matrices of the same size.  (A binary ma ...

  10. python django 更改模型字段出错时的一个解决办法

    python/django 框架自带的 orm 无疑是django框架最拿得出手的一个亮点,orm无疑极大的方便了项目的开发,提高了开发的效率. 在实际的项目开发过程中,我们有时候需要修改模型的字段, ...