1.单点登录与状态共享方案
随着企业的应用规模不断增大,一个单体应用很难满足用户量增长的需求,这就需要我们将单体应用集群化部署,或者将单体应用微服务化。在这个过程中,就涉及到两个问题:
- 集群应用之间如何共享session,我在应用1上保存的数据,在访问应用2接口的时候还可不可以获取到了?理想的答案是可以
- 我在访问应用1的时候已经登陆了,作为统一体系下的子应用,我再访问应用2还需要再次登陆么?理想的答案是不需要
为了让以上两个问题获得理想的答案,通常可以使用以下四种方案
方案一:基于session共享
1.1. 单个应用的session应用
要了解集群的session如何共享,一定要先了解单体应用是如何使用session的
- 单体应用用户登陆之后,将状态信息保存到session里面。服务端自动维护sessionid,即将sessionid写入cookie。
- cookie随着HTTP响应,被自动保存到浏览器端。
- 当用户再次发送HTTP请求,sessionid随着cookies被自动带回服务器端(浏览器行为,无需编码)
- 服务器端根据sessionid,可以找回该用户之前保存在session里面的数据。
1.2.集群应用的Session共享
- 同一IP(域名),不同端口,在同一个浏览器cookies是共享的。不同IP(域名)的Cookies,在同一个浏览器Cookies肯定不共享的。对于这种情况需要在集群应用的前面加上负载均衡器逆向代理,如:nginx,haproxy。让客户端看上去访问的是同一个IP(代理IP),从而浏览器认为基于这个IP的Cookies是共享的。
- SESSION正常是由Servlet容器来维护的(内存里面,每个服务器内存是不共享的),这样集群节点之间SESSION就无法共享。如果希望Session共享,就需要把sessionID的存储放到一个统一的地方,如:redis。SessionID的维护交给Spring session则更加方便。
- 除了Cookies可以维持Sessionid,Spring Session还提供了了另外一种方式,就是使用header传递SESSIONID。目的是为了方便类似于手机这种没有cookies的客户端进行session共享。
这种应用架构方式,特别适合一份应用代码启动多个实例, 也就是说上图中的应用1、应用2、应用3是从一份代码的基础上启动的三个实例实现共享session。因为代码是一样的,session状态信息是一样的,所以客户端的sessionid一致就被认为是同一个用户,在一个实例上登陆,就可以访问其他实例的接口。因为类似于Spring Security这种权限管理框架,权限信息都在session里。
方案二:基于CAS单点登录
我们刚才讲的第一种方案特别适合一份应用代码启动多个实例的场景。那下面为大家介绍的CAS方案,就比较适合不同的代码启动的应用实例之间实现单点登录。比如说:chrome、youtube、gmail都是谷歌公司的,但是不同的产品,我们在chrome浏览器登陆了,就直接可以访问YouTube和gmail,这是怎么做的?
2.1.第一次访问应用1
需要在应用服务之外单独搭建一个CAS Server,也就是登录认证服务器
- 用户访问应用1的资源
- 应用1发现这个用户没有登录,响应302,告知浏览器将该用户的请求重定向到CAS Server进行登录
- 浏览器重定向到CAS Server
- CAS Server收到请求,同样确认该用户未登录,响应login页面让其登录
- 用户输入正确用户名和密码认证后,CAS Server为该用户颁发TGC和ST,并在自己的内存里保存TGT。并告知浏览器重定向回到应用1
- TGC与TGT的关系就好像sessionid与session的关系。TGT中保存了该登陆用户的信息,TGC是用于匹配这些信息的钥匙
- TGC保存在cookies里面,在浏览器请求与响应中自动携带。
- ST比较单纯,Service Ticket,用于访问服务资源,并且只能使用一次
- 浏览器重定向回到应用1的资源,并携带(TGC、ST)
- 应用1收到重定向请求后,向CAS Server发请求,根据TGC找到TGT,并验证ST是否有效
- CAS Server验证该ST是有效的,应用1已知该用户合法。在自己的应用里面记录用户的session信息,并将用户请求的资源响应回浏览器
登录认证不等于服务接口鉴权,这里只是代表用户登录成功了,不代表你进系统之后什么接口都可以访问!接口授权鉴权的工作仍然是Spring Security来做。
2.2.第二次访问应用1
用户已经访问过应用1了,并且登录认证成功,第一次登录认证成功之后,应用1在自己的应用session内记录用户状态信息,因此这里就直接通过了,不需要二次登录认证。
2.3.在应用1内调用应用2资源
现在问题来了,chrome(应用1)登陆成功了,我们要访问gmail(应用2),都是谷歌的产品,你不好让用户重新登陆吧?
- 用户浏览应用1的时候,访问了应用2的页面或接口(上一次认证成功颁发的TGC一直还携带着)
- 应用2发现该用户重来没有访问过我,所以应用2认为该用户没有权限访问我的资源
- 告知浏览器重定向到CAS Server进行认证
- 认证服务器根据TGC找到上一次保存的TGT,找到之后即表示认证成功,签发ST
- 认证成功之后响应302,告知浏览器重定向回到应用2,
- 应用2在收到重定向请求之后,重复2.1中的7和8步骤,向CAS Server验证ST的有效性,如果有效,在自己的应用里面记录用户的session信息,并将用户请求的资源响应回浏览器
这样这个用户以后访问应用2就不需要再次认证了。
有的朋友可能会问,这只解决了应用之间单点登陆的问题,那session共享的问题呢?一般来说,不同代码启动的应用实例之间是不需要共享session的(除了认证数据),认证过程已经交给CAS Server实现了。还有共享session的必要么?
三、基于JWT
上面的这个CAS Server集中验证的方式虽然能够实现单点登录,但是请求认证过程过于复杂,不够灵活。需要独立维护一个CAS Server,现在我们想让应用的部署更灵活一些,而且不需要CAS的外部依赖,不需要session共享的前端代理和后端redis session集中存储,这些我们都不想要。
如果没有独立的状态存储,这种应用就是无状态应用。有状态应用用户的数据是存储在session里面,或者CAS Server提供认证状态,或者redis session状态集中存储。那么无状态应用,用户的登录状态、角色、权限这些数据放在哪?答案就是JWT令牌,JWT实现也有两种方式
- 方式一:JWT中只含用户的唯一标志,用户每一次访问资源都使用该唯一标志重新去数据库加载用户状态信息数据。
- 方式二:JWT中包含所有的用户状态信息数据,这样做的坏处是增加了网络带宽的负担,但是因为完全不需要session,从而降低了对于内存的需求;也降低了数据库用户状态数据查询操作的需求。
用户在应用A上登录认证,应用A会颁发给他一个JWT令牌(一个包含若干用户状态信息的字符串)。当用户访问应用B接口的时候,将这个字符串交给应用B,应用B根据Token中的内容进行鉴权。不同的应用之间按照统一标准发放JWT令牌,统一标准验证JWT令牌。从而你在应用A上获得的令牌,在应用B上也被认可,当然这样这些应用之间底层数据库必须是同一套用户、角色、权限数据。
如果说CAS Server是网盘,那么JWT就是U盘。你品、你细品。U盘容易丢,丢了怎么办,关于JWT安全相关的问题,我们后面章节会为大家介绍。
四、基于OAuth2(也可以加JWT)
以上的所有的单点集群登陆方案,都是有一个前提就是:应用A、应用B、应用1、应用2、应用3都是你们公司的,你们公司内部应用之间进行单点登陆验证。
但是我想大家都见过这样一个场景:我们登录某一个网站,然后使用的是我们在QQ、微信上保存的用户数据。也就是说第三方应用想使用某个权威平台的用户数据做登录认证,那么这个权威平台该如何对第三方应用提供认证服务?目前比较通用的做法就是OAuth2(现代化的社交媒体网站登录基本都使用OAuth2)
OAuth2协议的授权码模式,和JWT令牌的原理有些类似,但是它和JWT又不相同,OAuth2协议有独立的认证服务器。这个我们后面章节会为大家详细介绍,这里知道有这么一个方案和它的应用场景就可以了。
2.基于session共享登陆方案
请结合上一节的讲解,将集群应用session共享的原理(上图)理解清楚,下面我们来做编码实现。
一、集成Spring session
1.引入spring-session-redis的maven依赖
项目内引入spring-session-data-redis
,配合spring-boot-starter-data-redis
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.配置启用Redis的httpSession
在启动类
上方加上注解,启动SpringSession管理应用的session,并设置session数据的有效期30分钟
让redis来管理session
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 30 * 60 * 1000)
3.配置redis链接信息(application.yml)
之前的章节我们也已经配置过(对于redis哨兵模式、cluster模式连接配置不一样,可以参考Spring Boot基础进行学习,在Spring Security课程内不做赘述)
spring:
redis:
database: 0 # redis 16个库,我们选择0号库
host: 192.168.161.3
password: 4rfv$RFV # 没有可忽略
port: 6379
4.测试
做好上文的配置之后,并加入如下的测试代码,后文测试的时候会用到
@Controller
public class SessionController {
@RequestMapping(value="/uid",method = RequestMethod.GET)
public @ResponseBody
String uid(HttpSession session) {
return "sessionId:" + session.getId();
}
}
5. 一个项目多个端口启动
- 点击edit configuration ,
取消勾选single instance only
(只允许单节点运行)。在比较新的版本中这个勾选框变成了Allow parallel run(允许多实例并发运行)
,那你就给它勾选上。总之我们是要运行多实例。 - 复制一份当前配置,在environment选项中的vm options 中设置不同的端口号
-Dserver.port=8889 -Dserver.httpPort=89 -Dspring.profiles.active=dev -Ddebug
-Dserver.port=8888 -Dserver.httpPort=88 -Dspring.profiles.active=dev -Ddebug
二、测试
依次访问,看看效果.通过返回值session.getId()
即:sessionid来判断,如果sessionid一致,则证明session共享成功了。
用浏览器访问下面的地址,自己看一下效果,再理解一下。
- https://localhost:8888/uid
- https://localhost:8889/uid
- https://127.0.0.1:8888/uid
- https://127.0.0.1:8889/uid
因为我们在同一台机器上启动多个实例,ip相同所以session是共享的。如果你在不同的服务器上启动多个实例(IP)不同,你需要在应用前方加上负载均衡逆向代理(haproxy、nginx)才可以实现session共享。(如本文首图)
3.CAS认证服务器搭建
一、什么是 CAS
CAS (Central Authentication Service) 是 Yale 大学发起的一个 Java 开源项目,旨在为 Web应用系统提供一种可靠的 单点登录 解决方案( Web SSO )
1.1 CAS 组成部分
请结合《单点登录与状态共享方案》那一节的内容理解CAS Server和CAS Client的作用。
- CAS Server,需要独立部署,用来校验用户名/密码等凭证,也可以被称作:认证服务器或授权服务器
- CAS Client,CAS 客户端,与需要被保护的资源应用集成在一起,以过滤器的形式存在。当用户未登陆的时候,它负责将请求重定向到CAS Server进行登录认证。
1.2 CAS中重要的概念
请结合《单点登录与状态共享方案》那一节的内容理解以下三个词汇的含义。
- TGT(Ticket Granting Ticket),用户登录认证成功之后,CAS Server在自己的内存里面保存的票根,包含这个用户的用户基本信息,用户名、登录有效期等信息。
- TGC( Ticket Granting Cookie),票根唯一标识,由 CAS Server 通过 SSL 方式发送给终端用户,以cookies的方式保存。 TGC与TGT的关系就好像sessionid与session的关系。TGT中保存了该登陆用户的信息,TGC是用于匹配这些信息的钥匙
- **ST (Service ticket)**服务票据,由CASServer 颁发给CASClient,CASClient如果能正确接收 Service Ticket ,说明在 CASClient-CASServer 之间的信任关系已经被正确建立起来,通常为一张数字加密的证书;
二、CAS Server的搭建
2.1.下载安装包源码
下载地址: https://github.com/apereo/cas-overlay-template
我选择了6.2版本。但是新版本有几个需要注意的事项:
- 依赖JDK 11,请先安装好
- 去掉了maven,使用gradle进行构建。(不需要提前安装,会自动下载,跟着我进行操作即可)
笔者也曾想使用比较旧的版本,但是旧的版本我实验了一下,项目的开源作者已经放弃了对旧版本的维护,导致构建、启动经常会报错。所以我们还是使用比较新的版本吧。
下载的是源码,我们使用IDEA编辑器把它打开,该项目默认最低支持是JDK 11。所以配置项目的JDK。
并在项目根目录下的文件gradlew.bat(linux下使用gradlew),加上JAVA_HOME指向JDK11
2.2.依赖SSL证书
出于网络传输安全性的考量,CAS Server要使用HTTPS通信,互联网SSL证书都是收费的,我们只是用于学习的化,可以使用下面的方法来生成自用证书。
在Windows的搜索字段中键入cmd以找到命令提示符,然后以“以管理员身份运行”右键单击。使用如下的keytool命令。您可以提及所需的证书名称,如下所示。
PS C:\Program File (x86)\Java\jdk-11.0.9\bin>
keytool -genkeypair -alias cas-server -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore ./cas-server.p12
- 证书的别名cas-server ,随意命名,能体现证书的用途和唯一性是最好的命名方式
- -keyalg RSA -keysize 2048 -storetype PKCS12 指定算法和证书大小,证书的类型
- -keystore 证书保存的位置,我们就放在bin目录下面了
- 命令回车之后,会提示输入密码(这个密码要记住,后面章节会用到)
- 命的名字与姓氏,一定要填写CAS Server部署的主机的域名(我这里本机,所以使用了localhost)
同时项目里面的cas.properties里面的cas.server.name
也要改成localhost,对应上。cas.server.name
代表CAS Server部署的主机的域名
2.3. 配置CAS Server
在根目录下执行如下命令,作用是将cas overlay的原始war包解压到build目录。我们主要使用它里面的配置文件,并在其基础上进行个性化修改。
./gradlew.bat explodeWar
新建 src/main/resources 目录,并将刚解压出来的 build/cas-resources/application.properties 文件和刚刚生成的 keystore 证书文件拷贝进来。
然后修改 application.properties ,主要配置一下 keystore 的位置和密钥,如下:
server.ssl.key-store=classpath:cas-server.p12
server.ssl.key-store-password=123456 #生成证书时候的密码
server.ssl.key-password=123456 #生成证书时候的密码
server.ssl.enabled=true
配置完成后,在项目根目录下执行如下命令启动项目。
./gradlew.bat run
启动的过程会有jar包下载等动作,所以第一次启动可能会时间很长。启动过程中会有找不到tomcat war的异常信息,出现STOP标志,都不用管它。等待,直到出现如下的标志,表示启动成功了
三、访问测试
访问 https://localhost:8443/cas/login (注意是https),出现下面的图是因为我们的证书是自己生成的,不是国际互联网认证的证书。继续访问即可。
默认的用户名是 casuser,密码是 Mellon,输入用户名密码就可以登录了。
默认的用户名/密码也可以在配置文件application.properties 文件中修改如下配置
cas.authn.accept.users=casuser::Mellon
修改用户名密码,需要重启才能生效,这里的用户名密码是静态写死的,这显然不是我们想要的效果,下一节我们来解决这个问题。
4.CAS动态加载用户密码数据
上一节我们成功的搭建了CAS 认证服务器,但是能够登录认证的用户名和密码是在配置文件里面写死的,这显然不是我们需要的效果,我们本节来为大家实现从数据库加载用户认证所需的信息。
一、gradle配置文件
把下面的依赖包加入到build.gradle的dependencies配置段里面。就当作maven在引入jar包是一样的。
dependencies {
// Other CAS dependencies/modules may be listed here...
// implementation "org.apereo.cas:cas-server-support-json-service-registry:${casServerVersion}"
implementation "org.apereo.cas:cas-server-support-jdbc:${casServerVersion}"
implementation "mysql:mysql-connector-java:5.1.47"
}
看上去我们只引入了2个包,但是实际上包含了很多的子包。所以下载的过程还是挺耗时的,可以把如下的maven库(国内的阿里源)添加到build.gradle中的buildscript.repositories
和repositories
配置内,这样我们后续加入新的包下载的会更快。
buildscript {
repositories {
maven {
url 'https://maven.aliyun.com/repository/public/'
}
……
}
}
二、增加动态加载用户认证信息的配置
以下的所有配置都是在application.properties中完成(该文件上一节加入)。首先将上一节中使用的静态用户名密码配置注释掉。
##
# CAS Authentication Credentials
#
#cas.authn.accept.users=casuser::Mellon #注释掉上节写死的账号密码
#cas.authn.accept.name=Static Credentials
配置用户认证信息数据从哪张表里面查,密码等信息是哪一个字段?分局用户名查到密码之后,CAS Server会将其与用户输入的密码进行比对认证。
我们仍然从RBAC建模使用的sys_user表查询用户的password数据
#查询账号密码SQL,*必须包含密码字段
cas.authn.jdbc.query[0].sql=select * from sys_user where username=?
#指定上面查询的SQL密码字段名(必须)
cas.authn.jdbc.query[0].fieldPassword=password
#指定过期字段,1为过期,若过期不可用(可选,过期会提示修改密码)
cas.authn.jdbc.query[0].fieldExpired=
#1表示该账户不可用,需要解锁(可选)
cas.authn.jdbc.query[0].fieldDisabled=
上面配置中使用的sys_user表在哪一个数据库?如何访问?
#数据库连接
cas.authn.jdbc.query[0].url=jdbc:mysql://192.168.161.3:3306/testdb3?useUnicode=true&characterEncoding=utf-8&useSSL=false
#数据库dialect配置
cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialect
#数据库用户名
cas.authn.jdbc.query[0].user=test
#数据库用户密码
cas.authn.jdbc.query[0].password=4rfv$RFV
#数据库事务自动提交
cas.authn.jdbc.query[0].autocommit=false
#数据库驱动
cas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Driver
#超时配置
cas.authn.jdbc.query[0].idleTimeout=5000
如果你的password是明文存储的,那我们的配置就完成了。但是大家都是到我们的password是使用Spring Security的BCrpytPasswordEncoder加密存储的。怎么办?加入如下的配置即可。
#默认加密策略是NONE不加密
cas.authn.jdbc.query[0].passwordEncoder.type=BCRYPT
cas.authn.jdbc.query[0].passwordEncoder.characterEncoding=UTF-8
#通过encodingAlgorithm来指定算法
cas.authn.jdbc.query[0].passwordEncoder.encodingAlgorithm=SHA
#BCRYPT密码强度,参考《PasswordEncoder介绍》进行复习
cas.authn.jdbc.query[0].passwordEncoder.strength=10
三、登陆测试
登录CAS Server
这回我们输入的密码不再是casuser:Mellon,而是我们在数据库sys_user表里面保存的用户密码。输入正确的用户名和密码之后,登陆成功。可以看到认证数据出自数据库。
5.CAS资源服务器搭建
之前我们搭建了CAS Server,也就是登录认证服务器。我们本节来为大家实现资源服务器,也就是对外提供业务服务的应用,也可以被称为CAS Client客户端(这里的客户端时相对CAS Server而言,对于用户它仍然是服务端)。
一个应用的权限控制包括两个核心部分
- 登录认证,这个结合前几节搭建的CAS Server去做
- 另一部分授权鉴权,这个部分和第一章第二章讲的内容几乎是一致的
一、新建cas-resource1应用
我们新建一个Spring Boot应用作为被CAS 保护的资源服务(即:通过CAS Server进行单点登录认证)。
新建的应用里面除了登录认证和之前的basic-server不一样,其他的部分几乎是一致的。
- 所以我们有必要将cas-resource1和basic-server公用的业务逻辑代码拆分为一个独立的模块commons-spring-security-starter。参考本文档《附录一:抽取公共资源为独立模块》的使用方法
- 另外,mybatis、freemarker的application全局配置文件pom、代码、结构和basic-server都是一样的,直接拿过来即可。这里不做过多的赘述。
- 因为使用CAS 进行统一登录认证,所以login.html页面我们不要了
二、导出JDK11证书并导入JDK8
另外需要注意的一点是:我们搭建的cas-server使用的是JDK 11,cas-server.p12也是在《CAS认证服务器搭建》中通过JDK11生成的,我新建的cas-resource1使用的是JDK8,那么这个证书是导入JDK11的密钥库,还是导入JDK8的密钥库?如果需要导入JDK8密钥库,JDK11生成的证书,JDK8能用么?
- 首先,明确一点JDK11使用cas-server.p12证书进行数据签名,cas-resource1需要对这个签名进行解签。所以需要将证书导入cas-resource1应用使用的JDK密钥库。对于我来说就是JDK8
- 其次,cer证书是通用的,不区分JDK版本。(它压根就跟JDK没什么关系,JDK只是可以生成自建证书的一个工具)
将《CAS认证服务器搭建》生成的证书(cas-server.p12)copy到JDK8的bin目录下面,并导出cert证书(cas-server.cert),别名要和《CAS认证服务器搭建》中生成证书时一致。
PS C:\Program File (x86)\Java\jdk1.8.0_171\bin>
keytool -export -trustcacerts -alias cas-server -file ./cas-server.cer -keystore ./cas-server.p12
截图如下,会提示输入口令,口令要和生成证书设置的密码一致
PS C:\Program File (x86)\Java\jdk1.8.0_171\bin>
keytool -import -trustcacerts -alias cas-server -file ./cas-server.cer -keystore ../jre/lib/security/cacerts
输入上面的命令之后,会提示输入密钥库的口令。大家要注意这个口令,不是我们生成证书时设置的密码,而是JDK证书库密码。如果你没修改过,默认是:changeit
三、集成spring-security-cas
应用创建完成,数据安全证书搞定。下面我们来真正的处理资源服务(也就是对外服务的接口的授权鉴权问题),结合Spring Security该如何做?
首先,在cas-resource1项目maven配置文件里面引入如下坐标
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
然后在application.yml里面加入如下的自定义配置,这两段配置分别代表了CAS Server的登录和注销地址,CAS Resource的登录和注销地址。
- 访问CAS Resource的资源会被拦截,如果用户没经过认证会重定向到CAS Server的登录地址
- 访问CAS Resource的退出功能,如果用户没经过认证会被重定向到CAS Server的注销地址进行注销
server:
port: 9001
cas:
server:
baseurl: https://localhost:8443/cas
loginurl: /login
logouturl: /logout
client:
baseurl: http://localhost:9001
loginurl: /login/cas
logouturl: /logout/cas
所以为了配合上文中的注销地址,我们需要对首页index.html做出如下修改。原来是“logout”改成“/logout/cas”
为了能获取到上文中自定义的配置,我们新加入两个配置属性加载实体类
@Data
@Component
@ConfigurationProperties(prefix = "cas.client")
public class CASClientProperties {
private String baseurl;
private String loginurl;
private String logouturl;
}
@Data
@Component
@ConfigurationProperties(prefix = "cas.server")
public class CASServerProperties {
private String baseurl;
private String loginurl;
private String logouturl;
}
CAS配置文件Spring Security与Spring Security CAS之间的配置桥梁。
@Configuration
public class CASSecurityConfig {
@Resource
CASClientProperties casClientProperties;
@Resource
CASServerProperties casServerProperties;
@Resource
MyUserDetailsService myUserDetailsService;
@Bean
ServiceProperties casServiceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
//CAS Server登录成功后,重定向回来接收ST的地址。
serviceProperties.setService(casClientProperties.getBaseurl() + casClientProperties.getLoginurl());
return serviceProperties;
}
//资源端CAS 验证的入口
@Bean
AuthenticationEntryPoint casAuthenticationEntryPoint() {
CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
//设置 CAS Server 的登录地址(知道没权限上哪里去认证)
entryPoint.setLoginUrl(casServerProperties.getBaseurl() + casServerProperties.getLoginurl());
//认证成功回来之后哪个路径来处理ST的验证
entryPoint.setServiceProperties(casServiceProperties());
return entryPoint;
}
@Bean
TicketValidator ticketValidator() {
//配置 ST的校验服务地址
//资源端拿到 ST要去 CAS Server 上校验真伪,所以参数是CAS server的地址
return new Cas20ProxyTicketValidator(casServerProperties.getBaseurl());
}
====================以上的bean都是处理CAS登录认证的问题================================================
//处理授权的问题,也就是用户登陆认证成功之后能访问哪些资源的问题
@Bean
CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setServiceProperties(casServiceProperties());
provider.setTicketValidator(ticketValidator());
//核心配置参数是myUserDetailsService
provider.setUserDetailsService(myUserDetailsService);
provider.setKey("achang-secret"); //并不重要,唯一即可
return provider;
}
//casAuthenticationFilter过滤器,所有的请求都要被过滤,如果该用户认证过则通过casAuthenticationProvider进行授权
@Bean
CasAuthenticationFilter casAuthenticationFilter(AuthenticationProvider authenticationProvider) {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setServiceProperties(casServiceProperties());
List<AuthenticationProvider> providers = new ArrayList<>();
providers.add(authenticationProvider);
filter.setAuthenticationManager(new ProviderManager(providers));
return filter;
}
//LogoutFilter 拦截“/logout/cas”注销请求并转发到 CAS Server
@Bean
SingleSignOutFilter casSingleSignOutFilter() {
SingleSignOutFilter sign = new SingleSignOutFilter();
sign.setIgnoreInitConfiguration(true);
return sign;
}
//若CAS Server注销这个用户之后,通知各个资源段,casSingleSignOutFilter就是接收CAS server注销通知的,接收到之后在资源端也对这个用户进行注销。就是将资源端注销用户
@Bean
LogoutFilter casLogoutFilter() {
LogoutFilter filter = new LogoutFilter(
casServerProperties.getBaseurl() + casServerProperties.getLogouturl()
,new SecurityContextLogoutHandler());
filter.setFilterProcessesUrl(casClientProperties.getLogouturl());
return filter;
}
}
- casServiceProperties中主要配置一下Client 的cas登录地址即可,这个地址就是在 CAS Server 上登录成功后,重定向回来的接收ST的地址。
- casAuthenticationEntryPoint 是资源端CAS 验证的入口,这里首先设置 CAS Server 的登录地址(知道没权限上哪里去认证);设置casServiceProperties,认证成功回来之后哪个路径来处理ST的验证。
- TicketValidator 这是配置 ST的校验服务地址,资源端拿到 ST要去 CAS Server 上校验真伪,所以参数是CAS server的地址。
- 以上的bean都是处理CAS登录认证的问题,casAuthenticationProvider主要是用来处理授权的问题(也就是用户登陆认证成功之后能访问哪些资源的问题),所以核心配置参数是myUserDetailsService。
- casAuthenticationFilter过滤器,所有的请求都要被过滤,如果该用户认证过则通过casAuthenticationProvider进行授权。
- LogoutFilter 拦截“/logout/cas”注销请求并转发到 CAS Server。
- CAS Server注销这个用户之后,在通知各个资源段,casSingleSignOutFilter就是接收CAS server注销通知的,接收到之后在资源端也对这个用户进行注销。
各个请求接收与响应的链路组件上面就完成了,我们怎么让它生效。就是SecurityConfig的Spring’ Security配置文件的任务了。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
AuthenticationEntryPoint casAuthenticationEntryPoint;
@Resource
AuthenticationProvider casAuthenticationProvider;
@Resource
SingleSignOutFilter casSingleSignOutFilter;
@Resource
LogoutFilter casLogoutFilter;
@Resource
CasAuthenticationFilter casAuthenticationFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//这个provider调用了MyUserDetailsService
auth.authenticationProvider(casAuthenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login/cas").permitAll()
.anyRequest()
.access("@rbacService.hasPermission(request,authentication)")
.and()
.exceptionHandling()
.authenticationEntryPoint(casAuthenticationEntryPoint)
.and()
.addFilter(casAuthenticationFilter)
.addFilterBefore(casSingleSignOutFilter, CasAuthenticationFilter.class)
.addFilterBefore(casLogoutFilter, LogoutFilter.class);
}
}
四、服务注册
要将某一个服务资源接入CAS Server进行统一的登录认证,这个服务资源就必须先在CAS Server进行注册。具体配置方式如下,在 CAS Server 中创建如下目录src/main/resources/services
在该目录下创建一个名为 casresource1-10000001.json 的文件,casresource1表示要接入的 client 的名字,10000001表示要接入的 client 的 id。
json 文件内容如下,这个配置可以参考官方给出的模版:WEB-INF/classes/services/Apereo-10000002.json
{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "^(https|http)://localhost:9001.*",
"name" : "casresource1",
"id" : 10000001,
"description" : "CAS resource 测试",
"evaluationOrder" : 1
}
- @class:必须为org.apereo.cas.services.RegisteredService的实现类,对其他属性进行一个json反射对象,常用的有RegexRegisteredService,匹配策略为id的正则表达式
- serviceId:唯一的服务id
- name: 服务名称,会显示在默认登录页
- id:全局唯一标志
- description:服务描述,会显示在登录页上面(用户在登录什么系统)
- evaluationOrder:确定已注册服务的相对评估顺序。当两个服务URL表达式覆盖相同的服务时,此标志尤其重要;评估顺序决定首先评估哪个注册,并作为内部排序因素。
(越小越优先)
CAS Server的application.properties新增如下配置,让services目录下的配置文件生效。
#开启识别json文件,默认false
cas.serviceRegistry.initFromJson=true
cas.serviceRegistry.config.location=classpath:/services
五、测试登录与注销
cas-server端启动,使用如下的命令
./gradlew.bat run
并且将cas-resource1应用启动之后,访问 http://localhost:9001/syslog 。因为该用户还没有登录认证所以被我们上文配置的casAuthenticationFilter过滤器拦截,拦截后跳转到cas server的登陆页面
输入用户名和密码(数据库sys_user表里面配置的,也是我们之前章节一直使用的),我的是admin:123456. 用户密码在CAS server验证正确之后,又回到"/syslog"日志管理界面(其实中间是先回到“/login/cas”,一闪而过)。
这时访问根路径“/”(index.html)也是可以的,说明该用户被正确授权,casAuthenticationProvider生效。
点击首页的退出登录“/logout/cas”,被casLogoutFilter拦截跳转到CAS Server进行了注销。
CAS Server注销之后会通知各个资源端,这个过程通过页面观察不到。