CAS5.3X 之配置默认Service实现

CAS5.3X 之配置默认service

  • 前言
  • CAS配置默认service
    • 静态Service配置
    • Spring Web Flow
      • Springboot 流程实现(解读源码)
      • 分析总结
      • 原因分析
  • 解决方案:

前言

今儿老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]]’

截图:CAS5.3X 之配置默认Service实现_第1张图片
我:“哟,中奖了!来来来,分析分析…”

CAS配置默认service

经过前面的对话,咱都了解了老A的问题,也去看了CAS的文档,知道了有个配置是用来定义一个默认URL,如果没有服务的情况下,CAS可以重定向到该URL。
当然,还要注意一句话:“provided in the authentication request. ”,意思是说你这个URL也不能乱写,必须要通过serviceid的正则验证才行
通过入门文档,咱都知道,在resources/services下新建的主题json文件,基本都长下面这样:

静态Service配置

{
  "@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.RegisteredService的实现类,对其他属性进行一个json反射对象,常用的有RegexRegisteredService,匹配策略为id的正则表达式
  • serviceId:唯一的服务id
  • name: 服务名称,会显示在默认登录页
  • id:全局唯一标志
  • description:服务描述,会显示在默认登录页
  • evaluationOrder:确定已注册服务的相对评估顺序。当两个服务URL表达式覆盖相同的服务时,此标志尤其重要;评估顺序决定首先评估哪个注册,并作为内部排序因素。 (越小越优先)
    除了以上说的还有很多配置策略以及节点,具体还要看官方文档,配置不同的RegisteredService也会有稍微不一样,当然咱也可以写自己的,这是后话,没啥不行的。
    按老A的描述,他这里也做了修改,长下面这样,确实把啥url都给放过了,没话说。
{
  "@class" : "org.apereo.cas.services.RegexRegisteredService",
  "serviceId" : "^(https|imaps|http)://.*",
  "name" : "Landing One",
  "id" : 1000,
  "description" : "landingone 项目访问过来,跳转到demo主题",
  "evaluationOrder" : 10,
  "theme": "landingone"
}

Spring Web Flow

咱们都知道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>

疑问来了,哎:

  1. 为啥老版本流程节点分支那么多,这里就这些呢?
  2. 为啥在初始化流程对象的时候,看到的节点数量比这个xml要多?

这就问到点子上了,大兄弟们请看好:
我们在开发过程中啊,不会只有上面这个viewLoginForm中username和password俩字段,有些复杂的场景呢,比如多加个验证码字段,多加个注册流程,再来个第三方登录(微信、支付宝、WhatsApp)等等要求,所以上面这个登录流程,咱们真的就当他是个流程就行了,扯远了。咱们下面说下CAS 5.3.1 是怎么用Springboot实现的web flow吧

Springboot 流程实现(解读源码)

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给它有了哪些实现:
CAS5.3X 之配置默认Service实现_第2张图片
实现类:
CAS5.3X 之配置默认Service实现_第3张图片

  • DefaultLoginWebflowConfigurer:登录流程初始化
  • DefaultLogoutWebflowConfigurer:登出流程初始化
  • DelegatedAuthenticationWebflowConfigurer:为pac4j集成调整初始化webflow上下文
  • GroovyWebflowConfigurer :执行Groovy脚本来自动配置webflow上下文,不多做阐述

后面两个咱们就不先不讨论了,另外因为登出贼简单,这次问题也在登录,所以咱这次就先关注登录流程。
CAS5.3X 之配置默认Service实现_第4张图片
我们可以看到,当获取到的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); 的代码:
CAS5.3X 之配置默认Service实现_第5张图片
哎?发现没有,有这么一句 createHasServiceCheckDecisionState(flow);
明显就是创建判断是否有Service服务的节点嘛,继续看:
CAS5.3X 之配置默认Service实现_第6张图片CAS5.3X 之配置默认Service实现_第7张图片

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";

CAS5.3X 之配置默认Service实现_第8张图片
哎,我们知道了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);
    }
}

你可能感兴趣的:(CAS)