CAS在windows AD下实现企业微信自动扫码登陆的总结
基础名词
- CAS(Central Authentication Service),可以看作中心授权服务器
- SSO(Single Sign On),单点登录
SSO相关知识
- 同域SSO(blog.212490197.xyz和www.212490197.xyz)
- 同域SSO(blog.212490197.xyz和www.212490197.xyz)
- SSO单点登录主要用于一套ID,密码登陆一套或多套系统。就像登陆支付宝之后,在登陆淘宝可以自动登陆,不需要再进行账号密码的输入。
- 下边的图显示了SSO登陆流程与注销流程
- 用户与SSO认证中心建立的会话叫做全局会话。
- 用户与各个子系统建立的会话叫做局部会话。创建局部会话之后,用户与子系统之间交流受保护数据将不再需要使用认证中心认证。
- 局部会话与全局会话之间的关系
- 局部会话存在,全局会话存在(因为局部会话依赖于全局会话的认证)
- 全局会话存在,局部会话不一定存在(如果用户只是登陆了,但是没有使用任何一个子系统,则不存在局部会话)
- 全局会话销毁,局部会话必须销毁(总认证失效,局部认证不能存在)
- 在一个子系统中进行了注销,相当于在认证中心注销,会同时让所有的子系统进行注销。所以其他子系统在之后进行数据请求的时候发现已经注销,将不会再传输数据,直接跳到登陆界面。所有子系统都使用同一个令牌。
CAS相关知识
- CAS结构主要分为CAS Server和CAS Client
- CAS可以看作是一个票据协议。
- TGC(Ticket-Granting cookie) 授权的票据证明。由CAS Server通过SSL发送给终端用户。当作Key值
- TGT(Ticket-Granting Ticket) 用户的登陆票据(CAS签发),存储在TGCcookie中的代表用户的SSO会话。可用来判断用户是否在CAS登陆过。
- ST(Service Ticket) 访问某一服务的票据,作为参数以GET方式放到URL中
- PGT(Proxy Granting Ticket) 代理凭据,
- PGTIOU(Proxy Granting Ticket I Owe You) 附加票据
- PT(Proxy Ticket) 用于C/S结构,没有cookie的情况下。
- ST是TGT签发的
- PGT是ST签发的
- PT是PGT签发的
- KDC(Key Distribution Center) **发放中心
- Authentication Service(AS) 认证服务,索取Crendential,发放TGT
- Ticket-Granting Service(TGS) 票据授权服务,索取TGT,发放ST
- 具体名词的实现过程可以参考该链接
- 图片引用出处,可以在这里查看一下具体的流程解释。
LDAP与Windows AD常识
具体过程
- 进入CAS客户端,判断用户是否登陆。
- 登陆的话直接显示客户端相关内容,没有登陆则进行CAS客户端跳转到CAS服务端
- CAS服务端已经编写好相关登陆界面,同时拥有企业微信二维码。
- 企业微信扫码,获取权限。经过OAuth2授权后获得用户信息,可以读取到用户唯一ID
- 根据用户唯一ID,自动登陆后台,获取TGC。
- 获取相应的TGT,通过TGT获取ST,将ST返回CAS客户端。
- CAS客户端拿ST去CAS服务端判断是否正确。正确则进行登陆
使用模板
使用工具
JDK11
- JDK配置PATH的时候,环境变量要放到最开始,以免被Oracle的默认JDK顶掉
Tomcat9
- HTTPS的配置
- 运行加入了JDBC的cas.war出现了ACTION: AUTHENTICATION_FAILED
MySQL5.7
Gradle
- windows和linux可以用同一个下载包
- 下载好之后,环境变量中
- GRADLE_HOME放gradle的根目录
- GRADLE_USER_HOME放gradle的根目录的.gradle。这样就不会将东西下载到C盘了
- 代理配置
- 修改项目的gradle.properties文件(参考博客)
方法一
org.gradle.jvmargs=-Xmx1536m -DsocksProxyHost=127.0.0.1 -DsocksProxyPort=1080
方法二
systemProp.http.proxyHost=127.0.0.1
systemProp.http.proxyPort=1080
systemProp.https.proxyHost=127.0.0.1
systemProp.https.proxyPort=1080
Maven
- maven在idea的配置
- 下载好之后,环境变量中
- M2_HOME放maven的根目录
- 根目录的conf/settings.xml修改默认仓库路径如下
<localRepository>D:\\software\\Maven\\repository</localRepository>
- 代理配置
- 在settings.xml进行如下配置(参考该博客)
- proxies中可以配置多个proxy,但是默认第一个proxy生效。
- active中的TRUE表示该代理目前生效状态
- http协议、主机地址、端口不在赘述。
- 用户名密码按需配置即可。
- nonProxyHost表示不需要代理访问的地址。中间的竖线分隔多个地址,此处可以使用星号作为通配符号。
- 在settings.xml进行如下配置(参考该博客)
<proxies>
<proxy>
<id>ss</id>
<active>true</active>
<protocol>http</protocol>
<!-- <protocol>socks</protocol> -->
<!-- <username>代理账号</username> -->
<!-- <password>代理密码</password> -->
<host>127.0.0.1</host>
<port>1087</port>
<!-- <nonProxyHosts>local.net|some.host.com</nonProxyHosts> -->
</proxy>
</proxies>
虚拟机相关设置
我在笔记本上装了虚拟机来部署linux服务器。版本使用的是CentOS-7-x86_64-Minimal-2003.iso,由于网易的其他单独的版本都被删掉了,readme上写着得去官网下载。所以我就直接下载了这个。参考地址是这里。
虚拟机里边的linux装好之后,进行xshell的连接,这里主要参考了这篇文章。我在配置CAS的时候,测试了CAS5.x和CAS6.x的版本,用了两个不同的虚拟机,我原来想使用同样的IP来进行访问。但是发现当两个机器都被挂起的时候,好像这样就不能用了,所以一台的子网IP配置了192.168.241.3,另一台配置了192.168.241.4.(只要最后的数字不同即可)
# ip配置
子网IP 192.168.241.3 # 或者改成192.168.241.4
子网掩码 255.255.255.0
网关IP 192.168.241.2
可以使用xshell之后,虚拟机就没什么用了。开启之后放在那里就行。接下来进行CentOS下载源的配置,这里使用阿里的下载源。配置方法参考这里。因为安装的minimal中没有wget这个包,所以我直接将链接内容下载下来,用notepad++打开后,粘贴到xshell中的。保存后,进行yum makecache即可进行其他软件的下载。我就是因为子网IP配置成同样的了,这里老是yum makecache不成功。所以记得不同机器IP配置为不同即可
IP相同,有一个最大的问题就是ping www.baidu.com
会提示找不到。
另外,会有情况是虚拟机设置好了静态IP,xshell仍旧无法连接,可以参考这个文章。勾选其中的一个东西就可以了。
设置好之后,用yum来安装一些软件
yum install -y wget lrzsz net-tools # 使用-y会自动选择安装的包,可以参考这里 https://blog.csdn.net/aiynmimi/article/details/76819961
运行的时候需要开启一些端口,这里可以参考这篇文章。
这里列出一些会用到的命令
firewall-cmd --zone=public --add-port=80/tcp --permanent # 开端口命令
firewall-cmd --zone=public --remove-port=80/tcp --permanent # 关端口命令
systemctl restart firewalld.service # 重启防火墙
firewall-cmd --list-ports # 查看开启的所有端口
主要流程
- 1 下载CAS-Overlay-template。因为CAS6.x已经使用Gradle来进行构建了,所以下边的操作需要Gradle的支持
- 2 下载好之后,可以在项目的根目录打开build.gradle文件进行配置(Gradle的配置文件,下载源,相关依赖包都在这里配置)。
- 如果不熟悉Gradle脚本可以先在这里进行相关的了解学习。
- 以下进行注释的地方都是修改过的地方。其他地方均使用原脚本内容。
buildscript {
repositories {
// 从本地maven仓库进行依赖的查找
mavenLocal()
gradlePluginPortal()
// 配置阿里源的仓库,这样对于依赖的下载会更加快
maven {
url 'http://maven.aliyun.com/nexus/content/groups/public/'
}
maven {
url 'http://maven.aliyun.com/nexus/content/repositories/jcenter'
}
mavenCentral()
jcenter()
maven {
url "https://repo.spring.io/libs-milestone"
mavenContent { releasesOnly() }
}
maven {
url "https://repo.spring.io/libs-snapshot"
mavenContent { snapshotsOnly() }
}
maven {
url "https://plugins.gradle.org/m2/"
mavenContent { releasesOnly() }
}
}
dependencies {
classpath "de.undercouch:gradle-download-task:${project.gradleDownloadTaskVersion}"
classpath "org.springframework.boot:spring-boot-gradle-plugin:${project.springBootVersion}"
classpath "gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:${project.jibVersion}"
classpath "io.freefair.gradle:maven-plugin:${project.gradleMavenPluginVersion}"
classpath "io.freefair.gradle:lombok-plugin:${project.gradleLombokPluginVersion}"
}
}
// 为所有的项目添加阿里源
allprojects {
repositories {
maven {
url 'http://maven.aliyun.com/nexus/content/groups/public/'
}
maven {
url 'http://maven.aliyun.com/nexus/content/repositories/jcenter'
}
}
}
// 添加相关依赖项
dependencies {
// Other CAS dependencies/modules may be listed here...
//Service配置
implementation "org.apereo.cas:cas-server-support-json-service-registry:${casServerVersion}"
// 使CAS服务端支持LDAP模式的查询。后边需要在application.properties中进行相关配置
implementation "org.apereo.cas:cas-server-support-ldap:${project.'cas.version'}"
// 用于获取TGT和ST。引入之后就可以通过v1/tickets获取这两个参数了,不需要其他配置。
compile "org.apereo.cas:cas-server-support-rest:${project.'cas.version'}"
// 自定义认证需要引入的两个依赖包
implementation "org.apereo.cas:cas-server-core-authentication-api:${project.'cas.version'}"
implementation "org.apereo.cas:cas-server-core-configuration-api:${project.'cas.version'}"
// 阿里的JSON处理依赖包。用于返回给前台JSON数据时进行数据组装
compile group: 'com.alibaba', name: 'fastjson', version: '1.2.47'
// HTTPClient相关依赖包,用来进行后边HTTPS相关请求的处理
compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'
compile group: 'commons-logging', name: 'commons-logging', version: '1.2'
compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.9'
compile group: 'org.apache.httpcomponents', name: 'httpcore', version: '4.4.13'
// 用于与后台数据库交互的JDBC
compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.16'
}
- 3 在当前根目录下打开命令行,然后输入以下命令,进行一次项目构建。
.\gradlew.bat clean build
- 4 构建好之后,在当前目录下执行以下命令进行war包解
.\gradlew.bat explodeWar
- 5 在根目录的src/main文件夹下创建java文件夹和resources文件夹
- 6 刚刚解压之后的文件都在根目录的build/cas-resources文件夹下,把该文件夹下的所有文件都复制到src/main/resources文件夹下。
- 这里解释下src/main文件夹的各自作用
java 用于存放自己写的一些类文件
resources 存放CAS中的配置文件(application.properties等),HTML网页模板,CSS,JS等静态资源,一些登陆认证方式(services文件夹中的文件)
webapp 可以操作最内层的web.xml,用于为自己定义的Java类中的Servlet进行路径标识
- 7 进入src/main/resources/services文件夹,删掉原来的json文件,然后在里边创建文件web-10000001.json。
- 这个文件的作用主要是在用户登录的时候进行认证使用的
- 下边为代码内容。
- 这里的name为刚刚创建json文件的中划线前边的名字,id为刚刚创建json文件时的中划线后边的数字。这里需要对应,否则会出错。
- theme是我自定义导入的主题,用来覆盖CAS默认自带的主题。CAS默认自带的HTML模板在src/main/resources/templates,css,JS的文件在src/main/resources/static中
{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "^(https|imaps|http)://.*",
"name" : "web",
"id" : 10000001,
"evaluationOrder" : 10,
"theme": "mytheme"
}
- 8 在src/main/resources文件夹下创建刚刚mytheme主题的配置文件mytheme.properties
- 在CAS内部是使用ThymeLeaf进行前后端连接的,所以创建这个配置文件也是为了配合该插件
- 在写HTML界面的时候可以直接使用ThymeLeaf的语法进行相关静态资源的读取。
- 这里我使用了我比较常用的LayUI作为前后端的连接。
# 有了这个配置文件,样式会自动从static下的该文件中进行读取
mytheme.javascript.file=/themes/mytheme/layui/layui.js
mytheme.standard.css.file=/themes/mytheme/layui/css/layui.css
mytheme.login.images.path=/themes/mytheme/images
# 默认cas的样式
cas.standard.css.file=/css/cas.css
cas.javascript.file=/js/cas.js
cas.admin.css.file=/css/admin.css
- 9 在src/main/resources/templates文件夹下创建mytheme文件夹,用于存放自己的要覆盖的CAS中的HTML模板。这里我只创建了casLoginView.html这一个登陆模板。
- 这里具体的样式可以自己写。
- 因为在实际运行的时候src/main/resources/static的目录相当于静态资源的根目录,所以我们的静态资源可以都放到这个下边。
- 在该文件夹下创建一个themes文件夹,再创建mytheme文件夹,用于存放mytheme相关的所有静态文件。
- 之后我在当前目录创建了相关的静态资源文件夹
# themes/mytheme文件夹下的目录结构
css 存放CSS文件
images 存放图像文件
js 存放图像文件
layui 存放layui组件
- 以下为casLoginView.html的详细代码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>测试使用</title>
<!--使用ThymeLeaf的语法引入相关静态资源-->
<link rel="stylesheet" th:href="@{${#themes.code('mytheme.standard.css.file')}}" />
<script th:src="@{${#themes.code('mytheme.javascript.file')}}"></script>
<!--使引入企业微信二维码构造JS-->
<script src="https://rescdn.qqmail.com/node/ww/wwopenmng/js/sso/wwLogin-1.0.0.js"></script>
</head>
<body>
<div class="layui-container">
<div class="layui-row" style="margin:4em 0;">
<div class="layui-col-md8 layui-col-md-offset1">
<!--直接从themes文件夹下进行静态文件的导入-->
<img src="themes/mytheme/images/19.png" style="height:80px" />
</div>
</div>
<div class="layui-row" style="background-image:url(themes/mytheme/images/bg-loginimg.png);height:320px;">
<!--该div用于企业微信二维码的构造-->
<div class="layui-col-md4">
<div id="wx_reg"></div>
</div>
<div class="layui-col-md-offset2 layui-col-md5" style="background-color:rgba(255,255,255,0.7);padding:2em;margin-top:4em;">
<!--这里直接引入了Anumbrella文章中的表单模式-->
<form method="post" th:object="${credential}">
<div class="layui-form-item">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block" th:unless="${openIdLocalId}">
<input class="required layui-input" id="username" size="25" tabindex="1" placeholder="用户名" type="text" th:disabled="${guaEnabled}" th:field="*{username}" th:accesskey="#{screen.welcome.label.netid.accesskey}" autocomplete="off" />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">密码</label>
<div class="layui-input-inline">
<input class="required layui-input" type="password" id="password" size="25" tabindex="2" placeholder="密码" th:accesskey="#{screen.welcome.label.password.accesskey}" th:field="*{password}" autocomplete="off" />
</div>
<div class="layui-form-mid layui-word-aux">密码为6-16位</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<input type="hidden" name="execution" th:value="${flowExecutionKey}" />
<input type="hidden" name="_eventId" value="submit" />
<input type="hidden" name="geolocation" />
<input class="btn btn-submit btn-block btn_login layui-btn" name="submit" accesskey="l" th:value="#{screen.welcome.button.login}" tabindex="6" type="submit" />
</div>
</div>
</form>
</div>
</div>
</div>
</body>
<script src="themes/mytheme/js/casLoginView.js"></script>
</html>
- 10 接下来在src/main/resources/application.properties文件夹下进行整个项目的一些配置。以下仅列出修改或增加的部分
###
# 修改部分
#
# 对ThymeLeaf项目进行热部署,否则每次修改前端都需要重启tomcat
spring.thymeleaf.cache=false
###
# 添加部分
#
# 注释掉这两行,以下两行是写死了登陆账号密码,后期进行LDAP连接,这里就不需要了
cas.authn.accept.users=casuser::Mellon
cas.authn.accept.name=Static Credentials
# 因为数据是使用Windows AD进行存储的,所以这里使用Windows AD的相关配置.openLDAP与该配置不同。需要注意
cas.authn.ldap[0].order=0
cas.authn.ldap[0].name=Active Directory
cas.authn.ldap[0].type=AD
# LDAP服务地址,如果支持SSL,地址为 ldaps://127.0.0.1:389
cas.authn.ldap[0].ldapUrl=ldap://x.x.x.x:389
# 是否使用SSL
cas.authn.ldap[0].useStartTls=false
cas.authn.ldap[0].connectTimeout=5000
# LDAP中基础DN
cas.authn.ldap[0].baseDn=dc=xx,dc=xx,dc=xx
cas.authn.ldap[0].poolPassivator=NONE
cas.authn.ldap[0].validatePeriod=180
cas.authn.ldap[0].searchFilter=sAMAccountName={user}
cas.authn.ldap[0].subtreeSearch=true
cas.authn.ldap[0].dnFormat=%aaa@qq.com
cas.authn.ldap[0]aaa@qq.com
cas.authn.ldap[0].bindCredential=xxx
cas.authn.ldap[0].principalAttributeList=cn,objectCategory,accountExpires,distinguishedName
#defaultAttributesToRelease 定义了 principalAttributeList 中的属性可以发放,即提供客户端获取**
cas.authn.attributeRepository.defaultAttributesToRelease=cn,objectCategory,accountExpires,distinguishedName
##
# Service Registry(服务注册)
#
# 开启识别Json文件,默认false
cas.serviceRegistry.initFromJson=true
#自动扫描服务配置,默认开启
cas.serviceRegistry.watcherEnabled=true
#120秒扫描一遍
cas.serviceRegistry.schedule.repeatInterval=120000
#延迟15秒开启
# cas.serviceRegistry.schedule.startDelay=15000
# Json配置,这里读取的是src/main/resources/services/文件夹下的配置文件
cas.serviceRegistry.json.location=classpath:/services
# 默认主题配置
cas.theme.defaultThemeName=mytheme
# ST使用次数和过期时间
cas.ticket.st.numberOfUses=1
cas.ticket.st.timeToKillInSeconds=100
- 11 现在进行src/main/java中相关类的编写。可以先思考一下相关实现流程
- CAS服务端先加载刚刚的casLoginView.html,同时将企业微信二维码进行显示
- 手机企业微信进行扫码操作。
- 扫码之后,企业微信的API会根据在企业微信管理员后台创建的APP上配置的域名进行域名跳转,这个时候会在跳转链接中附带一个code
- casLoginView.html引入的JS判断有code,获取该code,发送到后台的某个Servlet进行access_token的获取
- 根据code和access_token进行用户信息的获取,然后获得用户的唯一ID
- windows AD无法查询用户密码,故而这里可以使用用户的唯一ID和自己设置好的密码进行登陆。这里需要进行自定义认证登陆
- 在自定义认证登陆的过程中,需要进行模拟登陆的提交,直到最后获取ST
- 将ST返回到前台,前台进行跳转操作。
- 登陆成功
casLoginView.html获取到code后以ajax的GET方法请求后台VerifyUserId.java的ST(只粘贴部分代码)
需要在src/main最内层的web.xml进行该Servlet的标识,否则访问不到。
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
/* 该类在刚刚gradle.build中的commons-lang3依赖中 */
import org.apache.commons.lang3.StringUtils;
/* 在未进行自定义认证登陆时使用。定义之后不再使用 */
import CasDataBaseOperation;
/* 进行TGC,TGT,ST等参数的操作 */
import CasParamsOperation;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
public class VerifyUserId extends HttpServlet {
/* 获取用户信息的企业微信URL */
private static final String USERINFO_REQUEST_URL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?";
/* 登陆页。登录端地址进行了URL编码。如果多客户端,可以通过参数传入,再连接到service后边 */
private static final String LOGIN_PAGE = "https://服务端地址/login?service=https%3a%2f%2f客户端地址%3a9443%2fsample%2f";
/* 客户端的URL链接 */
private static final String TAGET_URL = "https://客户端地址/sample/";
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException{
resp.setCharacterEncoding("UTF-8");
resp.setContentType("application/json;charset=UTF-8");
JSONObject json = new JSONObject();
PrintWriter write = resp.getWriter();
try {
if (req.getParameter("code") != null) {
/* 获取企业微信登陆后的code */
String code = req.getParameter("code");
/* 获取access_token。该参数需要存储到数据库,频繁获取会被微信禁止暂时获取 */
String accessToken = CasDataBaseOperation.getAccessToken();
/* 获取访问用户的身份信息 */
String userInfoStr = CasParamsOperation.httpsRequest(USERINFO_REQUEST_URL + "access_token=" + accessToken + "&code=" + code, "GET", null);
Map userInfoMap = (Map)JSON.parse(userInfoStr);
int errcode = Integer.parseInt(userInfoMap.get("errcode").toString());
if (errcode == 0) {
if(userInfoMap.get("UserId").toString() == null) {
/* 非企业人员,返回的是OpenId */
json.put("code", 3);
json.put("msg", "not in the company");
} else {
/* 获取企业人员的UserId */
String userId = userInfoMap.get("UserId").toString();
/* 设置一个固定的自定义认证登陆密码 */
String userPass = "1*-6qq,aw451";
/* 从login页获取execution参数,这个是必须要使用的参数 */
String execution = CasParamsOperation.getExecution(CasParamsOperation.httpsRequest(LOGIN_PAGE, "GET", null));
/* 获取TGC,写入response。如果返回true,表示写入成功 */
if (CasParamsOperation.putTGC(userId, userPass, execution, resp)) {
/* 获取TGT */
String tgt = CasParamsOperation.getTGT(userId, userPass);
if (StringUtils.isNotBlank(tgt)) {
/* 获取ST */
String ticket = CasParamsOperation.getST(tgt,TAGET_URL);
if(StringUtils.isNotBlank(ticket)){
json.put("code", 0);
json.put("msg", "success");
json.put("data", "ticket=" + ticket); /* 将ST返回前台 */
}else{
resp.sendRedirect("/login");
}
} else {
resp.sendRedirect("/login");
}
}
/* 这里是最开始没有进行自定义认证登陆时,让用户填写密码存入数据库的流程
可以不使用该流程,这样太麻烦了。 */
/*
String userPass = CasDataBaseOperation.getPasswordOfUser(userId);
if (userPass.equals("no_this_user")) {
// 没有在数据库中查到该用户,需要进行密码绑定
json.put("code", 4);
json.put("msg", "not this person");
HttpSession session = req.getSession(true);
session.setAttribute("userId", userId);
} else {
// 从login页获取execution参数
String execution = CasParamsOperation.getExecution(CasParamsOperation.httpsRequest(LOGIN_PAGE, "GET", null));
// 获取TGC,写入response。如果返回true,表示写入成功,返回false,表示登陆的时候失败。需要用户更新密码
if (!CasParamsOperation.putTGC(userId, userPass, execution, resp)) {
// 用户修改过密码,需要用户重新绑定密码
json.put("code", 5);
json.put("msg", "get new password");
HttpSession session = req.getSession(true);
session.setAttribute("userId", userId);
} else {
// 获取TGT
String tgt = CasParamsOperation.getTGT(userId, userPass);
if (StringUtils.isNotBlank(tgt)) {
// 获取ST
String ticket = CasParamsOperation.getST(tgt,TAGET_URL);
if(StringUtils.isNotBlank(ticket)){
json.put("code", 0);
json.put("msg", "success");
json.put("data", "ticket=" + ticket); // 将ST返回前台
}else{
resp.sendRedirect("/login");
}
} else {
resp.sendRedirect("/login");
}
}
}
*/
}
} else if(errcode == 40029) {
/* code过期 */
json.put("code", 2);
json.put("msg", "invalid code");
}
} else {
json.put("code", 1);
json.put("msg", "wrong param");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
write.print(json);
write.flush();
write.close();
}
resp.getWriter().print(json);
}
}
CasParamsOperation类,进行参数的操作
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
/* httpclient引入的相关类 */
import org.apache.http.Consts;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.cookie.Cookie;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.apache.commons.lang3.StringUtils;
public class CasParamsOperation {
/* 定义从相应的HTML中分离出execution参数的正则 */
private static final String EXECUTION_REGEX = "\"execution\" value\\=\"|\" \\/\\>\\<input type\\=\"hidden\" name\\=\"\\_event";
/* 用于获取TGT和ST的URL */
private static final String GET_TOKEN_URL = "https://服务端地址/v1/tickets";
/* 用于进行登陆操作 */
private static final String GET_TOKEN_URL_TGC = "https://服务端地址/login";
/**
* 处理https GET/POST请求
* @param String requestURL 求的URL
* @param String requestMethod 求方式
* @param String outputStr POST或GET的数据
* @return 返回响应的页面信息
*/
/*
这里主要是为了在访问的时候忽略HTTPS证书的障碍。但是后边在获取ST和TGT的时候会出问题。
所以需要申请一个服务端和客户端的域名的证书,否则可能获取不到TGT和ST
*/
public static String httpsRequest(String requestURL, String requestMethod, String outputStr) {
StringBuffer buffer = null;
HttpsURLConnection conn = null;
OutputStream os = null;
InputStream is = null;
InputStreamReader isr = null;
BufferedReader br = null;
try {
/* 创建SSLContext */
SSLContext sslContext = SSLContext.getInstance("SSL");
TrustManager[] tm = {new DoHttpRequests()};
/* 初始化 */
sslContext.init(null, tm, new java.security.SecureRandom());
/* 获取SSLSocketFactory对象 */
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestURL);
conn = (HttpsURLConnection)url.openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
conn.setRequestMethod(requestMethod);
/* 设置当前实例使用的SSLSocketFactory */
conn.setSSLSocketFactory(ssf);
conn.connect();
/* 向接口端写内容 */
if (outputStr != null) {
OutputStream os = conn.getOutputStream();
os.write(outputStr.getBytes("utf-8"));
}
/* 读取服务器端返回的内容 */
is = conn.getInputStream();
isr = new InputStreamReader(is, "utf-8");
br = new BufferedReader(isr);
buffer = new StringBuffer();
String line = null;
while((line = br.readLine()) != null) {
buffer.append(line);
}
} catch(Exception e) {
e.printStackTrace();
} finally {
try {
if (br != null)
br.close();
if (isr != null)
isr.close();
if (is != null)
is.close();
if (os != null)
os.close();
if (conn != null)
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
return buffer.toString();
}
/**
* 获取login页中的execution参数
* @param String htmlString 传入response的login页html数据
* @return String 返回页面中execution参数的值
*/
public static String getExecution(String htmlString) {
String[] dataArr = htmlString.split(EXECUTION_REGEX);
return dataArr[1];
}
/**
* 获取TGC,并将TGC插入到response中
* @param String username 用户名
* @param String password 用户密码
* @param String execution 登陆CAS服务端时页面的execution参数的值
* @param HttpServletResponse responses 访问页面的response对象
* @return boolean true表示获取TGT正确,false表示登陆失败
*/
/* 这里我试过用普通的http请求方式,总是会出错,所以使用了httpclient的依赖进行的操作 */
public static boolean putTGC(String username, String password, String execution, HttpServletResponse responses)
throws ClientProtocolException, IOException {
CloseableHttpClient httpClient = null;
CloseableHttpResponse response = null;
try {
CookieStore cookieStore = new BasicCookieStore();
SSLConnectionSocketFactory scsf = new SSLConnectionSocketFactory(
SSLContexts.custom().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build(),
NoopHostnameVerifier.INSTANCE);
httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).setSSLSocketFactory(scsf).build();
HttpPost httpPost = new HttpPost(GET_TOKEN_URL_TGC);
List<NameValuePair> nvps = new ArrayList<NameValuePair>();
nvps.add(new BasicNameValuePair("username", username));
nvps.add(new BasicNameValuePair("password", password));
nvps.add(new BasicNameValuePair("execution", execution));
nvps.add(new BasicNameValuePair("_eventId", "submit"));
nvps.add(new BasicNameValuePair("geolocation", ""));
HttpEntity reqEntity = new UrlEncodedFormEntity(nvps, Consts.UTF_8);
httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
httpPost.setEntity(reqEntity);
response = httpClient.execute(httpPost);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 401) {
return false;
}
List<Cookie> cookies = cookieStore.getCookies();
if (null != cookies && cookies.size() > 0) {
javax.servlet.http.Cookie cookie = new javax.servlet.http.Cookie(cookies.get(0).getName(),
cookies.get(0).getValue());
cookie.setPath(cookies.get(0).getPath());
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setMaxAge(1800);
responses.addCookie(cookie);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (response != null)
response.close();
if (httpClient != null)
httpClient.close();
}
return true;
}
/**
* 获取TGT
* @param String username 用户名
* @param String password 用户密码
* @return String tgt 返回TGT的字符串数据
*/
public static String getTGT(String username, String password) throws ClientProtocolException, IOException {
String tgt = "";
CloseableHttpClient httpClient = null;
CloseableHttpResponse response = null;
try {
CookieStore cookieStore = new BasicCookieStore();
SSLConnectionSocketFactory scsf = new SSLConnectionSocketFactory(
SSLContexts.custom().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build(),
NoopHostnameVerifier.INSTANCE);
httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).setSSLSocketFactory(scsf).build();
HttpPost httpPost = new HttpPost(GET_TOKEN_URL);
List<NameValuePair> nvps = new ArrayList<NameValuePair>();
nvps.add(new BasicNameValuePair("username", username));
nvps.add(new BasicNameValuePair("password", password));
HttpEntity reqEntity = new UrlEncodedFormEntity(nvps, Consts.UTF_8);
httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
httpPost.setEntity(reqEntity);
CloseableHttpResponse response = httpClient.execute(httpPost);
try {
Header[] tgtHead = response.getAllHeaders();
if (tgtHead != null) {
for (int i = 0; i < tgtHead.length; i++) {
if (StringUtils.equals(tgtHead[i].getName(), "Location")) {
tgt = tgtHead[i].getValue().substring(tgtHead[i].getValue().lastIndexOf("/") + 1);
}
}
}
HttpEntity respEntity = response.getEntity();
EntityUtils.consume(respEntity);
} catch (Exception e) {
e.printStackTrace();
} finally {
response.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (response != null)
response.close();
if (httpClient != null)
httpClient.close();
}
return tgt;
}
/**
* 获取ST
* @param String tgt TGT字符串参数
* @param String TAGET_URL 请求获得ST的URL
* @return String serviceTicket 返回ST的字符串数据
*/
public static String getST(String tgt, String TAGET_URL) {
String serviceTicket = "";
BufferedReader in = null;
OutputStreamWriter out = null;
BufferedWriter wirter = null;
HttpsURLConnection conn = null;
try {
URL url = new URL(GET_TOKEN_URL + "/" + tgt);
conn = (HttpsURLConnection) url.openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
String param = "service=" + URLEncoder.encode(TAGET_URL, "utf-8");
out = new OutputStreamWriter(conn.getOutputStream());
wirter = new BufferedWriter(out);
wirter.write(param);
wirter.flush();
wirter.close();
out.close();
in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = "";
while ((line = in.readLine()) != null) {
serviceTicket = line;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (in != null)
in.close();
if (wirter != null)
wirter.close();
if (out != null)
out.close();
if (conn != null)
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
return serviceTicket;
}
}
DoHttpRequests类,自定义的信任管理器
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;
/**
* 自定义信任管理器
*/
public class DoHttpRequests implements X509TrustManager {
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
- 11 在当前目录中再创建两个文件夹,authentication和config,这里进行自定义认证登陆的配置
authentication中的CustomerHandlerAuthentication类
import org.apereo.cas.authentication.*;
import org.apereo.cas.authentication.credential.UsernamePasswordCredential;
import org.apereo.cas.authentication.handler.support.AbstractPreAndPostProcessingAuthenticationHandler;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.services.ServicesManager;
import javax.security.auth.login.FailedLoginException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CustomerHandlerAuthentication extends AbstractPreAndPostProcessingAuthenticationHandler {
public CustomerHandlerAuthentication(String name, ServicesManager servicesManager, PrincipalFactory principalFactory, Integer order) {
super(name, servicesManager, principalFactory, order);
}
@Override
public boolean supports(Credential credential) {
/* 判断传递过来的Credential 是否是自己能处理的类型 */
return credential instanceof UsernamePasswordCredential;
}
@Override
protected AuthenticationHandlerExecutionResult doAuthentication(Credential credential) throws GeneralSecurityException, PreventedException {
UsernamePasswordCredential usernamePasswordCredentia = (UsernamePasswordCredential) credential;
/* 这里获取了用户名密码,因为密码是自己固定的,所以只要在这里判断一下,然后返回成功信息即可 */
String username = usernamePasswordCredentia.getUsername();
String password = usernamePasswordCredentia.getPassword();
if ("1*-6qq,aw451".equals(password)) {
final List<MessageDescriptor> list = new ArrayList<>();
return createHandlerResult(usernamePasswordCredentia,
this.principalFactory.createPrincipal(username, Collections.emptyMap()), list);
} else {
throw new FailedLoginException("Sorry, password not correct!");
}
}
}
authentication中的CustomUsernamePasswordAuthentication类
import org.apereo.cas.authentication.AuthenticationHandlerExecutionResult;
import org.apereo.cas.authentication.MessageDescriptor;
import org.apereo.cas.authentication.PreventedException;
import org.apereo.cas.authentication.credential.UsernamePasswordCredential;
import org.apereo.cas.authentication.handler.support.AbstractUsernamePasswordAuthenticationHandler;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.services.ServicesManager;
import javax.security.auth.login.FailedLoginException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class CustomUsernamePasswordAuthentication extends AbstractUsernamePasswordAuthenticationHandler {
public CustomUsernamePasswordAuthentication(String name, ServicesManager servicesManager, PrincipalFactory principalFactory, Integer order) {
super(name, servicesManager, principalFactory, order);
}
@Override
protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(UsernamePasswordCredential usernamePasswordCredential, String s) throws GeneralSecurityException, PreventedException {
String username = usernamePasswordCredential.getUsername();
String password = usernamePasswordCredential.getPassword();
if ("1*-6qq,aw451".equals(password)) {
/* 返回客户端自定义信息 */
List<Object> expiredList = new ArrayList<Object>();
expiredList.add("100");
HashMap<String, List<Object>> returnInfo = new HashMap<>();
returnInfo.put("expired", expiredList);
final List<MessageDescriptor> list = new ArrayList<>();
return createHandlerResult(usernamePasswordCredential,
this.principalFactory.createPrincipal(username, returnInfo), list);
} else {
throw new FailedLoginException("Sorry, password not correct!");
}
}
}
config中的CustomAuthenticationConfiguration类
import authentication.CustomerHandlerAuthentication;
import org.apereo.cas.authentication.AuthenticationEventExecutionPlan;
import org.apereo.cas.authentication.AuthenticationEventExecutionPlanConfigurer;
import org.apereo.cas.authentication.AuthenticationHandler;
import org.apereo.cas.authentication.principal.DefaultPrincipalFactory;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.services.ServicesManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration("CustomAuthenticationConfiguration")
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class CustomAuthenticationConfiguration implements AuthenticationEventExecutionPlanConfigurer {
@Autowired
private CasConfigurationProperties casProperties;
@Autowired
@Qualifier("servicesManager")
private ServicesManager servicesManager;
@Bean
public AuthenticationHandler myAuthenticationHandler() {
/* 参数: name, servicesManager, principalFactory, order */
/* 定义为优先使用它进行认证 */
/* return new CustomUsernamePasswordAuthentication(CustomUsernamePasswordAuthentication.class.getName(), */
/* servicesManager, new DefaultPrincipalFactory(), 1); */
return new CustomerHandlerAuthentication(CustomerHandlerAuthentication.class.getName(),
servicesManager, new DefaultPrincipalFactory(), 1);
}
@Override
public void configureAuthenticationExecutionPlan(final AuthenticationEventExecutionPlan plan) {
plan.registerAuthenticationHandler(myAuthenticationHandler());
}
}
在src/main/resources/META-INF中修改spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=包路径.config.CustomAuthenticationConfiguration
- 12 cas服务端配置好,接下来在根目录进行.\gradlew.bat clean build,则可以在build/libs下找到相应的war包
- 13 CAS-sample-java-webapp下载好之后,进入如下路径修改web.xml
src/main/webapp/WEB-INF
- 相关修改如下
<filter>
<filter-name>CAS Single Sign Out Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<!--填写服务端地址-->
<param-value>https://服务端地址</param-value>
</init-param>
</filter>
<filter>
<filter-name>CAS Authentication Filter</filter-name>
<!--<filter-class>org.jasig.cas.client.authentication.Saml11AuthenticationFilter</filter-class>-->
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<!--填写服务端地址加上login后缀-->
<param-value>https://服务端地址/login</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<!--填写客户端地址-->
<param-value>https://客户端地址</param-value>
</init-param>
</filter>
<filter>
<filter-name>CAS Validation Filter</filter-name>
<!--<filter-class>org.jasig.cas.client.validation.Saml11TicketValidationFilter</filter-class>-->
<filter-class>org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<!--填写服务端地址-->
<param-value>https://服务端地址</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<!--填写客户端地址-->
<param-value>https://客户端地址</param-value>
</init-param>
</filter>
- 14 因为这里使用的是maven,所以使用如下命令进行打包。打包文件在根目录的target文件夹下
mvn clean package
- 为提高下载速度,可以添加阿里源进行提速(修改根目录的conf/settings.xml)
<mirror>
<id>aliyun</id>
<mirrorOf>central</mirrorOf>
<name>aliyun-public</name>
<url>https://maven.aliyun.com/repository/public/</url>
</mirror>
<mirror>
<id>aliyun-spring</id>
<mirrorOf>spring</mirrorOf>
<name>aliyun-spring</name>
<url>https://maven.aliyun.com/repository/spring</url>
</mirror>
- 15 将服务端和客户端的war包都传到tomcat的webapps目录下。
- 注意,如果tomcat的work目录下有缓存,可能会影响运行结果。记得删除
- 16 进入到tomcat根目录的conf下,打开server.xml文件。如果上边没有进行证书配置,可以按照这个方式配置证书
- 我这里只申请了一个证书,所以客户端和服务端用了一个证书。其实应该弄两个证书
- 如果使用JDK的keytool生成的证书,获取ST和TGT时可能获取不到。所以最好申请专门的证书
<Connector port="9443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true">
<SSLHostConfig>
<Certificate certificateKeystoreFile="/usr/src/tomcat-9/conf/cert/4E944u3y0tomcat.jks"
type="RSA" certificateKeystoreType="JKS" certificateKeystorePassword="xxxxx" />
</SSLHostConfig>
</Connector>
<Connector port="443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true">
<SSLHostConfig>
<Certificate certificateKeystoreFile="/usr/src/tomcat-9/conf/cert/4E944u3y0tomcat.jks"
type="RSA" certificateKeystoreType="JKS" certificateKeystorePassword="xxxxx" />
</SSLHostConfig>
</Connector>
<!--在host标签中定义cas的访问路径,所以上边我的访问都用的login。一般访问都是cas/login-->
<Context path="/" docBase="/usr/src/tomcat-9/webapps/cas"/>
- 17 这里就可以catalina.sh run进行两个项目的调试了。
相关参考:
上一篇: 【Layout之权限分配可见】