整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

单点登录的CAS实践

、在谈项目实践之前,先说两个概念:

(1)ruoyi-vue:采用前后端分离的单体web框架,参见介绍 | RuoYi,使用MIT开源协议,对商业使用友好。

(2)CAS:英文为Central Authentication Service,即中央认证服务,为耶鲁大学发起的一个开源项目,是实现sso单点登录的框架。官网地址:https://www.apereo.org/projects/cas。关于spring secuity集成cas可参考:https://docs.spring.io/spring-security/site/docs/5.4.1/reference/html5/#servlet-cas。

CAS官方架构:https://apereo.github.io/cas/development/planning/Architecture.html。

CAS架构图

CAS执行逻辑:CAS分CAS Server和CAS Client。其中CAS Server用于分发和验证tickets,CAS Client从CAS Server检索授权用户的身份。

时序图参见:https://apereo.github.io/cas/development/protocol/CAS-Protocol.html。

(1)访问服务: SSO 客户端发送请求访问应用系统。

(2)定向认证: SSO 客户端重定向请求到 CAS服务器。

(3)用户认证:用户身份认证。

(4)发放票据: 成功登录后,CAS服务器向用户发放TGT(ticket-granting ticket)全局票据,创建SSO会话。Server端是TGT,Client端是TGC(Ticket Granted Cookie),类似于session和cookie。同时TGT签发一个ST返回给浏览器(如不同应用请求,发现TGC对应了一个TGT,同样会签发ST)。

(5)验证票据: CAS服务器验证票据 Service Ticket (ST)的合法性,验证通过后,允许客户端访问服务。ST作为Url中的get参数传递。

(6)传输用户信息:CAS服务器验证票据通过后,传输用户认证结果信息给客户端。

CAS执行逻辑图

二、回到正题,如何在ruoyi-vue(3.8.1)项目中集成CAS呢。由于ruoyi-vue基于spring seruity。需集成spring-security-cas依赖。另外认证要增加和CAS服务端的集成。

本文参考:RuoYi-Vue前后端分离版集成cas_GGX-520的博客-CSDN博客_ruoyi分离版cas

后端集成:

1、common模块添加对CAS的支持。

<!-- 添加spring security cas支持 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-cas</artifactId>
</dependency>

2、配置文件修改

#CAS
cas:
  server:
    host:
      #CAS服务地址
      url: http://localhost:8888/cas
      #CAS服务登录地址
      login_url: ${cas.server.host.url}/login
      #CAS服务登出地址
      logout_url: ${cas.server.host.url}/logout?service=${app.server.host.url}
# 应用访问地址
app:
  #开启cas
  casEnable: true
  server:
    host:
      url: http://localhost:${server.port}
  #应用登录地址
  login_url: /
  #应用登出地址
  logout_url: /logout
  #前端登录地址
  web_url: http://localhost/

3、修改LoginUser.java

由于CAS认证需要authorities属性,在common模块中直接new HashSet():

@Override
public Collection<? extends GrantedAuthority> getAuthorities()
{
    return new HashSet();
}

4、在common模块中修改Constants.java

/**
 * CAS登录成功后的后台标识
 */
public static final String CAS_TOKEN = "cas_token";

/**
 * CAS登录成功后的前台Cookie的Key
 */
public static final String WEB_TOKEN_KEY = "Admin-Token";

5、在framework模块config.properties下新增CasProperties.java

CasProperties代码

6、在framework模块web.service下新增CasUserDetailsService.java

CasUserDetailService代码

7、在framework模块security.handle下添加CasAuthenticationSuccessHandler.java

CasAuthenticationSuccessHandler代码

8、在framework模块config.SecurityConfig下修改SecurityConfig。(setCasServerUrlPrefix 从cas-client 3.6.0已移除)

SecurityConfig代码

前端集成:

1、修改settings.js

settings.js代码

2、修改permission.js,判断没有token时访问cas登录页面

permission.js 代码

3、修改request.js、Navbar.vue,登出后不做响应

request.js代码

Navbar.vue代码

4、修改user.js,登出后跳转到cas登出页面

user.js代码

另外CAS Server需另外搭建。地址为:https://github.com/apereo/cas/releases/

JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁

  • 锁机制存在以下问题:

(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。

(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。

独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。

一、什么是CAS

CAS,compare and swap的缩写,中文翻译成比较并交换。

CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少,”CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时 修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法 可以对该操作重新计算。

二、CAS的目的

利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。而整个J.U.C都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。

三、CAS存在的问题

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

1. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

关于ABA问题参考文档: http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html

2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

四、concurrent包的实现

由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:

  1. A线程写volatile变量,随后B线程读这个volatile变量。
  2. A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
  3. A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
  4. A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

  1. 首先,声明共享变量为volatile;
  2. 然后,使用CAS的原子条件更新来实现线程之间的同步;
  3. 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:

点登录 (SingleSign-On,SSO) ,是一种帮助用户快捷访问网络中多个站点的安全通信技术。单点登录系统基于一种安全的通信协议,该协议通过多个系统之间的用户身份信息的交换来实现单点登录。使用单点登录系统时,用户只需要登录一次,就可以访问多个系统,不需要记忆多个口令密码。

云程平台支持CAS、OAuth2、JWT三种主流的单点登录技术,客户可根据需求选择对应技术方案。

一、CAS总体架构介绍

CAS(Central Authentication Service)是 Yale大学发起的一个企业级的、开源的项目,旨在为 Web 应用系统提供一种可靠的单点登录解决方法。CAS的目标是允许用户访问多个应用程序只提供一次用户凭据(如用户名和密码)。

CAS 体系包含两个部分: CAS Server 和 CAS Client。CAS Server 需要独立部署,主要负责对用户的认证工作;CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。

CAS 具有以下特点:

  • 一个开放的,文档齐全的协议。
  • 开源的JAVA服务器组件。
  • CAS Client 支持非常多的客户端(这里指单点登录系统中的各个 Web 应用),包括 Java, .Net, PHP, Perl, Apache, uPortal, Ruby 等。
  • 文档社区化和实现的支持。
  • 具有广泛的客户群的支持。

CAS官方文档:https://apereo.github.io/cas/5.3.x/index.html#

二、CAS单点原理和集成流程

在 CAS 的整个登录过程中,有三个重要的概念。

  1. TGT:TGT 全称叫做 Ticket Granting Ticket,这个相当于我们平时所见到的 HttpSession 的作用,用户登录成功后,用户的基本信息,如用户名、登录有效期等信息,都将存储在此。
  2. TGC:TGC 全称叫做 Ticket Granting Cookie,TGC 以 Cookie 的形式保存在浏览器中,根据 TGC 可以帮助用户找到对应的 TGT,所以这个 TGC 有点类似与会话 ID。
  3. ST:ST 全称是 Service Ticket,这是 CAS Sever 通过 TGT 给用户发放的一张票据,用户在访问其他服务时,发现没有 Cookie 或者 ST ,那么就会 302 到 CAS Server 获取 ST,然后会携带着 ST 302 回来,CAS Client 则通过 ST 去 CAS Server 上获取用户的登录状态。

CAS的单点登录SSO流程如下, 应用系统要做单点登录,需要跟CAS服务进行集成,首先要理解CAS集成流程和原理。

  1. 用户通过浏览器访问应用1,应用1 发现用户没有登录,于是返回 302,并且携带上一个 service 参数,让用户去 CAS Server 上登录。
  2. 浏览器自动重定向到 CAS Server 上,CAS Server 获取用户 Cookie 中携带的 TGC,去校验用户是否已经登录,如果已经登录,则完成身份校验(此时 CAS Server 可以根据用户的 TGC 找到 TGT,进而获取用户的信息);如果未登录,则重定向到 CAS Server 的登录页面,用户输入用户名/密码,CAS Server 会生成 TGT,并且根据 TGT 签发一个 ST,再将 TGC 放在用户的 Cookie 中,完成身份校验。
  3. CAS Server 完成身份校验之后,会将 ST 拼接在 service 中,返回 302,浏览器将首先将 TGC 存在 Cookie 中,然后根据 302 的指示,携带上 ST 重定向到应用1。
  4. 应用1 收到浏览器传来的 ST 之后,拿去 CAS Server 上校验,去判断用户的登录状态,如果用户登录合法,CAS Server 就会返回用户信息给 应用1。
  5. 浏览器再去访问应用2,应用2 发现用户未登录,重定向到 CAS Server。
  6. CAS Server 发现此时用户实际上已经登录了,于是又重定向回应用2,同时携带上 ST。
  7. 应用2 拿着 ST 去 CAS Server 上校验,获取用户的登录信息。
  8. 在整个登录过程中,浏览器分别和 CAS Server、应用1、应用2 建立了会话,其中,和 CAS Server 建立的会话称之为全局会话,和应用1、应用2 建立的会话称之为局部会话;一旦局部会话成功建立,以后用户再去访问应用1、应用2 就不会经过 CAS Server 了。

三、CAS服务端如何部署

云程平台对CAS 5.3.x版本无缝集成,并对CAS认证校验进行了扩展,项目上请使用平台提供的CAS 5.3.x运行包。运行CAS之前需要在数据库先执行平台的脚本,CAS获取用户信息需访问平台的SYS_USER表。

1 修改数据库连接

打开 cas\WEB-INF\classes\application.properties

修改如下配置:

#数据库配置

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/yuncheng2021?characterEncoding=UTF-8&useUnicode=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource
spring.jpa.database=mysql
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect

2 启动cas

需要准备tomcat,把cas包放到tomcat/webapps目录下,在tomcat/bin目录下执行startup.bat(windows)或startup.sh(linux)。

启动成功后访问cas地址,界面如下图所示:

四、云程如何对接CAS

平台后端CAS配置

云程平台后端已集成CAS代码,在yml配置文件中配置cas服务地址即可。

application.yml 进行如下配置:

#cas单点登录
cas:
prefixUrl: http://cas.example.org:8443/cas

平台前端CAS配置

修改public/config/bootConfig.js

VUE_APP_SSO设置为true

VUE_APP_CAS_BASE_URL配置单点登录服务地址