百科:SSO英文全称Single Sign On,单点登陆。SSO是在多个应用系统中,用户只须要登陆一次就能够访问全部相互信任的应用系统。它包括能够将此次主要的登陆映射到其余应用中用于同一个用户的登陆的机制。它是目前比较流行的企业业务整合的解决方案之一。css
简单来讲,SSO出现的目的在于解决同一产品体系中,多应用共享用户session的需求。SSO经过将用户登陆信息映射到浏览器cookie中,解决其它应用免登获取用户session的问题。html
开放平台业务自己不须要SSO,可是若是平台的普通用户也能够在申请后成为一个应用开发者,那么就须要将平台加入到公司的总体帐号体系中去,另外,对于企业级场景来讲,通常都会有SSO系统,充当统一的帐号校验入口。java
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,即用户登陆凭证最终映射的cookiesgit
1.用户在浏览器中访问应用 2.应用发现须要索要用户信息,跳转至SSO服务器 3.SSO服务器向用户展现登陆界面,用户进行登陆操做,SSO服务器进行用户校验后,映射出TGC 4.SSO服务器向回调应用服务url,返回ST 5.应用去SSO服务器校验ST权限及合法性 6.SSO服务器校验成功后,返回用户信息github
如下为基本的CAS协议流程,图一为初次登陆时的流程,图二为已进行过一次登陆后的流程web
以上是oauth的单点登陆的流程,下面咱们来看下应该如何配置单点登陆:spring
继承了WebSecurityConfigurerAdapter的类上加@EnableOAuth2Sso注解来表示支持单点登陆:express
@Configuration @EnableOAuth2Sso public class SecurityConfiguration extends WebSecurityConfigurerAdapter {}
另外还须要在应用中添加以下的两个类:apache
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; }
(喜欢记得点星支持哦,谢谢!)