Bootstrap

CAS4.1单点登录实现(包含原理配置实现及简易demo)

CAS单点登录-简介

CAS 简介

CAS ( Central Authentication Service ) 是 Yale 大学发起的一个企业级的、开源的项目,旨在为 Web 应用系统提供一种可靠的单点登录解决方法(属于 Web SSO )。

SSO 简介

单点登录( Single Sign-On , 简称 SSO )是目前比较流行的服务于企业业务整合的解决方案之一, SSO 使得在多个应用系统中,用户只需要 登录一次 就可以访问所有相互信任的应用系统。

CAS 的基本原理

从结构体系看, CAS 包括两部分: CAS Server 和 CAS Client 。(服务端/客户端)

服务端CAS Server 负责完成对用户的认证工作 , 需要独立部署 , CAS Server 会处理用户名 / 密码等凭证(Credentials) 。

客户端CAS Client 与受保护的客户端应用部署在一起,以 Filter 方式保护受保护的资源。CAS Client 负责处理对客户端受保护资源的访问请求,需要对请求方进行身份认证时,重定向到 CAS Server 进行认证。(原则上,客户端应用不再接受任何的用户名密码等 Credentials )。

基于CAS的SSO访问流程步骤:

访问服务: CAS Client 客户端发送请求访问应用系统提供的服务资源。

定向认证: CAS Client 客户端会重定向用户请求到 CAS Server 服务器。

用户认证: 用户在浏览器端输入用户验证信息,CAS Server服务端完成用户身份认证。

发放票据: CAS Server服务器会产生一个随机的 Service Ticket 。

验证票据: CAS Server服务器验证票据 Service Ticket 的合法性,验证通过后,允许客户端访问服务。

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

下面是 CAS 最基本的协议过程:

搭建CAS Server

首先下载cas-overlay,进入cas的git项目,选择版本,下载zip包并解压。(要求至少为jdk8,tomcat 8+)

https://github.com/apereo/cas-overlay-template

本demo中使用的cas server版本为4.1(maven版本),较高版本的server(如5.3)在代码拉取后自带了脚本,可直接用于打包构建。

(5.3之后的都是gradle项目,5.3以之前都是maven 项目,build.cmd为win版自动构建+运行的脚本,sh为linux版)

下面说明不使用脚本的server构建方式。

使用idea打开项目后,在较新的版本中,cas的很多配置都放在了cas.properties里,这个文件可以放在服务器的任何位置,但是需要修改propertyFileConfigurer.xml这个配置文件的目标路径,以告诉cas系统cas.properties需要去哪个位置加载。

下述截图标明了配置文件路径,右侧需对应修改cas.properties配置文件的存放路径,demo中我将配置文件放置于E:\prop中,直接修改路径即可。

点击package将本项目打包,会生成一个上图路径target中的war文件:cas.war。该文件即打包好的cas server文件,将该war放入tomcat安装路径下的webapps文件夹中,启动tomcat,便会同时部署web应用程序cas server。

由于我的tomcat端口由8080修改为9527,当tomcat启动后,访问路径 http://localhost:9527/cas/login ,能见到cas登录页面时,说明本demo的cas服务端已安装部署成功。此时可尝试输入测试账号及密码,测试是否能实现登录功能。(静态账号casuser,密码Mellon)

也可以自己修改账号密码,在deployerConfigContext.xml文件内进行修改,如下图

至此,CAS Server的demo服务端搭建已完成。

编写CAS Client

为验证单点登录的有效性,新建立两个springboot项目,作为客户端2和客户端3。分别完成以下代码编写。当两个客户端及服务端1均启动时,若客户端2登录后,客户端3只需刷新页面即可同时处于已登录状态。

①引入CAS client依赖

在pom.xml中引入CAS Client的依赖包。代码如下:

<dependency>

<groupId>net.unicon.cas</groupId>

<artifactId>cas-client-autoconfig-support</artifactId>

<version>2.3.0-GA</version>

</dependency>


 

②配置

在application.properties或者application.yml中添加相关配置,主要配置内容包括服务器的相关地址,客户端的相关地址等。我这里是application.yml,配置内容如下:

(注意配置时的端口区分,本demo中,客户端2采用端口8890,客户端3采用端口9990)

cas:
#后端服务地址
client-host-url: http://127.0.0.1:8890
#cas认证中心地址
server-url-prefix: http://127.0.0.1:9527/cas
#cas认证中心登录地址
server-login-url: http://127.0.0.1:9527/cas/login
#Ticket校验器使用Cas30ProxyReceivingTicketValidationFilter
validation-type: cas3


 

③在启动类中添加启用注解

//启用CAS

@EnableCasClient

@SpringBootApplication

public class SpringBootSsoApplication { //省略部分内容 }

④编写测试接口Controller层

import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.util.AbstractCasFilter;
import org.jasig.cas.client.validation.Assertion;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
* Author Haozhonghao
* Date 2021/12/20 10:00
* Version 1.0
*/
@RequestMapping("/casTest2")
@Controller
public class CASTestController {
@Value(value = "${cas.server-url-prefix}")
private String serverUrlPrefix = "";
@Value(value = "${cas.client-host-url}")
private String clientHostUrl = "";

@GetMapping("/user2")
@ResponseBody
public String user(HttpServletRequest request) {
Assertion assertion = (Assertion) request.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
String loginName = null;
if (assertion != null) {
AttributePrincipal principal = assertion.getPrincipal();
loginName = principal.getName();
System.out.println("访问者2:" + loginName);
}
return "访问者2:" + loginName;
}

@GetMapping("/logout")
public String logout(HttpSession session) {
session.invalidate();
return "redirect:" + serverUrlPrefix + "/logout?service=" + clientHostUrl + "/casTest2/user2";
}

@GetMapping("/test2")
public String test() {
return "test2....";
}
}

实现CAS Client单点登录过程

测试过程:

初步测试时,开启服务端1、客户端2和客户端3。打开浏览器,输入地址http://127.0.0.1:9990/casTest3/user3,出现如下报错:(输入客户端2的接口地址同样报错)

显示错误信息:权限配置问题

  1. Application Not Authorized to Use CAS
  2. The application you attempted to authenticate toisnot authorized to use CAS.

解决办法:

修改Tomcat/webapps/cas/WEB-INF/classes/services目录下的HTTPSandIMAPS-10000001.Json文件:直接复制替换

​
{

"@class": "org.jasig.cas.services.RegexRegisteredService",

"serviceId" : "^(https|http|imaps)://.*",

"name": "https://localhost",

"id": 1,

"evaluationOrder": 0,

"logoutType": "BACK_CHANNEL",

"proxyPolicy" : {

"allowedToProxy": true,

"@class" : "org.jasig.cas.services.RegexMatchingRegisteredServiceProxyPolicy",

"pattern" : "^(https?)://localhost.*"

}

}

​

修改后重启服务端及客户端

此时访问客户端2接口地址,能够正常显示如下界面,允许输入用户名及密码,便于测试仍然使用默认账号及密码(账号:casuser,密码:Mellon)

输入相应用户名及密码后,登陆成功:

输入客户端3接口地址后访问,直接登陆成功

单点登录简易demo效果至此实现成功。为形成完整的登录登出流程,后续还需要实现cas单点登出。

注:图所显示的错误Non-secure Connection,是由于没有使用HTTPS协议的关系,而默认的登陆界面有对此进行验证的代码,而在实际项目中的登陆界面一般需要自己写,通过修改webapps\cas\WEB-INF\view\jsp\default\ui下的casLoginView.jsp即可。将下图所示代码删掉即可去除错误警告。

实现CAS Client单点登出过程

实现单点登出,需要在前文代码及配置的基础上进行。

首先需要添加服务端配置。

CAS服务端需进行相应的更改,在cas.properties中将下述配置改为true(若没有下述条目配置,则直接复制粘贴入文件中),添加配置后方可实现登出后的重定向,重定向地址为测试接口Controller层中编写的地址,当调用logout登出后,cas服务端发现该客户端不在登录状态,就会再次跳转到登录页,成功登录之后,便会再次跳转到目标接口路径。

# Specify whether CAS should redirect to the specified service parameter on /logout requests

cas.logout.followServiceRedirects=true

如果不添加此配置,就无法实现登出后的跳转,效果图如下(仅显示登出成功,而不会跳转到登录页)

其次是要注意测试接口Controller层中的注解使用。注意不要使用@RestController注解本类,而是使用@Controller注解,否则会导致重定向地址仅仅渲染在页面上,而不会实现页面跳转。

原因分析

@RestController注解相当于@ResponseBody + @Controller合在一起的作用

重定向失效即是@ResponseBody注解引起的。 @ResponseBody是作用在方法上的,@ResponseBody 表示该方法的返回结果直接写入 HTTP response body 中,一般在异步获取数据时使用【也就是AJAX】,在使用 @RequestMapping后,返回值通常解析为跳转路径,但是加上 @ResponseBody 后返回结果不会被解析为跳转路径,而是直接写入 HTTP response body 中。 比如异步获取 json 数据,加上 @ResponseBody 后,会直接返回 json 数据。@RequestBody 将 HTTP 请求正文插入方法中,使用适合的 HttpMessageConverter 将请求体写入某个对象。

不会被解析成跳转路径,那么 return "redirect:” 重定向也就失效了。

因此解决方法就是把@RestController改成@Controller注解。

解决上述两点问题之后,重新启动服务端1、客户端2、客户端3。

实现单点登录操作后,任选其中一方,访问其logout接口:

访问成功后,客户端2页面成功跳转至登录界面,回到客户端3,刷新页面,由于单点登出的效果,客户端3也处于离线状态,单点登出效果实现成功!

实际cas单点登录的应用场景,不可能只使用一个测试账号登录,因此需要实现cas与数据库相结合,实现自定义账号的单点登录登出,方有实际使用意义。

结合mysql数据库,实现CAS单点登出的客户及服务的交互过程

主要修改部分为 CAS Server的配置部分,通过在服务端配置数据源及相关验证,实现自定义用户及密码的登录替换,具体步骤及实施方式如下:

①新增cas登录的相关库表,具体库表语句如下:

# 创建测试数据库

CREATE DATABASE cas;



USE cas;



# 创建测试表

CREATE TABLE `cas_t_user` (

`id` int(11) NOT NULL AUTO_INCREMENT,

`user_name` varchar(50) DEFAULT NULL,

`password` varchar(255) DEFAULT NULL,

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8;



# 插入两条测试用户数据

INSERT INTO `cas_t_user` VALUES (1,'admin','96e79218965eb72c92a549dd5a330112');

INSERT INTO `cas_t_user` VALUES (2,'user','96e79218965eb72c92a549dd5a330112');



 

②修改配置文件内容,并新增部分配置内容

修改%tomcat_home%/webapps/cas/WEB_INF/deployerConfigContext.xml

首先注释掉下述代码(注释默认用户名和密码)

<!-- 配置数据库验证bean,注释原有handler,取消默认用户名及密码-->
<!-- <bean id="primaryAuthenticationHandler"-->
<!-- class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">-->
<!-- <property name="users">-->
<!-- <map>-->
<!-- <entry key="casuser" value="Mellon"/>-->
<!-- </map>-->
<!-- </property>-->
<!-- </bean>-->

添加下述代码,MD5密码验证,数据库配置,认证类配置

<!-- 配置passwordEncoder -->
<bean id="MD5PasswordEncoder"
class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder">
<constructor-arg index="0" value="MD5"/>
</bean>

<!-- 添加数据源 -->
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/cas?characterEncoding=UTF-8&amp;autoReconnect=true"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>

<!-- 配置认证类 -->
<bean id="primaryAuthenticationHandler" class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
<!--dataSource指向上面配置的dataSource bean-->
<property name="dataSource" ref="dataSource"></property>
<property name="sql" value="select password from cas_t_user where user_name=?"></property>
<!--passwordEncoder 指向上面配置的 passwordEncoder bean-->
<property name="passwordEncoder" ref="MD5PasswordEncoder"></property>
</bean>

③添加相关jar包

  • 目录:apache-tomcat-8.5.73\webapps\cas\WEB-INF\lib
  • 在上面目录下添加以下三个jar包:

mysql-connector-java-5.1.46.jar

cas-server-support-jdbc-4.1.0.jar

c3p0-0.9.5.5.jar

(最开始使用了cas-server-support-jdbc-4.0.0.jar,发现输入正确的用户及密码时,无法实现正常页面跳转,且密码栏会被清空,切换为与本demo中的casServer版本完全匹配的jar后此问题被修复)

重启tomcat的服务端1,重启客户端2、客户端3进行测试

输入admin/111111

  • 其中用户的密码为MD5之后的值
  • “111111”的MD5值为:“96e79218965eb72c92a549dd5a330112”

效果如下图

可见,此时我们自己在数据库中添加的用户可以实现单点登录及单点登出。

客户端采用cas-client-core 3.5.0版本依赖,实现cas单点登录登出效果

前述文中阐述了使用模板依赖cas-client-autoconfig-support 2.3.0-GA的客户端实现方式:

<dependency>

<groupId>net.unicon.cas</groupId>

<artifactId>cas-client-autoconfig-support</artifactId>

<version>2.3.0-GA</version>

</dependency>

而实际上,cas-client的客户端接入方式不止一种,下面将简述另一种客户端接入方式:采用cas-client-core包的客户端接入。

首先引入依赖:

<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.5.0</version>
</dependency>

与前文 SpringBoot 集成 CAS Client 略有不同,本集成不需要在 SpringBoot 启动类上加入启用 CAS Client 的 @EnableCasClient 注解。如果加了这个注解, CAS Client是不能实现根据配置文件开关的,每次想要关掉,必须修改代码,注释调注解才行。

然后编写配置类:

import lombok.extern.slf4j.Slf4j;
import org.jasig.cas.client.authentication.AuthenticationFilter;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.jasig.cas.client.util.HttpServletRequestWrapperFilter;
import org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;

/**
* Author Haozhonghao
* Date 2022/1/10 10:34
* Version 1.0
* CAS集成核心配置类
*/
@Configuration
@Slf4j
@ConditionalOnProperty(value = "cas.loginType", havingValue = "cas")
public class CasFilterConfig {

/**
* 需要走cas拦截的地址(/* 所有地址都拦截)
*/
@Value("${cas.urlPattern}")
private String filterUrl;

/**
* 默认的cas地址,防止通过 配置信息获取不到
*/
@Value("${cas.server-url-prefix}")
private String casServerUrl;

/**
* 应用访问地址(这个地址需要在cas服务端进行配置)
*/
@Value("${cas.authentication-url}")
private String authenticationUrl;

/**
* 应用访问地址(这个地址需要在cas服务端进行配置)
*/
@Value("${cas.client-host-url}")
private String appServerUrl;

@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean() {
log.info(" \n cas 单点登录配置 \n appServerUrl = " + appServerUrl + "\n casServerUrl = " + casServerUrl);
log.info(" servletListenerRegistrationBean ");
ServletListenerRegistrationBean listenerRegistrationBean = new ServletListenerRegistrationBean();
listenerRegistrationBean.setListener(new SingleSignOutHttpSessionListener());
listenerRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return listenerRegistrationBean;
}

/**
* 单点登录退出
*/
@Bean
public FilterRegistrationBean singleSignOutFilter() {
log.info(" servletListenerRegistrationBean ");
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new SingleSignOutFilter());
registrationBean.addUrlPatterns(filterUrl);
registrationBean.addInitParameter("casServerUrlPrefix", casServerUrl);
registrationBean.setName("CAS Single Sign Out Filter");
registrationBean.setOrder(1);
return registrationBean;
}

/**
* 单点登录认证
*/
@Bean
public FilterRegistrationBean AuthenticationFilter() {
log.info(" AuthenticationFilter ");
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new AuthenticationFilter());
registrationBean.addUrlPatterns(filterUrl);
registrationBean.setName("CAS Filter");
registrationBean.addInitParameter("casServerLoginUrl", casServerUrl);
registrationBean.addInitParameter("serverName", appServerUrl);
registrationBean.setOrder(1);
return registrationBean;
}

/**
* 单点登录校验
*/
@Bean
public FilterRegistrationBean Cas30ProxyReceivingTicketValidationFilter() {
log.info(" Cas30ProxyReceivingTicketValidationFilter ");
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new Cas30ProxyReceivingTicketValidationFilter());
registrationBean.addUrlPatterns(filterUrl);
registrationBean.setName("CAS Validation Filter");
registrationBean.addInitParameter("casServerUrlPrefix", authenticationUrl);
registrationBean.addInitParameter("serverName", appServerUrl);
registrationBean.setOrder(1);
return registrationBean;
}

/**
* 单点登录请求包装
*/
@Bean
public FilterRegistrationBean httpServletRequestWrapperFilter() {
log.info(" httpServletRequestWrapperFilter ");
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new HttpServletRequestWrapperFilter());
registrationBean.addUrlPatterns(filterUrl);
registrationBean.setName("CAS HttpServletRequest Wrapper Filter");
registrationBean.setOrder(1);
return registrationBean;
}
}

接着,编写yml配置文件:

server:
port: 9797
cas:
# 认证中心登录页面地址
server-url-prefix: http://127.0.0.1:9527/cas/login
# 应用地址,也就是自己的系统地址。
client-host-url: http://127.0.0.1:9797
# 认证中心地址
authentication-url: http://127.0.0.1:9527/cas
# 动态开启 cas 单点登录
loginType: cas
# cas 验票拦截路径
urlPattern: /*

最后编写测试接口Controller层:

import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.util.AbstractCasFilter;
import org.jasig.cas.client.validation.Assertion;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
* Author Haozhonghao
* Date 2022/1/10 10:45
* Version 1.0
*/
@RequestMapping("/casTest5")
@Controller
public class CASDemoController {
/**
* cas 单点登录
*/
@Value(value = "${cas.server-url-prefix}")
private String serverUrlPrefix = "";
@Value(value = "${cas.client-host-url}")
private String clientHostUrl = "";

@GetMapping("/user5")
@ResponseBody
public String user(HttpServletRequest request) {
Assertion assertion = (Assertion) request.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
String loginName = null;
if (assertion != null) {
AttributePrincipal principal = assertion.getPrincipal();
loginName = principal.getName();
System.out.println("访问者5:" + loginName);
}
return "访问者5:" + loginName;
}

@GetMapping("/logout")
public String logout(HttpSession session) {
//将session设置为失效
session.invalidate();
return "redirect:" + serverUrlPrefix + "/logout?service=" + clientHostUrl + "/casTest5/user5";
}
}

进行测试,cas-client-core3.5.0版本比较稳定,可以兼容多个版本的 CAS Server,包括前文我们在本地构建的cas-server-4.1.0。

登录,输入admin/111111,登录效果如图,实现完成。

cas客户端demo至此功能已全部实现。

;