0%

CAS教程2-自定义登录

简介

CAS提供了单点登录服务功能,但是默认只使用账号密码登录,在现实项目中往往需要添加各种验证因子比如短信和验证码等,通过对源码的跟踪和分析这边简介一下CAS登录验证逻辑的大致流程和如何定制登录代码.

前置学习条件

CAS 是使用 Spring Web Flow 和 Thymeleaf来实现登录页面的 Thymeleaf 是一个模板渲染引擎,这部分和jsp有一点点类似,只要接触过 MVC 架构这一块就没有太大问题,在跟踪代码问题中主要问题在 Spring Web Flow 需要对Spring Web Flow的基础概念有一定的了解才能大致了解源码的运行流畅和自定义登录代码,Spring Web Flow的基础概念,只要了解了基础概念定制 CAS 的登录就不会有太大问题:一下是之前写的关于 Spring Web Flow 的基础概念的文档
https://liushaohuang.cn/2020/01/17/Spring-Web-flow-%E6%A6%82%E5%BF%B5%E7%AE%80%E4%BB%8B/

找到入口Flow 入口

  • 在代码编译起来后在 overlays/org.apereo.cas.cas-server-webapp-tomcat-5.3.1/WEB-INF/webflow/login/ 目录下我们可以找到login-webflow.xml 这个文件就是配置登录的 Flow的地方

  • login-webflow.xml的初始代码如下:这里我们可以看见这里定义了一个flow 里面包含两个 action-state 和 一个 view-state 他们之间根据Flow执行的状态相互跳转

    1. initializeLoginForm : 进入这个 state 后会调用一个注入到Spring容器中的 initializeLoginAction 对象如果它返回的Event是 success的话就跳转到 viewLoginForm state

    2. viewLoginForm : 进入这个 state 后会自动跳转到 WEB-INF/templates 目录下的 casLoginView 这个页面,这里我们可以看到这个页面还绑定了一个credential 对象,字段名分别为username,password,具体在哪里绑定的看到这里我们还不懂,这部分后面再看,然后还有一个submit的事件,触发后跳转到 realSubmit state

    3. realSubmit : 这部分和 initializeLoginForm 没有太大区别,就是进入这个state后调用 authenticationViaFormAction 然后根据返回值跳转到不同的地方,但是跳转的除了 initializeLoginForm,其它的跳转在xml中一概没有标识看,这部分我们后面再看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?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>

CAS M和V的绑定

CAS 使用 Spring Web Flow 完成 MVC 的 M 和 V 的绑定
我们首先在官网上找到如何配置自定义的Flow的方法

https://apereo.github.io/cas/5.3.x/installation/Webflow-Customization-Extensions.html

我们通过在网上找到的其它文档发现 CAS 实现登录逻辑是在 DefaultLoginWebflowConfigurer类下,部分源码如下,我们根据方法名可以看到其在初始化时,好像配置了一系列的东西,比如createDefaultActionStates 方法,我们可以猜想到其又在 xml 的基础上用java代码的方式创建了很多 action state,这就解释了为什么上一步中我们在 realSubmit 下配置的跳转信息在xml中没有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DefaultLoginWebflowConfigurer extends AbstractCasWebflowConfigurer {

protected void doInitialize() {
Flow flow = this.getLoginFlow();
if (flow != null) {
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");
}

}

}

我们这里先不管具体的逻辑,我们主要找到入口,在源码中全局搜索 DefaultLoginWebflowConfigurer 我们会找到一个 CasWebflowContextConfiguration的配置类,部分源码如下,我只复制了我感兴趣的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Configuration("casWebflowContextConfiguration")
@EnableConfigurationProperties(CasConfigurationProperties.class)
@Slf4j
public class CasWebflowContextConfiguration {

@Bean
public FlowDefinitionRegistry loginFlowRegistry() {
final FlowDefinitionRegistryBuilder builder = new FlowDefinitionRegistryBuilder(this.applicationContext, builder());
builder.setBasePath(BASE_CLASSPATH_WEBFLOW);
builder.addFlowLocationPattern("/login/*-webflow.xml");
return builder.build();
}


@ConditionalOnMissingBean(name = "defaultWebflowConfigurer")
@Bean
@Order(0)
@RefreshScope
public CasWebflowConfigurer defaultWebflowConfigurer() {
final DefaultLoginWebflowConfigurer c = new DefaultLoginWebflowConfigurer(builder(), loginFlowRegistry(), applicationContext, casProperties);
c.setLogoutFlowDefinitionRegistry(logoutFlowRegistry());
c.setOrder(Ordered.HIGHEST_PRECEDENCE);
return c;
}



@ConditionalOnMissingBean(name = "casDefaultWebflowExecutionPlanConfigurer")
@Bean
public CasWebflowExecutionPlanConfigurer casDefaultWebflowExecutionPlanConfigurer() {
return new CasWebflowExecutionPlanConfigurer() {
@Override
public void configureWebflowExecutionPlan(final CasWebflowExecutionPlan plan) {
plan.registerWebflowConfigurer(defaultWebflowConfigurer());
plan.registerWebflowConfigurer(defaultLogoutWebflowConfigurer());
plan.registerWebflowConfigurer(groovyWebflowConfigurer());
}
};
}


}
  • loginFlowRegistry()

    之前关于 Spring Web Flow 的文章中提过 Flow 是通过Registry去管理的,CAS 对 Spring Web Flow进行了二次封装,这里在 Spring 中注册了Registry,并且将 /login 下的xml注册到 Registry中,进入 builder.build() 方法

    1
    2
    3
    4
    5
    6
    7
    8
    public FlowDefinitionRegistry build() {
    DefaultFlowRegistry flowRegistry = new DefaultFlowRegistry();
    flowRegistry.setParent(this.parent);
    this.registerFlowLocations(flowRegistry);
    this.registerFlowLocationPatterns(flowRegistry);
    this.registerFlowBuilders(flowRegistry);
    return flowRegistry;
    }

    我们再进入this.registerFlowLocations(flowRegistry); 这里注意一点
    this.flowResourceFactory.createResource(path, attributes, id); 方法将flow 的xml的路径当做id保存在 FlowDefinitionResource中(如 WEB-INF/webflow/login/login-webflow.xml 的id为login),这个id将会被当做 Flow 的id在 flowRegistry中注册

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private void registerFlowLocations(DefaultFlowRegistry flowRegistry) {
    Iterator var2 = this.flowLocations.iterator();

    while(var2.hasNext()) {
    FlowDefinitionRegistryBuilder.FlowLocation location = (FlowDefinitionRegistryBuilder.FlowLocation)var2.next();
    String path = location.getPath();
    String id = location.getId();
    AttributeMap<Object> attributes = location.getAttributes();
    this.updateFlowAttributes(attributes);
    FlowDefinitionResource resource = this.flowResourceFactory.createResource(path, attributes, id);
    this.registerFlow(resource, flowRegistry);
    }

    }

    我们发现 this.registerFlow(resource, flowRegistry); 很有可能是注册逻辑的代码,进入查看,发现 flowRegistry.getFlowModelRegistry().registerFlowModel(resource.getId(), flowModelHolder);这一行代码中将resource的id当做flow的id在flowRegistry中注册,至此我们在overlays下定义的 login-webflow.xml 就已经完成注册

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    private void registerFlow(FlowDefinitionResource resource, DefaultFlowRegistry flowRegistry) {

    if (resource.getPath().getFilename().endsWith(".xml")) {
    FlowModelBuilder flowModelBuilder = new XmlFlowModelBuilder(resource.getPath(), flowRegistry.getFlowModelRegistry());
    DefaultFlowModelHolder flowModelHolder = new DefaultFlowModelHolder(flowModelBuilder);
    FlowModelFlowBuilder flowBuilder = new FlowModelFlowBuilder(flowModelHolder);
    FlowBuilderContextImpl builderContext = new FlowBuilderContextImpl(resource.getId(), resource.getAttributes(), flowRegistry, this.flowBuilderServices);
    FlowAssembler assembler = new FlowAssembler(flowBuilder, builderContext);
    DefaultFlowHolder flowHolder = new DefaultFlowHolder(assembler);
    flowRegistry.getFlowModelRegistry().registerFlowModel(resource.getId(), flowModelHolder);
    flowRegistry.registerFlowDefinition(flowHolder);
    } else {
    throw new IllegalArgumentException(resource + " is not a supported resource type; supported types are [.xml]");
    }
    }
  • defaultWebflowConfigurer()

    这个方法是对之前我们在 xml 中编写的 login-webflow.xml flow做扩展,上面有一个注解@ConditionalOnMissingBean(name = “defaultWebflowConfigurer”) 代表代码执行到这里时,Spring 容器你没有 defaultWebflowConfigurer 对象的话才进行装配,而@Order(0)注解又指定了这个类装配的优先级,那么我们到这里就有思路了要实现登录的主要逻辑,我们可以在这个类进行装配前,自己实现一个defaultWebflowConfigurer,里面包含我们自己的登录逻辑,我们进入 DefaultLoginWebflowConfigurer 查看一下具体的逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
      public class DefaultLoginWebflowConfigurer extends AbstractCasWebflowConfigurer {

    protected void doInitialize() {
    Flow flow = this.getLoginFlow();
    if (flow != null) {
    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");
    }

    }

    }

    this.getLoginFlow(); 方法是从我们上一步FlowRegistry中注册的 login-webflow.xml 中取出 Flow,然后之后对这个 Flow 用代码调用API的方式手动增加了许多 state,这里就解释了我们最早看到的 login-webflow.xml 中很多跳转并没有配置 state,原因就在这里,那些跳转的 state 全部都在这里完成写入,我们重点注意一下 this.createRememberMeAuthnWebflowConfig(flow); 和 this.setStartState(flow, “initialAuthenticationRequestValidationCheck”);方法

  • this.createRememberMeAuthnWebflowConfig(flow);

    我们重点看下 else 中的逻辑,这里在 flow 中创建了一个 credential 变量并和 UsernamePasswordCredential.class 对象做绑定,我们可以重新查看一下 login-webflow.xml ,发现 viewLoginForm 标签中有一个 modal 参数就是和 credential做绑定的,这里和UsernamePasswordCredential.class对象做了绑定,而 UsernamePasswordCredential 又是实现了 Credential 接口,那么这里思路就很明确了,我们 继承 DefaultLoginWebflowConfigurer 重写 createRememberMeAuthnWebflowConfig 方法将我们自定义的 Credential对象和 login-webflow.xml 中的 credential 做绑定 ,这样我们就完成了 MVC 中的 M 和 V 的绑定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    protected void createRememberMeAuthnWebflowConfig(Flow flow) {
    if (this.casProperties.getTicket().getTgt().getRememberMe().isEnabled()) {
    this.createFlowVariable(flow, "credential", RememberMeUsernamePasswordCredential.class);
    ViewState state = (ViewState)this.getState(flow, "viewLoginForm", ViewState.class);
    BinderConfiguration cfg = this.getViewStateBinderConfiguration(state);
    cfg.addBinding(new Binding("rememberMe", (String)null, false));
    } else {
    this.createFlowVariable(flow, "credential", UsernamePasswordCredential.class);
    }

    }
  • this.setStartState(flow, “initialAuthenticationRequestValidationCheck”)

    这个方法给 login Flow 设置了一个 on-start 标签。之前我手动尝试在 login-webflow.xml 中配置一个 on-start 标签,但是发现无论怎么也无法执行,后来查看源码发现这里重新设置了 on-start 标签的内容,这里关联到 initialAuthenticationRequestValidationCheck 标签,initialAuthenticationRequestValidationCheck标签是前面 this.createDefaultActionStates(flow); 中创建的,如果有兴趣可以根据代码中的每一个Action 会发现最后其会关联到我们在 login-webflow.xml 中定义的initializeLoginForm 标签,官网文档中有提到,不要轻易手动修改 login-webflow.xml 中的标签,除非你知道他干什么,看到这里就已经基本明白了,xml定义的标签都是会和这里新增的 state 做关联的。

    自定义 CAS M和V的绑定

  • 自定义Credential

    根据上一节的思路,我们自定义绑定 M 和 V,首先我们要实现一个 Credential 对象,这个对象是用来 M 和 V 做绑定的参数,类似我们 Spring MVC Controller 层的参数,我们这里选择直接继承 UsernamePasswordCredential,因为 UsernamePasswordCredential 实现了 Credential我们在其中扩展了一个code字段.注意必须添加一个空的构造,不然绑定会发生错误

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class MyUsernamePasswordCredential extends UsernamePasswordCredential {

    public MyUsernamePasswordCredential(String username, String password, String code) {
    super(username, password);
    this.code = code;
    }

    //必须添加空参构造,不然Web Flow无法注入
    public MyUsernamePasswordCredential() {

    }

    private String code;

    public String getCode() {
    return code;
    }

    public void setCode(String code) {
    this.code = code;
    }
    }
  • 自定义 CasWebflowConfigurer

    我们这里直接继承 DefaultLoginWebflowConfigurer 重写 createRememberMeAuthnWebflowConfig 方法即可,我们将credential变量绑定给了我们自定义的 MyUsernamePasswordCredential 对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
      public class MyLoginWebflowConfiger extends DefaultLoginWebflowConfigurer {

    public MyLoginWebflowConfiger(FlowBuilderServices flowBuilderServices, FlowDefinitionRegistry flowDefinitionRegistry, ApplicationContext applicationContext, CasConfigurationProperties casProperties) {
    super(flowBuilderServices, flowDefinitionRegistry, applicationContext, casProperties);
    }
    @Override
    protected void createRememberMeAuthnWebflowConfig(Flow flow){

    if (this.casProperties.getTicket().getTgt().getRememberMe().isEnabled()) {
    this.createFlowVariable(flow, "credential", RememberMeUsernamePasswordCredential.class);
    ViewState state = (ViewState)this.getState(flow, "viewLoginForm", ViewState.class);
    BinderConfiguration cfg = this.getViewStateBinderConfiguration(state);
    cfg.addBinding(new BinderConfiguration.Binding("rememberMe", (String)null, false));
    } else {
    this.createFlowVariable(flow, "credential", MyUsernamePasswordCredential.class);
    }
    }
    }
  • 注册自定义 CasWebflowConfigurer

    我们模仿CasWebflowContextConfiguration 将我们自定义的 MyLoginWebflowConfiger注入到Spring中,注意一下我们使用@Order(-1)注解来标识我们的defaultWebflowConfigurer() 对象 在默认 的 defaultWebflowConfigurer() 对象之前注入到 Spring 中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    @EnableConfigurationProperties({CasConfigurationProperties.class})
    public class CASWebFlowConfig implements CasWebflowExecutionPlanConfigurer{

    @Autowired
    private FlowBuilderServices flowBuilderServices;

    @Autowired
    @Qualifier("loginFlowRegistry")
    private FlowDefinitionRegistry loginFlowRegistry;

    @Autowired
    @Qualifier("logoutFlowRegistry")
    FlowDefinitionRegistry logoutFlowRegistry;

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private CasConfigurationProperties casProperties;

    @Autowired
    private ServicesManager servicesManager;

    @Bean
    @Order(-1)
    public CasWebflowConfigurer defaultWebflowConfigurer() {
    MyLoginWebflowConfiger c = new MyLoginWebflowConfiger(flowBuilderServices,loginFlowRegistry , this.applicationContext, this.casProperties);
    c.setLogoutFlowDefinitionRegistry(logoutFlowRegistry);
    return c;
    }
    }

接下来是编写配置文件,使用Spring 的SPI机制将 CASWebFlowConfig注入到Spring中,因为我们编写的类并没有在 CAS工程的 Spring 的扫描路径下,所以我们使用 Spring 的 SPI 机制 来配置,在 resources 下新建 META-INF 文件夹,新建文件 spring.factories,配置注册的类全路径,将我们刚才编写的注册类

1
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.sunnada.cas.Config,com.sunnada.cas.CASWebFlowConfig

自定义View

在定制界面和webflow时候,我们需要将我们重写的资源放到我们自定义的maven resources 对应的目录下(比如我们需要重写webflow/login/login-webflow.xml 那我们就要在我们的 resources 目录下新建webflow/login/login-webflow.xml,然后再修改自己的文件内容),然后再maven中排除overlays中的文件,详细配置请看注意一节

CAS 是用 Thymeleaf 模板来实现页面渲染的,我们在 login-webflow.xml 中找到 viewLoginForm 标签发现其重定向到 casLoginView 页面,我们在 bind下新增 code 属性 这边绑定的model credential 已经在我们上面改成了 MyUsernamePasswordCredential 对象,里面有个 code 字段

1
2
3
4
5
6
7
8
<view-state id="viewLoginForm" view="casLoginView" model="credential">
<binder>
<binding property="username" required="true"/>
<binding property="password" required="true"/>
<binding property="code" required="true"/>
</binder>
<transition on="submit" bind="true" validate="true" to="realSubmit" history="invalidate"/>
</view-state>

我们在 resources/templates 目录下找到 casLoginView.html,这是一个普通的 Thymeleaf 模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">

<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>

<title th:text="#{cas.login.pagetitle}">CAS Acceptable Use Policy View</title>
<link href="../../static/css/cas.css" rel="stylesheet" th:remove="tag" />

</head>

<body class="login">
<main role="main" class="container mt-3 mb-3">
<div layout:fragment="content" class="row">
<div class="col-md">
<!-- 表单部分代码 -->
<div th:replace="fragments/loginform :: loginform"><a href="fragments/loginform.html">Login Form goes
here</a></div>
</div>
<div id="notices" class="col-md mt-3 mt-md-0">
<div th:replace="fragments/insecure :: insecure"><a href="fragments/insecure.html">insecure alert goes
here</a></div>
<div th:replace="fragments/defaultauthn :: staticAuthentication">
<a href="fragments/defaultauthn.html">defaultAuthn</a>
fragment
</div>
<div th:replace="fragments/cookies :: cookiesDisabled"><a href="fragments/cookies.html">cookies</a> fragment
</div>
<div th:replace="fragments/serviceui :: serviceUI"><a href="fragments/serviceui.html">service ui</a> fragment</div>
<div th:replace="fragments/loginProviders :: loginProviders"><a href="fragments/loginProviders.html">loginProviders</a>
fragment
</div>
<div th:replace="fragments/cas-resources-list :: cas-resource-list">
<a href="fragments/cas-resources-list.html">cas-resource</a> list fragment
</div>
</div>
</div>
</main>
</body>
</html>

代码中得知,表单部分在在 fragments/loginform目录下,我们查看那部分代码,并在下面添加一个input框 其name属性为code 用于绑定我们自定义的 MyUsernamePasswordCredential 对象,添加部分已经注释出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>

<title>Login Form Fragment</title>
<link href="../../static/css/cas.css" rel="stylesheet" th:remove="tag" />
</head>
<body>
<main role="main" class="container mt-3 mb-3">
<div class="row">
<div class="col-md">
<!-- Login form template begins here -->
<div th:fragment="loginform" class="card">
<div class="card-header text-center">
<h2 th:text="#{cas.login.pagetitle}">Login</h2>
<span class="fa-stack fa-2x hidden-xs">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-lock fa-stack-1x fa-inverse"></i>
</span>
</div>
<div class="card-body">
<form method="post" id="fm1" th:object="${credential}" action="login">
<div class="alert alert-danger" th:if="${#fields.hasErrors('*')}">
<span th:each="err : ${#fields.errors('*')}" th:utext="${err}">Example error</span>
</div>

<h3 th:utext="#{screen.welcome.instructions}">Enter your Username and Password</h3>

<section class="form-group">
<label for="username" th:utext="#{screen.welcome.label.netid}">Username</label>

<div th:if="${openIdLocalId}">
<strong>
<span th:utext="${openIdLocalId}"/>
</strong>
<input type="hidden"
id="username"
name="username"
th:value="${openIdLocalId}"/>
</div>
<div th:unless="${openIdLocalId}">
<input class="form-control required"
id="username"
size="25"
tabindex="1"
type="text"
th:disabled="${guaEnabled}"
th:field="*{username}"
th:accesskey="#{screen.welcome.label.netid.accesskey}"
autocomplete="off"/>
</div>
</section>

<section class="form-group">
<label for="password" th:utext="#{screen.welcome.label.password}">Password</label>

<div>
<input class="form-control required"
type="password"
id="password"
size="25"
tabindex="2"
th:accesskey="#{screen.welcome.label.password.accesskey}"
th:field="*{password}"
autocomplete="off"/>
<span id="capslock-on" style="display:none;">
<p>
<i class="fa fa-exclamation-circle"></i>
<span th:utext="#{screen.capslock.on}"/>
</p>
</span>
</div>
</section>

<!-- 新增code 表单代码 -->

<section class="form-group">
<label for="code">code...</label>

<div>
<input class="form-control required"
type="text"
id="code"
size="25"
name="code"
tabindex="2"
th:accesskey="#{screen.welcome.label.password.accesskey}"
autocomplete="off"/>
<span id="capslock-on" style="display:none;">
<p>
<i class="fa fa-exclamation-circle"></i>
<span th:utext="#{screen.capslock.on}"/>
</p>
</span>
</div>
</section>



<section class="form-check" th:if="${passwordManagementEnabled && param.doChangePassword != null}">
<p>
<input type="checkbox" name="doChangePassword" id="doChangePassword"
value="true" th:checked="${param.doChangePassword != null}" tabindex="4"/>
<label for="doChangePassword" th:text="#{screen.button.changePassword}">Change Password</label>
</p>
</section>

<section class="form-check" th:if="${rememberMeAuthenticationEnabled}">
<p>
<input type="checkbox" name="rememberMe" id="rememberMe" value="true" tabindex="5"/>
<label for="rememberMe" th:text="#{screen.rememberme.checkbox.title}">Remember Me</label>
</p>
</section>

<section class="row" th:if="${recaptchaSiteKey != null AND recaptchaInvisible != null AND recaptchaSiteKey AND !recaptchaInvisible}">
<div class="g-recaptcha" th:attr="data-sitekey=${recaptchaSiteKey}"/>
</section>

<input type="hidden" name="execution" th:value="${flowExecutionKey}"/>
<input type="hidden" name="_eventId" value="submit"/>
<input type="hidden" name="geolocation"/>
<input class="btn btn-block btn-submit"
th:unless="${recaptchaSiteKey != null AND recaptchaInvisible != null AND recaptchaSiteKey AND recaptchaInvisible}"
name="submit"
accesskey="l"
th:value="#{screen.welcome.button.login}"
tabindex="6"
type="submit"
value="Login3"
/>
<button class="btn btn-block btn-submit g-recaptcha"
th:if="${recaptchaSiteKey != null AND recaptchaInvisible != null AND recaptchaSiteKey AND recaptchaInvisible}"
th:attr="data-sitekey=${recaptchaSiteKey}, data-badge=${recaptchaPosition}"
data-callback="onSubmit"
name="submitBtn"
accesskey="l"
th:text="#{screen.welcome.button.login}"
tabindex="6"
/>
</form>

<form th:if="${passwordManagementEnabled}" method="post" id="passwordManagementForm">
<input type="hidden" name="execution" th:value="${flowExecutionKey}"/>
<input type="hidden" name="_eventId" value="resetPassword"/>
<span class="fa fa-unlock"></span>
<a th:utext="#{screen.pm.button.resetPassword}" href="javascript:void(0)" onclick="$('#passwordManagementForm').submit();"/>
<p/>
</form>

<div th:unless="${passwordManagementEnabled}">
<span class="fa fa-question-circle"></span>
<span th:utext="#{screen.pm.button.forgotpwd}">Forgot your password?</span>
<p/>
</div>

<script type="text/javascript" th:inline="javascript">
var i = [[#{screen.welcome.button.loginwip}]]
$( document ).ready(function() {
$("#fm1").submit(function () {
$(":submit").attr("disabled", true);
$(":submit").attr("value", i);
console.log(i);
return true;
});
});
</script>

<div th:replace="fragments/loginsidebar :: loginsidebar" />
</div>
</div>
</div>
</div>
</main>
</body>
</html>

现在我们运行CAS Service 进入登录页已经可以看见页面增加了一个code 字段,我们也完成了M和V的绑定,前端的表单对象会绑定我们后端配置的MyUsernamePasswordCredential 对象,后续我们要做的就是完成自定义认证

自定义认证

以上部分我们只是完成了 M 和 V的绑定,现在我们实现自定义认证,这边我采用 mybatis 来当做持久层来从数据库中查询数据

mybtais配置

由于刚开始实践时我直接继承 mybatis-stater 来完成mybatis,但是发现无论怎么尝试,CAS 都无法完成对 Mybtais-stater中的配置文件完成自动配置,于是我采用手动配置的方法完成mybatis的集成

  • POM文件配置
    加上 mybatis-spring 和 mybatis的包

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.0.3</version>
    </dependency>

    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.6</version>
    </dependency>
  • 配置mybatis数据源
    mybatis 的核心是SqlSessionFactory 类而其有依赖DataSource 类所以我们进行如下配置即可,如果需要使用连接池,修改DataSource中的配置即可,现在我们就可以正常使用 mybatis 的功能了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public class DataSourceConfig {

    //配置数据源
    @Bean
    public DataSource dataSource(){

    DriverManagerDataSource dataSource= new DriverManagerDataSource("jdbc:mysql://192.168.169.94:3306/mysql?serverTimezone=CTT&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false","root","root");
    dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
    return dataSource;
    }

    //配置 mybatis自动扫描 Mapper
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
    MapperScannerConfigurer mapperScannerConfigurer=new MapperScannerConfigurer();
    mapperScannerConfigurer.setSqlSessionFactoryBeanName("sqlSessionFactory");
    mapperScannerConfigurer.setBasePackage("com.sunnada");
    return mapperScannerConfigurer;
    }
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    factoryBean.setMapperLocations();
    factoryBean.setDataSource(dataSource());
    ResourcePatternResolver resourcePatternResolver=new PathMatchingResourcePatternResolver();
    //配置扫描对应路径的xml
    resourcePatternResolver.getResources("classpath*:mapper/*.xml");
    factoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath*:mapper/*.xml"));
    return factoryBean.getObject();
    }
    }
  • 自定义 CAS 认证
    CAS 认证扩展类为 AuthenticationHandler 我们实现其抽象类 AbstractPreAndPostProcessingAuthenticationHandler即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    public class CustomUsernamePasswordAuthentication extends AbstractPreAndPostProcessingAuthenticationHandler {

    private UserMapper userMapper;

    //UserMapper 为mybatis的 mapper 接口,在构造方法中手动赋值
    public CustomUsernamePasswordAuthentication(String name, ServicesManager servicesManager, PrincipalFactory principalFactory, Integer order,UserMapper userMapper) {
    super(name, servicesManager, principalFactory, order);
    this.userMapper=userMapper;
    }

    //设置只支持验证 MyUsernamePasswordCredential 类型的 Credential
    @Override
    public boolean supports(Credential credential){

    return credential instanceof MyUsernamePasswordCredential ;

    }

    @Override
    protected AuthenticationHandlerExecutionResult doAuthentication(Credential credential) throws GeneralSecurityException, PreventedException {

    MyUsernamePasswordCredential myUsernamePasswordCredential=(MyUsernamePasswordCredential)credential;

    //如果从数据库中根据用户和密码查出的用户id不为空则用户存在

    String userId=userMapper.findUserName(myUsernamePasswordCredential.getUsername()
    ,myUsernamePasswordCredential.getPassword());

    //验证code时候正确
    if(userId!= null
    && "code".equals(((MyUsernamePasswordCredential) credential).getCode())

    ){
    List<MessageDescriptor> list = new ArrayList<>();
    return createHandlerResult(credential,this.principalFactory.createPrincipal(credential.getId()),list);
    }
    //登录失败抛出异常
    throw new FailedLoginException("登录失败 ");
    }
    }
  • 在CAS 中注册 AuthenticationHandler

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class Config implements AuthenticationEventExecutionPlanConfigurer {

    @Autowired
    @Qualifier("servicesManager")
    private ServicesManager servicesManager;

    @Autowired
    private UserMapper userMapper;

    //AuthenticationHandler依赖 "dataSource","mapperScannerConfigurer","sqlSessionFactory" 这三个bean
    @Bean
    @DependsOn({"dataSource","mapperScannerConfigurer","sqlSessionFactory"})
    public AuthenticationHandler authenticationHandler(){

    return new CustomUsernamePasswordAuthentication(CustomUsernamePasswordAuthentication.class.getName(), servicesManager, new DefaultPrincipalFactory(), 1,userMapper);
    }

    @Override
    public void configureAuthenticationExecutionPlan(AuthenticationEventExecutionPlan plan) {
    plan.registerAuthenticationHandler(authenticationHandler());
    }

    }
  • 在 spring.factories 文件 中注册你需要在Spring中的配置类

1
2
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.sunnada.cas.DataSourceConfig,\
com.sunnada.cas.Config,com.sunnada.cas.CASWebFlowConfig

到此为止自定义登录就已经完成

注意点

我们在基于CAS overlay做扩展的时候,有一些jar包已经集成在war中,但是我们编写扩展类的时候也需要这部分代码,我们可以将这部分pox文件引入并设置作用域为provided,特别注意在设置 maven-war-plugin 插件时需要将 overlays中重复的配置文件排除出去

如本人依赖如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd ">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-overlay</artifactId>
<packaging>war</packaging>
<version>1.0</version>

<dependencies>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-webapp${app.server}</artifactId>
<version>${cas.version}</version>
<type>war</type>
<scope>runtime</scope>
</dependency>

<!--json服务注册-->
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-json-service-registry</artifactId>
<version>${cas.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-core-authentication</artifactId>
<version>${cas.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-core-authentication-api</artifactId>
<version>${cas.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.3</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter -->
<!--引入Spring SPI机制 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>1.5.2.RELEASE</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-core-configuration</artifactId>
<version>${cas.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-rest</artifactId>
<version>${cas.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-core-webflow-api</artifactId>
<version>${cas.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-core-webflow</artifactId>
<version>${cas.version}</version>
<scope>provided</scope>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apereo.cas/cas-server-support-actions -->
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-actions</artifactId>
<version>${cas.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.12.6</version>
<scope>provided</scope>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>com.rimerosolutions.maven.plugins</groupId>
<artifactId>wrapper-maven-plugin</artifactId>
<version>0.0.5</version>
<configuration>
<verifyDownload>true</verifyDownload>
<checksumAlgorithm>MD5</checksumAlgorithm>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${springboot.version}</version>
<configuration>
<mainClass>${mainClassName}</mainClass>
<addResources>true</addResources>
<executable>${isExecutable}</executable>
<layout>WAR</layout>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<warName>cas</warName>
<failOnMissingWebXml>false</failOnMissingWebXml>
<recompressZippedFiles>false</recompressZippedFiles>
<archive>
<compress>false</compress>
<manifestFile>${manifestFileToUse}</manifestFile>
</archive>
<overlays>
<overlay>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-webapp${app.server}</artifactId>
<!--原有的服务不再初始化进去-->
<excludes>
<exclude>WEB-INF/classes/services/*</exclude>
<exclude>WEB-INF/classes/application.*</exclude>
<exclude>WEB-INF/classes/log4j2.*</exclude>
</excludes>
</overlay>
</overlays>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
</plugin>
</plugins>
<finalName>cas</finalName>
</build>

<properties>
<cas.version>5.3.1</cas.version>
<springboot.version>1.5.18.RELEASE</springboot.version>
<!-- app.server could be -jetty, -undertow, -tomcat, or blank if you plan to provide appserver -->
<app.server>-tomcat</app.server>

<mainClassName>org.springframework.boot.loader.WarLauncher</mainClassName>
<isExecutable>false</isExecutable>
<manifestFileToUse>${project.build.directory}/war/work/org.apereo.cas/cas-server-webapp${app.server}/META-INF/MANIFEST.MF</manifestFileToUse>

<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<repositories>
<repository>
<id>sonatype-releases</id>
<url>http://oss.sonatype.org/content/repositories/releases/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
<releases>
<enabled>true</enabled>
</releases>
</repository>
<repository>
<id>sonatype-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>shibboleth-releases</id>
<url>https://build.shibboleth.net/nexus/content/repositories/releases</url>
</repository>
</repositories>

<profiles>
<profile>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<id>default</id>
<dependencies>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-webapp${app.server}</artifactId>
<version>${cas.version}</version>
<type>war</type>
<scope>runtime</scope>
</dependency>
<!--
...Additional dependencies may be placed here...
-->
</dependencies>
</profile>

<profile>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<id>exec</id>
<properties>
<mainClassName>org.apereo.cas.web.CasWebApplication</mainClassName>
<isExecutable>true</isExecutable>
<manifestFileToUse></manifestFileToUse>
</properties>
<build>
<plugins>
<plugin>
<groupId>com.soebes.maven.plugins</groupId>
<artifactId>echo-maven-plugin</artifactId>
<version>0.3.0</version>
<executions>
<execution>
<phase>prepare-package</phase>
<goals>
<goal>echo</goal>
</goals>
</execution>
</executions>
<configuration>
<echos>
<echo>Executable profile to make the generated CAS web application executable.</echo>
</echos>
</configuration>
</plugin>
</plugins>
</build>
</profile>

<profile>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<id>bootiful</id>
<properties>
<app.server>-tomcat</app.server>
<isExecutable>false</isExecutable>
</properties>
<dependencies>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-webapp${app.server}</artifactId>
<version>${cas.version}</version>
<type>war</type>
<scope>runtime</scope>
</dependency>
</dependencies>
</profile>

</profiles>
</project>
排除依赖

在定制界面和webflow时候,我们需要将我们重写的资源放到我们自定义的maven resources 对应的目录下(比如我们需要重写webflow/login/login-webflow.xml 那我们就要在我们的 resources 目录下新建webflow/login/login-webflow.xml,然后再修改自己的文件内容),然后再maven中排除overlays中的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<warName>cas</warName>
<failOnMissingWebXml>false</failOnMissingWebXml>
<recompressZippedFiles>false</recompressZippedFiles>
<archive>
<compress>false</compress>
<manifestFile>${manifestFileToUse}</manifestFile>
</archive>
<overlays>
<overlay>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-webapp${app.server}</artifactId>
<!--原有的服务不再初始化进去-->
<excludes>
<exclude>WEB-INF/classes/services/*</exclude>
<exclude>WEB-INF/classes/application.*</exclude>
<exclude>WEB-INF/classes/log4j2.*</exclude>
<exclude>WEB-INF/classes/webflow/login/*.xml</exclude>
<exclude>WEB-INF/classes/templates/fragments/loginform.html</exclude>

</excludes>
</overlay>
</overlays>
</configuration>
</plugin>