[原创]Oauth2.0实现SSO单点登录的CAS⽅式和相关Demo演⽰
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 (Attribute("_csrf") != null) {
model.put("_csrf", Attribute("_csrf"));
}
return new ModelAndView(new SsoSpelView(template), model);
}
protected String createTemplate(Map<String, Object> model, HttpServletRequest request) {
//解决从登录跳转到授权和应⽤之间跳转授权 form表单内action值相同导致⽆法完成授权的问题
if((ParameterMap()) 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' nam 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;}"
spring framework是什么系统
+"</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><inpu + "%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='den 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 + "%denial%</div></body></html>";
}
String template = TEMPLATE;
if (ainsKey("scopes") || Attribute("scopes") != null) {
template = place("%scopes%", createScopes(model, request)).replace("%denial%", "");
}
else {
template = place("%scopes%", "").replace("%denial%", DENIAL);
}
if (ainsKey("_csrf") || Attribute("_csrf") != null) {
template = place("%csrf%", CSRF);
}
else {
template = place("%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>) (ainsKey("scopes") ? ("scopes") : request
.getAttribute("scopes"));
for (String scope : scopes.keySet()) {
String approved = "true".(scope)) ? " checked" : "";
String denied = !"true".(scope)) ? " checked" : "";
String value = place("%scope%", scope).replace("%key%", scope).replace("%approved%", approved)
.replace("%denied%", denied);
builder.append(value);
}
builder.append("</ul>");
String();
}
private static String CSRF = "<input type='hidden' name='${_csrf.parameterName}' value='${_ken}' />";
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' nam // 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'/> // + "%denial%</div><script&ElementById('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 + "%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 t.expression.MapAccessor;
import pression.Expression;
import pression.spel.standard.SpelExpressionParser;
import pression.spel.support.StandardEvaluationContext;
import org.springframework.security.oauth2mon.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.prefix = new RandomValueStringGenerator().generate() + "{";
public String resolvePlaceholder(String name) {
Expression expression = parser.parseExpression(name);
Object value = Value(context);
return value == null ? null : String();
}
};
}
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 = place("${", prefix);
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper(prefix, "}");
String result = placePlaceholders(maskedTemplate, resolver);
result = place(prefix, "${");
response.setContentType(getContentType());
}
}
分析:SsoApprovalEndpoint这个类的来源于WhitelabelApprovalEndpoint这个类,主要⽤于单点登录是否授权进⼊⽤的,默认会有有个⽩⾊的授权页⾯出现让客户选择是否授权登录,看下源码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.dpoint;
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='${_ken}' />";
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> 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 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 = ateTemplate(model, request);
if (Attribute("_csrf") != null) {
model.put("_csrf", Attribute("_csrf"));
}
return new ModelAndView(new SpelView(template), model);
}
protected String createTemplate(Map<String, Object> model, HttpServletRequest request) {
String template = TEMPLATE;
if (!ainsKey("scopes") && Attribute("scopes") == null) {
template = place("%scopes%", "").replace("%denial%", DENIAL);
} else {
template = place("%scopes%", ateScopes(model, request)).replace("%denial%", "");
}
if (!ainsKey("_csrf") && Attribute("_csrf") == null) {
template = place("%csrf%", "");
} else {
template = place("%csrf%", CSRF);
}
return template;
}
private CharSequence createScopes(Map<String, Object> model, HttpServletRequest request) {
StringBuilder builder = new StringBuilder("<ul>");
Map<String, String> scopes = (Map)((Map)(ainsKey("scopes") ? ("scopes") : Attribute("scopes")));
Iterator var5 = scopes.keySet().iterator();
while(var5.hasNext()) {
String scope = (();
String approved = "true".(scope)) ? " checked" : "";
String denied = !"true".(scope)) ? " checked" : "";
String value = place("%scope%", scope).replace("%key%", scope).replace("%approved%", approved).replace("%denied%", denied);
builder.append(value);
}
builder.append("</ul>");
String();
}
}
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'/>< + "%denial%</div>
<script&ElementById('confirmationForm').submit()</script></body></html>";
我们⽤<div style='display:none;'>来表⽰这个页⾯是空⽩的,然后我们加上<script>的编写来⾃动提交,形成⼀个空⽩页⾯⼀闪⽽过的效果(不需要再⼿动点击授权)
遇到的坑总结:
时常配置好后报出如下错误:
***************************
APPLICATION FAILED TO START
***************************
Description:
Method springSecurityFilterChain in org.fig.figuration.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$TokenInfoServices - 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地址
(喜欢记得点星⽀持哦,谢谢!)
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论