今儿老A问我 :“怎么在没有service的情况下,给CAS配置默认的service,跳转到固定的url啊?”
我:“额,文档不是写了嘛?在springboot的配置文件application.properties里有个配置属性,就下边这个”
官网文档链接:
https://apereo.github.io/cas/5.0.x/installation/Configuration-Properties.html
# Defines a default URL to which CAS may redirect if there is no service
# provided in the authentication request.
cas.view.defaultRedirectUrl=https://www.github.com
过了一会儿…
老A:“哎,不对呀,你过来看看,这怎么还提示我CAS无法使用
呢,页面也没跳转呀”
错误提示:
Error: Exception thrown executing org.apereo.cas.web.flow.login.GenericSuccessViewAction@50118e2e in state ‘viewGenericLoginSuccess’ of flow ‘login’ – action execution attributes were ‘map[[empty]]’
经过前面的对话,咱都了解了老A的问题,也去看了CAS的文档,知道了有个配置是用来定义一个默认URL,如果没有服务的情况下,CAS可以重定向到该URL。
当然,还要注意一句话:“provided in the authentication request. ”,意思是说你这个URL也不能乱写,必须要通过serviceid的正则验证才行
通过入门文档,咱都知道,在resources/services下新建的主题json文件,基本都长下面这样:
{
"@class": "org.apereo.cas.services.RegexRegisteredService",
"serviceId": "^(http)://localhost.*",
"name": "本地服务",
"id": 100001,
"description": "这是一个本地允许的服务,通过localhost访问都允许通过",
"evaluationOrder": 1
}
注意:
json文件名字规则为 n a m e − {name}- name−{id}.json, id必须为json文件内容id一致
json文件解释:
{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "^(https|imaps|http)://.*",
"name" : "Landing One",
"id" : 1000,
"description" : "landingone 项目访问过来,跳转到demo主题",
"evaluationOrder" : 10,
"theme": "landingone"
}
咱们都知道CAS目前是集成了Spring Web Flow,在CAS中Spring Web Flow的配置文件最初始的就有login-webflow.xml面主要配置了登录的流程。这个如果用图来表示的话那应该是一个状态图,一些节点会有一些判断然后会有不同的分支。
不知道大伙有没有看过历史版本哈,那玩意真的是,节点挺多的,一眼望不到底,还得翻两页。
哎,咱们用的这个CAS 5.3.1 版本,贼简单,就长下面这样:
<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/webflow"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow.xsd">
<action-state id="initializeLoginForm">
<evaluate expression="initializeLoginAction" />
<transition on="success" to="viewLoginForm"/>
</action-state>
<view-state id="viewLoginForm" view="casLoginView" model="credential">
<binder>
<binding property="username" required="true"/>
<binding property="password" required="true"/>
</binder>
<transition on="submit" bind="true" validate="true" to="realSubmit" history="invalidate"/>
</view-state>
<action-state id="realSubmit">
<evaluate expression="authenticationViaFormAction"/>
<transition on="warn" to="warn"/>
<transition on="success" to="createTicketGrantingTicket"/>
<transition on="successWithWarnings" to="showAuthenticationWarningMessages"/>
<transition on="authenticationFailure" to="handleAuthenticationFailure"/>
<transition on="error" to="initializeLoginForm"/>
</action-state>
</flow>
疑问来了,哎:
这就问到点子上了,大兄弟们请看好:
我们在开发过程中啊,不会只有上面这个viewLoginForm中username和password俩字段,有些复杂的场景呢,比如多加个验证码字段,多加个注册流程,再来个第三方登录(微信、支付宝、WhatsApp)等等要求,所以上面这个登录流程,咱们真的就当他是个流程就行了,扯远了。咱们下面说下CAS 5.3.1 是怎么用Springboot实现的web flow吧
1.首先来看CAS的流程管理器 org.apereo.cas.web.flow.CasWebflowConfigurer
这是一个接口,它定义了CAS的核心流程,登录和登出。后面还有一堆的节点创建定义,咱们就先不说了,自个儿去琢磨吧。
public interface CasWebflowConfigurer extends Ordered {
String FLOW_ID_LOGIN = "login";
String FLOW_ID_LOGOUT = "logout";
void initialize();
Flow getLoginFlow();
Flow getLogoutFlow();
...
此接口的实现类 org.apereo.cas.web.flow.configurer.AbstractCasWebflowConfigurer
看名字就知道,这家伙是抽象的,肯定还有抽象方法等着小弟去实现。那咱们重点关注下它的抽象方法,再看看CAS给它有了哪些实现:
实现类:
后面两个咱们就不先不讨论了,另外因为登出贼简单,这次问题也在登录,所以咱这次就先关注登录流程。
我们可以看到,当获取到的flow对象不为null时,给flow初始化了以下这些配置,
(干了啥呢?你们自己点进去看,请允许我在这儿偷个懒,下面给你们解释)
key | Value |
---|---|
createInitialFlowActions(flow); | 创建初始流程,点进去看就知道,这里给flow对象的startActionList添加了initialFlowSetupAction的实现 |
createDefaultGlobalExceptionHandlers(flow); | 创建默认的全局异常处理器 |
createDefaultEndStates(flow); | 这个有意思,开发的大兄弟把注解写错了。这里就是定义流程的各类结束节点 |
createDefaultDecisionStates(flow); | 创建默认的决策节点 |
createDefaultActionStates(flow); | 创建默认的Action节点 |
createDefaultViewStates(flow); | 创建默认的视图节点 |
createRememberMeAuthnWebflowConfig(flow); | 创建记住我流程节点 |
最后设置开始节点:
setStartState(flow,CasWebflowConstants.STATE_ID_INITIAL_AUTHN_REQUEST_VALIDATION_CHECK);| Column 2
哎,看到这里,相信大家一定都知道怎么去找老A问题的原因了,话不多说,咱们继续分析:
老A的问题是登录成功后未重定向到配置的url:cas.view.defaultRedirectUrl=https://www.baidu.com
那经过前面的分析,我们了解到登录流程的初始化过程,那么到底哪段代码的问题呢?不卖关子了,那就是创建默认的决策节点的问题呗。
下面是createDefaultDecisionStates(flow); 的代码:
哎?发现没有,有这么一句 createHasServiceCheckDecisionState(flow);
明显就是创建判断是否有Service服务的节点嘛,继续看:
String STATE_ID_HAS_SERVICE_CHECK = "hasServiceCheck";
String STATE_ID_VIEW_GENERIC_LOGIN_SUCCESS = "viewGenericLoginSuccess";
看到这里,大家都很明白了,当节点hasServiceCheck判断没有Service 服务时,将会跳到下个节点viewGenericLoginSuccess。
那么viewGenericLoginSuccess从命名上来看,我们就知道查看登录是否成功,若成功则继续下个节点,那我们来看:viewGenericLoginSuccess的下个节点是
String VIEW_ID_GENERIC_SUCCESS = "casGenericSuccessView";
哎,我们知道了casGenericSuccessView 是个登录成功视图节点,我们都知道,视图节点的特点是会有对应的视图页面,那我们来找找看:
那我们登录完成后,会跳转到一个CAS登录成功的视图页面,不就是这个html嘛。
CAS通过springboot集成Spring Web Flow,从而定义流程管理器CasWebflowConfigurer实现了各类流程节点的初始化。
使用 cas.view.defaultRedirectUrl=https://www.baidu.com 配置默认的service,在默认不调整流程情况下,会出现重复重定向的错误。
导致了上述老A的问题发生。
在流程初始化时,将 createGenericLoginSuccessEndState(flow);的节点移除,
在application.properties中配置属性cas.view.defaultRedirectUrl=https://www.baidu.com
即可实现默认service的跳转,以下为方法重写:
public class BaseConfigurer extends DefaultLoginWebflowConfigurer {
public BaseConfigurer(FlowBuilderServices flowBuilderServices, FlowDefinitionRegistry flowDefinitionRegistry, ApplicationContext applicationContext, CasConfigurationProperties casProperties) {
super(flowBuilderServices, flowDefinitionRegistry, applicationContext, casProperties);
}
/**
* 绑定输入信息
*
* @param flow
*/
protected void bindCredential(final Flow flow, String stateid) {
this.createFlowVariable(flow, "credential", UsernamePasswordExtensionCredential.class);
ViewState state = (ViewState) this.getState(flow, stateid, ViewState.class);
BinderConfiguration cfg = this.getViewStateBinderConfiguration(state);
cfg.addBinding(new BinderConfiguration.Binding("verifycode", (String) null, false));
cfg.addBinding(new BinderConfiguration.Binding("verifytype", (String) null, false));
cfg.addBinding(new BinderConfiguration.Binding("submittype", (String) null, false));
cfg.addBinding(new BinderConfiguration.Binding("twicepassword", (String) null, false));
}
protected void createDefaultGlobalExceptionHandlers(final Flow flow, String stateId) {
TransitionExecutingFlowExecutionExceptionHandler h = new TransitionExecutingFlowExecutionExceptionHandler();
h.add(UnauthorizedSsoServiceException.class, stateId);
h.add(NoSuchFlowExecutionException.class, "viewServiceErrorView");
h.add(UnauthorizedServiceException.class, "serviceUnauthorizedCheck");
h.add(UnauthorizedServiceForPrincipalException.class, "serviceUnauthorizedCheck");
flow.getExceptionHandlerSet().add(h);
}
protected void createRememberMeAuthnWebflowConfig(final Flow flow, String stateId) {
if (this.casProperties.getTicket().getTgt().getRememberMe().isEnabled()) {
this.createFlowVariable(flow, "credential", RememberMeUsernamePasswordCredential.class);
ViewState state = (ViewState) this.getState(flow, stateId, ViewState.class);
BinderConfiguration cfg = this.getViewStateBinderConfiguration(state);
cfg.addBinding(new BinderConfiguration.Binding("rememberMe", (String) null, false));
} else {
this.createFlowVariable(flow, "credential", UsernamePasswordCredential.class);
}
}
}
public class LoginWebflowConfigurer extends BaseConfigurer {
public LoginWebflowConfigurer(FlowBuilderServices flowBuilderServices, FlowDefinitionRegistry loginFlowDefinitionRegistry, ApplicationContext applicationContext, CasConfigurationProperties casProperties) {
super(flowBuilderServices, loginFlowDefinitionRegistry, applicationContext, casProperties);
}
@Override
protected void doInitialize() {
final Flow flow = getLoginFlow();
if (flow != null) {
this.bindCredential(flow, "viewLoginForm");
this.createInitialFlowActions(flow);
this.createDefaultGlobalExceptionHandlers(flow);
this.createDefaultEndStates(flow);
this.createDefaultDecisionStates(flow);
this.createDefaultActionStates(flow);
this.createDefaultViewStates(flow);
this.createRememberMeAuthnWebflowConfig(flow);
this.setStartState(flow, "initialAuthenticationRequestValidationCheck");
}
}
@Override
protected void createDefaultEndStates(final Flow flow) {
createRedirectUnauthorizedServiceUrlEndState(flow);
createServiceErrorEndState(flow);
createRedirectEndState(flow);
createPostEndState(flow);
createInjectHeadersActionState(flow);
createServiceWarningViewState(flow);
createEndWebflowEndState(flow);
}
/**
* Create service error end state.
*
* @param flow the flow
*/
private void createServiceErrorEndState(final Flow flow) {
createEndState(flow, CasWebflowConstants.STATE_ID_VIEW_SERVICE_ERROR, CasWebflowConstants.VIEW_ID_SERVICE_ERROR);
}
}