介绍
CAS 是一个企业多语言单点登录解决方案和网络身份提供商,并试图成为满足您的身份验证和授权需求的综合平台
CAS的架构
Web 层是与所有外部系统(包括 CAS 客户端)通信的端点。Web 层委托Ticketing生成 CAS 客户端访问的票证。Ticketing委托给Authentication子系统做用户验证
CAS Server的登陆认证流程
在 CAS 的整个登录过程中,有三个重要的概念:
- TGT:TGT 全称叫做 Ticket Granting Ticket,这个相当于我们平时所见到的 HttpSession 的作用,用户登录成功后,用户的基本信息,如用户名、登录有效期等信息,都将存储在此。
- TGC:TGC 全称叫做 Ticket Granting Cookie,TGC 以 Cookie 的形式保存在浏览器中,根据 TGC 可以帮助用户找到对应的 TGT,所以这个 TGC 有点类似与会话 ID。
- ST:ST 全称是 Service Ticket,这是 CAS Sever 通过 TGT 给用户发放的一张票据,用户在访问其他服务时,发现没有 Cookie 或者 ST ,那么就会 302 到 CAS Server 获取 ST,然后会携带着 ST 302 回来,CAS Client 则通过 ST 去 CAS Server 上获取用户的登录状态。
这里我们以CAS2.0协议为例,展示单点登录认证的流程:
可以用文字描述整个流程:
- 用户通过浏览器访问app1,app1发现用户没有登录,于是返回 302,指向一个 带有service 参数的CAS Server登录地址,让用户去 CAS Server 上登录。
- 浏览器自动重定向到 CAS Server 上,CAS Server 获取用户 Cookie 中携带的 TGC,去校验用户是否已经登录,如果已经登录,则完成身份校验(此时 CAS Server 可以根据用户的 TGC 找到 TGT,进而获取用户的信息);如果未登录,则重定向到 CAS Server 的登录页面,用户输入用户名/密码,CAS Server 会生成 TGT,并且根据 TGT 签发一个 ST,再将 TGC 放在用户的 Cookie 中,完成身份校验。
- CAS Server 完成身份校验之后,会将 ST 拼接在 service 中,返回 302,浏览器将首先将 TGC 存在 Cookie 中,然后根据 302 的指示,携带上 ST 重定向到app1。
- app1收到浏览器传来的 ST 之后,拿去 CAS Server 上校验,去判断用户的登录状态,如果用户登录合法,CAS Server 就会返回用户信息给 app1。
- 浏览器再去访问app2,app2发现用户未登录,重定向到 CAS Server。
- CAS Server 发现此时用户实际上已经登录了,于是又重定向回app2,同时携带上 ST。
- app2拿着 ST 去 CAS Server 上校验,获取用户的登录信息。
CAS Server的定制化开发-手机验证码登陆
由cas的架构图可看出,cas server使用了spring webflow认证流程的管理(如果不了解spring webflow,请先查阅相关博文),在老版本的cas server中,都存在一个名为login-flow.xml的流程管理文件(新版本中流程管理的配置都在代码中完成):
默认的表单登陆逻辑如图中:realSubmit,要实现手机验证码登陆我们需要仿照realSubmit,想办法添加一个“mobileSubmit”的action-state。
cas server 6.6.x 部署方式基于spring boot+gradle的overlay,
如何构建自己的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:
可以看出这里是通过 transition标签,指向的的 “realSubmit” 的acition-state,切触发这个transition的 evenId 是 “submit”,再结合fragment/loginform.html中的代码
由此我们知道这个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);
}
二是实现手机验证码登陆的表单,并在表单提交的数据中包含红框中得内容:
至此,整个手机验证码登陆的逻辑已经基本实现。 有问题,可以留言~