Cas Server介绍及基于6.6.x版本实现手机验证码登陆

1,553 阅读6分钟

介绍

CAS 是一个企业多语言单点登录解决方案和网络身份提供商,并试图成为满足您的身份验证和授权需求的综合平台

CAS的架构

image.png

Web 层是与所有外部系统(包括 CAS 客户端)通信的端点。Web 层委托Ticketing生成 CAS 客户端访问的票证。Ticketing委托给Authentication子系统做用户验证

CAS Server的登陆认证流程

在 CAS 的整个登录过程中,有三个重要的概念:

  1. TGT:TGT 全称叫做 Ticket Granting Ticket,这个相当于我们平时所见到的 HttpSession 的作用,用户登录成功后,用户的基本信息,如用户名、登录有效期等信息,都将存储在此。
  2. TGC:TGC 全称叫做 Ticket Granting Cookie,TGC 以 Cookie 的形式保存在浏览器中,根据 TGC 可以帮助用户找到对应的 TGT,所以这个 TGC 有点类似与会话 ID。
  3. ST:ST 全称是 Service Ticket,这是 CAS Sever 通过 TGT 给用户发放的一张票据,用户在访问其他服务时,发现没有 Cookie 或者 ST ,那么就会 302 到 CAS Server 获取 ST,然后会携带着 ST 302 回来,CAS Client 则通过 ST 去 CAS Server 上获取用户的登录状态。

这里我们以CAS2.0协议为例,展示单点登录认证的流程:

image.png

可以用文字描述整个流程:

  1. 用户通过浏览器访问app1,app1发现用户没有登录,于是返回 302,指向一个 带有service 参数的CAS Server登录地址,让用户去 CAS Server 上登录。
  2. 浏览器自动重定向到 CAS Server 上,CAS Server 获取用户 Cookie 中携带的 TGC,去校验用户是否已经登录,如果已经登录,则完成身份校验(此时 CAS Server 可以根据用户的 TGC 找到 TGT,进而获取用户的信息);如果未登录,则重定向到 CAS Server 的登录页面,用户输入用户名/密码,CAS Server 会生成 TGT,并且根据 TGT 签发一个 ST,再将 TGC 放在用户的 Cookie 中,完成身份校验。
  3. CAS Server 完成身份校验之后,会将 ST 拼接在 service 中,返回 302,浏览器将首先将 TGC 存在 Cookie 中,然后根据 302 的指示,携带上 ST 重定向到app1。
  4. app1收到浏览器传来的 ST 之后,拿去 CAS Server 上校验,去判断用户的登录状态,如果用户登录合法,CAS Server 就会返回用户信息给 app1。
  5. 浏览器再去访问app2,app2发现用户未登录,重定向到 CAS Server。
  6. CAS Server 发现此时用户实际上已经登录了,于是又重定向回app2,同时携带上 ST。
  7. app2拿着 ST 去 CAS Server 上校验,获取用户的登录信息。

CAS Server的定制化开发-手机验证码登陆

由cas的架构图可看出,cas server使用了spring webflow认证流程的管理(如果不了解spring webflow,请先查阅相关博文),在老版本的cas server中,都存在一个名为login-flow.xml的流程管理文件(新版本中流程管理的配置都在代码中完成):

image.png

默认的表单登陆逻辑如图中:realSubmit,要实现手机验证码登陆我们需要仿照realSubmit,想办法添加一个“mobileSubmit”的action-state。

cas server 6.6.x 部署方式基于spring boot+gradle的overlay,

image.png 如何构建自己的overlay项目,大家可以查看相关博文

1.自定义LoginWebflowConfigurer

cas server 6.6.x的流程管理的配置是在 DefaultLoginWebflowConfigurer中进行的:

public class DefaultLoginWebflowConfigurer extends AbstractCasWebflowConfigurer {

    public DefaultLoginWebflowConfigurer(final FlowBuilderServices flowBuilderServices,
                                         final FlowDefinitionRegistry flowDefinitionRegistry,
                                         final ConfigurableApplicationContext applicationContext,
                                         final CasConfigurationProperties casProperties) {
        super(flowBuilderServices, flowDefinitionRegistry, applicationContext, casProperties);
    }
    //加载配置的入口
    @Override
    protected void doInitialize() {
        val flow = getLoginFlow();

        if (flow != null) {
            createInitialFlowActions(flow); //这里面加载了realSubmit
            createDefaultGlobalExceptionHandlers(flow);
            createDefaultEndStates(flow);
            createDefaultDecisionStates(flow);
            createDefaultActionStates(flow);
            createDefaultViewStates(flow);
            createRememberMeAuthnWebflowConfig(flow);
            setStartState(flow, CasWebflowConstants.STATE_ID_INITIAL_AUTHN_REQUEST_VALIDATION_CHECK);
        }
    }

   //省略若干代码
 

    /**
     * Create default action states.
     *
     * @param flow the flow
     */
    protected void createDefaultActionStates(final Flow flow) {
        createInitialLoginAction(flow);
        createRealSubmitAction(flow);//加载realSubmit
        createInitialAuthenticationRequestValidationCheckAction(flow);
        createCreateTicketGrantingTicketAction(flow);
        createSendTicketGrantingTicketAction(flow);
        createGenerateServiceTicketAction(flow);
        createGatewayServicesMgmtAction(flow);
        createServiceAuthorizationCheckAction(flow);
        createRedirectToServiceActionState(flow);
        createHandleAuthenticationFailureAction(flow);
        createTerminateSessionAction(flow);
        createTicketGrantingTicketCheckAction(flow);
    }


    //这个方法就是配置login-flow.xml 中的realSubmit 我们可以仿照这个方法去实现我们手机号验证码登陆的action-sate
    /**
     * Create real submit action.
     *
     * @param flow the flow
     */
    protected void createRealSubmitAction(final Flow flow) {
        val state = createActionState(flow, CasWebflowConstants.STATE_ID_REAL_SUBMIT,
            CasWebflowConstants.ACTION_ID_AUTHENTICATION_VIA_FORM_ACTION);
        createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_WARN,
            CasWebflowConstants.STATE_ID_WARN);
        createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_SUCCESS,
            CasWebflowConstants.STATE_ID_CREATE_TICKET_GRANTING_TICKET);
        createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_SUCCESS_WITH_WARNINGS,
            CasWebflowConstants.STATE_ID_SHOW_AUTHN_WARNING_MSGS);
        createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_AUTHENTICATION_FAILURE,
            CasWebflowConstants.STATE_ID_HANDLE_AUTHN_FAILURE);
        createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_ERROR,
            CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM);
        createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_TICKET_GRANTING_TICKET_VALID,
            CasWebflowConstants.STATE_ID_SERVICE_CHECK);
        createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_GENERATE_SERVICE_TICKET,
            CasWebflowConstants.STATE_ID_GENERATE_SERVICE_TICKET);
    }
    //省略若干代码
  }

看完这部分源码,你会发现我们可以通过继承DefaultLoginWebflowConfigurer,并重写createDefaultActionStates方法的方式,把我们“mobileSubmit”的action-state添加到流程中:

public class CustomLoginWebflowConfigurer extends DefaultLoginWebflowConfigurer{

    private final static String  ACTION_ID_AUTHENTICATION_MOBILE_FORM_ACTION = "mobileLoginAction";

private final static String STATE_ID_MONILE_SUBMIT = "mobileSubmit";

private final static String TRANSITION_ID_MOBILE_SUBMIT = "mobilesubmit";


    

    public CustomLoginWebflowConfigurer(FlowBuilderServices flowBuilderServices, FlowDefinitionRegistry flowDefinitionRegistry, ConfigurableApplicationContext applicationContext, CasConfigurationProperties casProperties) {
        super(flowBuilderServices, flowDefinitionRegistry, applicationContext, casProperties);
    }

    @Override
    protected void createDefaultActionStates(Flow flow){
        super.createDefaultActionStates(flow);
        createMobileSubmitAction(flow);
    }

    private void createMobileSubmitAction(Flow flow) {
    //STATE_ID_MONILE_SUBMIT action-state的id
    //ACTION_ID_AUTHENTICATION_MOBILE_FORM_ACTION是需要我们去实现的Action类,值是我们装配到bean容器中时的bean name;装配方式会在后面给出
        val state = createActionState(flow, STATE_ID_MONILE_SUBMIT,
                ACTION_ID_AUTHENTICATION_MOBILE_FORM_ACTION);
        createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_WARN,
                CasWebflowConstants.STATE_ID_WARN);
        createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_SUCCESS,
                CasWebflowConstants.STATE_ID_CREATE_TICKET_GRANTING_TICKET);
        createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_SUCCESS_WITH_WARNINGS,
                CasWebflowConstants.STATE_ID_SHOW_AUTHN_WARNING_MSGS);
        createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_AUTHENTICATION_FAILURE,
                CasWebflowConstants.STATE_ID_HANDLE_AUTHN_FAILURE);
        createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_ERROR,
                CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM);
        createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_TICKET_GRANTING_TICKET_VALID,
                CasWebflowConstants.STATE_ID_SERVICE_CHECK);
        createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_GENERATE_SERVICE_TICKET,
                CasWebflowConstants.STATE_ID_GENERATE_SERVICE_TICKET);
    }
 }

2.自定义Action

我们成功添加了自定义的action-state,下一步,我们需要创建一个action-state的事件处理类mobileLoginAction(ACTION_ID_AUTHENTICATION_MOBILE_FORM_ACTION)

public class MobileLoginAction extends AbstractNonInteractiveCredentialsAction{


    public MobileLoginAction(CasDelegatingWebflowEventResolver initialAuthenticationAttemptWebflowEventResolver, CasWebflowEventResolver serviceTicketRequestWebflowEventResolver, AdaptiveAuthenticationPolicy adaptiveAuthenticationPolicy) {
        super(initialAuthenticationAttemptWebflowEventResolver, serviceTicketRequestWebflowEventResolver, adaptiveAuthenticationPolicy);
    }

    @Override
    protected Credential constructCredentialsFromRequest(RequestContext context) {
        HttpServletRequest request = WebUtils.getHttpServletRequestFromExternalWebflowContext(context);
        return new MobileCodeLoginCredential(request.getParameter("mobileNumber"),request.getParameter("yzmCode"));
    }
}

在这个事件处理类里面,会执行用户认证逻辑,并生成认证凭证,相关调起逻辑在MobileLoginAction的父类里面,感兴趣的可以自己看下。

3.自定义Credential

自定义的Action需要我们实现的主要逻辑是创建一个Credential,它的类型决定了cas server会执行哪一个用户认证逻辑(用户名密码登陆验证还是手机验证码登陆验证),所以需要我们创建一个自定义的Credential,里面的字段在后续的用户认证逻辑中可能会用到

@Getter
@Setter
@NoArgsConstructor
public class MobileCodeLoginCredential extends AbstractCredential{

    private String mobileNumber;

    private String yzmCode;


    public MobileCodeLoginCredential(String mobileNumber, String yzmCode) {
        this.mobileNumber = mobileNumber;
        this.yzmCode = yzmCode;
    }

    @Override
    public String getId() {
        return mobileNumber;
    }
}

4.自定义AuthenticationHandler

接下来就是就是我们手机号验证码的认证逻辑了,在这里你需要校验验证码是否有效:

public class MobileLoginAuthenticationHandler extends AbstractPreAndPostProcessingAuthenticationHandler {

    private StringRedisTemplate redisTemplate;

    private LoginProperties loginProperties;

    private RestTemplate restTemplate;

    private static final String MOBILECODELOGIN = "mobilecodelogin";

    public MobileLoginAuthenticationHandler(String name, ServicesManager servicesManager, PrincipalFactory principalFactory, Integer order) {
        super(name, servicesManager, principalFactory, order);
    }

    @Override
    protected AuthenticationHandlerExecutionResult doAuthentication(Credential credential, Service service) throws AuthenticationException {
        return mobileAndCode((MobileCodeLoginCredential)credential);
    }

    //这里决定只有当Credential 类型为 MobileCodeLoginCredential时,才会使用当前
    @Override
    public boolean supports(Credential credential) {
        return credential instanceof MobileCodeLoginCredential;
    }

    @SuppressWarnings("static-access")
    protected final AuthenticationHandlerExecutionResult mobileAndCode(final MobileCodeLoginCredential credential)  {
        String mobileNumber = credential.getMobileNumber();
        String mobileCode = credential.getYzmCode();
        //从缓存中获取验证码
        ...
        //校验是否获取到验证码,且验证码是否正确,有问题抛出异常AuthenticationHandler
        ...
        
        List<MessageDescriptor> list = new ArrayList<>();
        return createHandlerResult(credential, this.principalFactory.createPrincipal(credential.getId()), list);

    }
 }

到现在,整个“mobileSubmit” 需要实现的逻辑我们已经完成了,接下来需要做的:
1. 将我们自己实现的逻辑进行配置给cas server
2. 万事俱备,只欠告诉 cas server 什么时候执行这部分逻辑

5.配置

找到项目resources/META_INF目录下的spring.factories文件,利用spring boot的扩展机制,对我们的自定实现进行配置

spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.apereo.cas.config.CasOverlayOverrideConfiguration
@AutoConfiguration
@EnableConfigurationProperties({CasConfigurationProperties.class,LoginProperties.class})
@AutoConfigureBefore(value = CasWebflowContextConfiguration.class)
public class CasOverlayOverrideConfiguration {
    @Autowired
    private LoginProperties loginProperties;

    @Autowired
    private CasConfigurationProperties casProperties;


    private final static int timeOut=6000;


    @Bean
    public Action mobileLoginAction(@Qualifier("adaptiveAuthenticationPolicy")
                                        final AdaptiveAuthenticationPolicy adaptiveAuthenticationPolicy,
                                    @Qualifier("serviceTicketRequestWebflowEventResolver")
                                        final CasWebflowEventResolver serviceTicketRequestWebflowEventResolver,
                                    @Qualifier("initialAuthenticationAttemptWebflowEventResolver")
                                        final CasDelegatingWebflowEventResolver initialAuthenticationAttemptWebflowEventResolver){

        return new MobileLoginAction(initialAuthenticationAttemptWebflowEventResolver,serviceTicketRequestWebflowEventResolver,adaptiveAuthenticationPolicy);
    }

    @Configuration("mobileidAuthenticationEventExecutionPlanConfiguration")
    public class MobileidAuthenticationEventExecutionPlanConfiguration
            implements AuthenticationEventExecutionPlanConfigurer {

        @Autowired
        @Qualifier("servicesManager")
        private ServicesManager servicesManager;
        @Resource
        private StringRedisTemplate stringRedisTemplate;
        //注册验证器
        @Bean
        public AuthenticationHandler mobileLoginAuthenticationHandler() {
            //优先验证
            MobileLoginAuthenticationHandler mobileLoginAuthenticationHandler = new MobileLoginAuthenticationHandler("mobileLoginAuthenticationHandler",
                    servicesManager, new DefaultPrincipalFactory(), 1);
            mobileLoginAuthenticationHandler.setLoginProperties(loginProperties);
            mobileLoginAuthenticationHandler.setRestTemplate(restTemplate());
            mobileLoginAuthenticationHandler.setRedisTemplate(stringRedisTemplate);
            return mobileLoginAuthenticationHandler;
        }
        @Bean
        public AuthenticationHandler passwordLoginAuthenticationHandler() {
            //优先验证
            PasswordLoginAuthenticationHandler passwordLoginAuthenticationHandler = new PasswordLoginAuthenticationHandler("passwordLoginAuthenticationHandler",
                    servicesManager, new DefaultPrincipalFactory(), 0);
            passwordLoginAuthenticationHandler.setLoginProperties(loginProperties);
            passwordLoginAuthenticationHandler.setRedisTemplate(stringRedisTemplate);
            passwordLoginAuthenticationHandler.setRestTemplate(restTemplate());
            return passwordLoginAuthenticationHandler;
        }
        @Override
        public void configureAuthenticationExecutionPlan(AuthenticationEventExecutionPlan plan) throws Exception {
            plan.registerAuthenticationHandler(mobileLoginAuthenticationHandler());
            plan.registerAuthenticationHandler(passwordLoginAuthenticationHandler());
        }
    }

    @Configuration("customLoginConfiguration")
    public class CustomLoginConfiguration implements CasWebflowExecutionPlanConfigurer {

        @Autowired
        private FlowDefinitionRegistry loginFlowRegistry;

        @Autowired
        private ConfigurableApplicationContext applicationContext;

        @Autowired
        private FlowBuilderServices flowBuilderServices;

        @Bean
        @Order(-1)
        public CasWebflowConfigurer defaultWebflowConfigurer() {
            return new CustomLoginWebflowConfigurer(flowBuilderServices,
                    loginFlowRegistry, applicationContext, casProperties);
        }
        @Override
        public void configureWebflowExecutionPlan(CasWebflowExecutionPlan plan) {
            plan.registerWebflowConfigurer(defaultWebflowConfigurer());
        }
    }


}

6.页面

同样的,页面实现我们也可以参考默认的用户名和密码登录的逻辑,我们继续看login-flow.xml中的view-state:

image.png

可以看出这里是通过 transition标签,指向的的 “realSubmit” 的acition-state,切触发这个transition的 evenId 是 “submit”,再结合fragment/loginform.html中的代码

image.png

由此我们知道这个evenId是通过页面的form表单提交过来的,也明确了接下来我们需要做的有两点: 一是在view-state 中再添加这样的一个transition

<transition on="mobilesubmit" bind="true' validate="true" to="mobileSubmit" history="invalidate" />

但是需要我们用代码添加这个配置到CustomLoginWebflowConfigurer中,覆盖DefaultLoginWebflowConfigurer中的createLoginFormView方法:

 @Override
    protected void createLoginFormView(Flow flow) {
        val propertiesToBind = Map.of(
                "username", Map.of("required", "true"),
                "password", Map.of("converter", StringToCharArrayConverter.ID),
                "source", Map.of("required", "true")
                ,"fromchannel",Map.of("required", "true")
        );
        val binder = createStateBinderConfiguration(propertiesToBind);
        casProperties.getView().getCustomLoginFormFields()
                .forEach((field, props) -> {
                    val fieldName = String.format("customFields[%s]", field);
                    binder.addBinding(new BinderConfiguration.Binding(fieldName, props.getConverter(), props.isRequired()));
                });

        val state = createViewState(flow, CasWebflowConstants.STATE_ID_VIEW_LOGIN_FORM, "login/casLoginView", binder);
        state.getRenderActionList().add(createEvaluateAction(CasWebflowConstants.ACTION_ID_RENDER_LOGIN_FORM));
        createStateModelBinding(state, CasWebflowConstants.VAR_ID_CREDENTIAL, CustomPasswordLoginCredential.class);

        val transition = createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_SUBMIT, CasWebflowConstants.STATE_ID_REAL_SUBMIT);
        val attributes = transition.getAttributes();
        attributes.put("bind", Boolean.TRUE);
        attributes.put("validate", Boolean.TRUE);
        attributes.put("history", History.INVALIDATE);
        //代码的方式添加<transition on="mobilesubmit" bind="true' validate="true" 
        //    to="mobileSubmit" history="invalidate" />
        val transition2 = createTransitionForState(state, TRANSITION_ID_MOBILE_SUBMIT, STATE_ID_MONILE_SUBMIT);
        val attributes2 = transition2.getAttributes();
        attributes2.put("bind", Boolean.TRUE);
        attributes2.put("validate", Boolean.TRUE);
        attributes2.put("history", History.INVALIDATE);
    }

二是实现手机验证码登陆的表单,并在表单提交的数据中包含红框中得内容:

image.png

至此,整个手机验证码登陆的逻辑已经基本实现。 有问题,可以留言~

参考博文:zhuanlan.zhihu.com/p/444084473