欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

CAS在windows AD下实现企业微信自动扫码登陆的总结

程序员文章站 2022-05-17 08:55:38
...

基础名词

  • 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登陆流程与注销流程
  • CAS在windows AD下实现企业微信自动扫码登陆的总结
  • CAS在windows AD下实现企业微信自动扫码登陆的总结
  • 用户与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
  • 具体名词的实现过程可以参考该链接
  • CAS在windows AD下实现企业微信自动扫码登陆的总结
  • 图片引用出处,可以在这里查看一下具体的流程解释。

LDAP与Windows AD常识

LDAPwindows AD

具体过程

  1. 进入CAS客户端,判断用户是否登陆。
  2. 登陆的话直接显示客户端相关内容,没有登陆则进行CAS客户端跳转到CAS服务端
  3. CAS服务端已经编写好相关登陆界面,同时拥有企业微信二维码。
  4. 企业微信扫码,获取权限。经过OAuth2授权后获得用户信息,可以读取到用户唯一ID
  5. 根据用户唯一ID,自动登陆后台,获取TGC。
  6. 获取相应的TGT,通过TGT获取ST,将ST返回CAS客户端。
  7. CAS客户端拿ST去CAS服务端判断是否正确。正确则进行登陆

使用模板

CAS-Overlay-template6.2

CAS-sample-java-webapp

企业微信相关API

使用工具

JDK11

  • JDK配置PATH的时候,环境变量要放到最开始,以免被Oracle的默认JDK顶掉

Tomcat9

  • HTTPS的配置
  • 运行加入了JDBC的cas.war出现了ACTION: AUTHENTICATION_FAILED
    1. 参考这个博客。mysql需要开启远程登录。
    2. 可能JDBC连接出了问题,我这里是因为war包运行出问题,注释了jdbc的依赖包的代码。也有可能是JDBC的jar包没有下载好,按照这个方式下载好mysql的JDBC的jar包即可。

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进行如下配置(参考该博客)
      1. proxies中可以配置多个proxy,但是默认第一个proxy生效。
      2. active中的TRUE表示该代理目前生效状态
      3. http协议、主机地址、端口不在赘述。
      4. 用户名密码按需配置即可。
      5. nonProxyHost表示不需要代理访问的地址。中间的竖线分隔多个地址,此处可以使用星号作为通配符号。
<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文件夹下进行整个项目的一些配置。以下仅列出修改或增加的部分
    • LDAP配置详解。通过博客里边使用的配置可以进行openLDAP的配置。windows AD参见下边
    • openLDAP安装及配置。注意SSHA填写自己生成密码之后的SSHA
###
# 修改部分
#
# 对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进行两个项目的调试了。

相关参考:

CAS单点登录(二)——搭建基础服务

CAS单点登录(四)——自定义认证登录策略

CAS单点登录(五)——Service配置及管理

CAS单点登录(六)——自定义登录界面和表单信息

java模拟登录CAS统一认证中心

史上最全的Cas学习整理