** 作者:后端小肥肠**
🍇 我写过的文章中的相关代码放到了gitee,地址:xfc-fdw-cloud: 公共解决方案
🍊 有疑问可私信或评论区联系我。
🥑 创作不易未经允许严禁转载。
姊妹篇:
【Spring Security系列】如何用Spring Security集成手机验证码登录?五分钟搞定!_springsecurity短信验证码登录-CSDN博客
【Spring Security系列】基于Spring Security实现权限动态分配之菜单-角色分配及动态鉴权实践_spring secrity权限角色动态管理-CSDN博客
【Spring Security系列】基于Spring Security实现权限动态分配之用户-角色分配_spring security 角色-CSDN博客
【Spring Security系列】权限之旅:SpringSecurity小程序登录深度探索_spring security 微信小程序登录-CSDN博客
【Spring Security系列】Spring Security+JWT+Redis实现用户认证登录及登出_spring security jwt 退出登录-CSDN博客
1. 前言
在现代分布式系统和微服务架构中,单点登录(Single Sign-On, SSO)已经成为用户身份管理中的一个重要组成部分。随着系统规模的扩大,用户往往需要在不同的应用系统之间频繁切换,而无须每次都进行身份验证。为了实现这种便捷的用户体验,SSO 技术应运而生。
CAS(Central Authentication Service)作为一种常用的单点登录解决方案,因其开源、易于扩展、以及与多个认证框架的良好兼容性,得到了广泛的应用。而在企业级开发中,Spring Security 则是保护 Java 应用安全性的不二选择。因此,如何将 CAS 和 Spring Security 有效整合,实现一个灵活、稳定的单点登录系统,是每个 Java 开发者都值得深入探讨的课题。
在本文中,我们将通过简单易懂的步骤,带领大家完成一个基于Spring Security + CAS的单点登录方案实现。无论您是初次接触 SSO,还是有一定经验的开发者,都能通过本篇教程快速掌握 CAS 的基本原理、部署方式,以及如何通过 Spring Security 实现应用的统一认证。同时,我们还会进行功能测试,确保单点登录效果满足预期。
2. CAS单点登录原理
在深入实施 CAS 和 Spring Security 整合之前,了解 CAS 的工作原理非常重要。CAS(Central Authentication Service)是一种集中式认证服务,它的主要作用是通过提供一个统一的认证中心,实现多个应用系统间的单点登录功能。在这个过程中,用户只需要在 CAS 认证中心登录一次,即可访问所有已授权的应用系统。
2.1. 核心概念
在理解 CAS 的单点登录原理时,有几个核心概念需要熟悉:
- **TGT (Ticket Granting Ticket)**:由 CAS 认证中心生成并存储在 CAS 服务器上的票据,用于标识用户的登录状态。用户成功登录后,TGT 会保存在用户的浏览器中,以便后续在访问其他应用系统时可以直接获取
ST。 - **ST (Service Ticket)**:在用户访问具体的应用系统时,由 CAS 认证中心生成的临时票据,标识用户对该应用系统的访问权限。
ST一次性有效,用于实现一次认证即授权。 - **LT (Login Ticket)**:在用户登录 CAS 认证中心时生成的临时票据,用于防止表单重复提交。
- **Proxy Ticket (PT)**:在特定场景下,允许一个应用代表用户访问其他服务,此时会使用代理票据机制。
2.2. CAS单点登录流程

CAS 的单点登录流程主要包括以下步骤:
- 用户访问应用系统:用户首先访问需要登录的应用系统,该应用系统发现用户尚未登录,于是将用户重定向到 CAS 认证中心。
- 用户登录 CAS 认证中心:用户在 CAS 认证中心输入用户名和密码进行登录。CAS 认证中心验证用户的身份信息,通过验证后,生成一个 **
TGT(Ticket Granting Ticket)用于标识用户的登录状态,并生成一个ST**(Service Ticket)来授权用户访问目标应用系统。 - CAS 将用户重定向回应用系统:在用户登录成功后,CAS 认证中心将用户重定向回应用系统,并携带
ST作为参数。 - 应用系统验证 Service Ticket:应用系统接收到
ST后,将其发送到 CAS 认证中心进行验证。如果验证通过,应用系统认为用户已通过认证,从而允许用户访问系统资源。应用系统还会在本地创建用户的会话,方便用户的后续访问。 - 其他应用系统单点登录:当用户登录一个应用系统后,再次访问其他已配置为 SSO 的应用时,应用系统会自动通过 CAS 认证,获取到用户的身份信息,而不再需要用户重新登录。
3. CAS Server 部署
3.1. 配置Tomcat支持HTTPS协议
1. 生成秘钥库
我们可以使用 JDK 自带的 **
keytool
**工具生成密钥库。首先,指定别名为
xfc
(可以自定义别名),然后将密钥库存储在路径**
D:\cas\keystore
**中。执行以下命令即可生成密钥库:
keytool -genkey -v -alias xfc -keyalg RSA -keystore D:\cas\keystore\xfc.keystore
这条命令将生成一个别名为**
xfc
** 的 RSA 加密密钥库,并将其保存在指定路径。

执行完毕会生成以下文件:

2. 从秘钥库里导出证书
基于生产的密钥库,我们需要从密钥库中导出证书,可以使用
keytool
工具执行以下命令:
keytool -export -trustcacerts -alias xfc -file D:/cas/keystore/xfc.cer -keystore D:/cas/keystore/xfc.keystore
在执行此命令时,需要输入在第一步创建密钥库时设置的密码,例如:**
666
**。该命令会从路径 **
D:/cas/keystore/xfc.keystore
中的密钥库导出别名为 xfc**的证书,并将其保存为 **
D:/cas/keystore/xfc.cer
**文件。

3. 将证书导入到JDK证书库
接下来就是将证书导入到 JDK 的证书库中,可以使用以下** keytool **命令:
keytool -import -trustcacerts -alias xfc -file D:/cas/keystore/xfc.cer -keystore "F:/environment/java/jre1.8.0_301/lib/security/cacerts"
执行该命令时,指定证书的别名为 **
xfc
**,将位于 **
D:/cas/keystore/xfc.cer
** 的证书文件导入到 JDK 的信任证书库 **
cacerts
**中,路径为 **
F:/environment/java/jre1.8.0_301/lib/security/cacerts
。导入过程中可能会提示输入密码,默认密码通常为
changeit。
**
4. tomcat配置https支持
为了在 Tomcat 9 中配置 HTTPS 支持,可以按照以下步骤进行。
首先,找到 Tomcat 安装目录中的 **
conf
**文件夹,并找到 **
server.xml
**文件。打开该文件,并在其中添加以下配置来启用 HTTPS 支持:
<Connector port="8445" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="150" SSLEnabled="true" scheme="https" secure="true" clientAuth="false" sslProtocol="TLS" keystoreFile="D:\cas\keystore\xfc.keystore" keystorePass="666666" />
这段配置将 HTTPS 连接器绑定到 8443 端口,并指向了之前生成的密钥库文件**
D:\cas\keystore\xfc.keystore
,密码为
666666
**。
配置完成后,可以启动 Tomcat 的**
bin
目录下的
startup.bat
**文件来运行 Tomcat。
如果在启动过程中遇到控制台输出中文乱码的情况,可以进入 Tomcat 目录下的 **
conf
** 文件夹,找到一个名为**
logging.properties
** 的文件,打开该文件,找到以下配置项:
java.util.logging.ConsoleHandler.encoding = UTF-8
将 **
UTF-8
修改为
GBK
**,使修改后的配置如下:
java.util.logging.ConsoleHandler.encoding = GBK
保存文件后,重启 Tomcat,即可解决中文乱码的问题。
5. 打开tomcat网址

到这步Tomcat支持 Https协议就配置成功了。
3.2. CAS Server服务器搭建
3.2.1. CAS Server war下载
在本文中我们需要下载cas-server-webapp-tomcat-5.3.14:
Central Repository: org/apereo/cas/cas-server-webapp-tomcat/5.3.14

3.2.2. CAS Server war发布到tomcat
把war包放tomcat下,启动tomcat会自动解压,我们把名称改成cas,方便访问:

访问https://xfc.com:8445/cas/login(这个根据你自己的配置)

3.2.3. 配置数据源,数据库用户认证
1. 新建数据表:
```sql
CREATE DATABASE `db_sso`;
USE `db_sso`;
/* Table structure for table `t_cas` */
DROP TABLE IF EXISTS `t_cas`;
CREATE TABLE `t_cas` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(30) DEFAULT NULL,
`password` VARCHAR(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
/* Data for the table `t_cas` */
INSERT INTO `t_cas` (`id`, `username`, `password`) VALUES (1, 'xfc', '123456');
**2. 修改CAS Server的application.properties配置文件**
进入cas目录修改配置文件:

**3. 注释写死的用户名和密码,加上jdbc数据源配置:**
cas.authn.accept.users=casuser::Mellon
cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQL5Dialect
cas.authn.jdbc.query[0].url=jdbc:mysql://localhost:3306/db_sso?serverTimezone=GMT
cas.authn.jdbc.query[0].user=root
cas.authn.jdbc.query[0].password=123456
cas.authn.jdbc.query[0].sql=select * from t_cas where username=?
cas.authn.jdbc.query[0].fieldPassword=password
cas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Driver
**4. 加上jdbc驱动及相关支持jar包**
进入F:\environment\apache-tomcat-9.0.53\webapps\cas\WEB-INF\lib(你自己cas的目录),粘贴以下jar包:

#### 3.2.4. 密码加密校验
**1. 将原数据库密码进行MD5加密**
SELECT MD5('123456');
**2. 修改CAS Server的application.properties配置文件**
在配置文件末尾加上:
cas.authn.jdbc.query[0].passwordEncoder.type=DEFAULT
cas.authn.jdbc.query[0].passwordEncoder.characterEncoding=UTF-8
#MD5加密策略
cas.authn.jdbc.query[0].passwordEncoder.encodingAlgorithm=MD5
输入用户名和密码后进入这个界面就是配置成功了:

### 4. SpringSecurity 整合 CAS
#### 4.1. SpringSecurity集成CAS单点登录认证流程
**以下为SpringSecurity集成CAS单点登录的认证流程:**
1. **用户访问保护资源**: 用户请求访问Spring Security保护的应用资源。如果用户未经验证,Spring Security的拦截器将拦截此请求。
2. **重定向到CAS登录**: Spring Security配置的CAS入口点(**CasAuthenticationEntryPoint**)将识别到用户未登录,并重定向用户到CAS服务器的登录页面。
3. **用户在CAS登录**: 用户在CAS登录页面输入凭据并提交。CAS服务器验证用户凭据,如果凭据正确,CAS将用户重定向回Spring Security应用,同时附带一个票据(通常是一个```Service Ticket```)。
4. **票据验证**: 用户返回到应用时,带有从CAS服务器获得的票据。Spring Security现在需要验证这个票据以确保它是有效的。这一步由配置的票据验证器(**如Cas30ServiceTicketValidator**)完成,它将与CAS服务器通信以验证票据的有效性。
5. **创建Security Context**: 一旦票据被验证为有效,CAS认证提供者(**CasAuthenticationProvider**)将基于从CAS服务器返回的用户数据创建一个认证对象。这个认证对象被用来填充Spring Security的Security Context,从而标记用户为已认证。
6. **授权与资源访问**: 用户现在被认为是经过验证的,Spring Security将根据用户的权限评估用户对请求资源的访问。如果用户有权访问,请求将继续处理;如果没有访问权限,将返回一个访问拒绝的错误。
7. **后续请求与单点登录**: 用户在初次登录后,随后的请求通常不需要重新认证。CAS和Spring Security支持会话的创建,所以用户可以在不再次输入凭据的情况下访问其他受保护的资源。
8. **登出处理**: 当用户从任一应用发起登出请求时,Spring Security将确保从应用和CAS服务器上都清除会话。通常,这也会导致所有使用CAS进行单点登录的其他应用会话被终止。
通过这个流程,Spring Security利用CAS实现了一个安全的单点登录解决方案,允许用户在多个相互信任的应用间无缝地进行身份认证和授权。
#### 4.2. 版本依赖
SpringSecurity集成CAS的Maven依赖如下(**SpringBoot版本为2.6.3**):
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
<version>5.6.3</version>
</dependency>
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.6.4</version>
</dependency>
**版本很重要,如果版本不匹配会报错!! **
#### 4.3. 核心代码
##### 4.3.1. 后端代码
**yml配置:**
server:
servlet:
context-path: "/test"
port: 7888
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/cas?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
cas:
server: https://java1234.com:8443/cas
client: http://localhost:7888/test
**编写CustomUserDetailsService用于从cas令牌中加载用户信息:**
@Service
public class CustomUserDetailsService implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> {
@Autowired
private ISysUserService userService;
@Autowired
private ISysRoleService roleService;
@Override
public UserDetails loadUserDetails(CasAssertionAuthenticationToken token)
throws UsernameNotFoundException {
SysUser user = userService.getUserByUserName(token.getName());
if (Objects.isNull(user)) {
throw new RuntimeException("用户不存在");
}
List<String> roles = roleService.getRolesByUserName(token.getName());
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
return new AuthUser(user.getUsername(), user.getPassword(), user.getIsEnabled(), authorities);
}
}
这段代码定义了一个服务**
CustomUserDetailsService
**,它实现了用于Spring Security的**
AuthenticationUserDetailsService
**接口,专门处理通过CAS单点登录系统验证后的用户。该服务通过从CAS认证令牌获取用户名,调用用户服务以检索用户详细信息,从角色服务获取用户角色,并将这些角色转换为Spring Security需要的权限格式,最后创建并返回一个包含用户凭据和权限的**
UserDetails
**对象供Spring Security进一步处理认证和授权。
**编写WebSecurityConfigurer:**
package com.xfc.auth.configuration.auth;
import com.xfc.auth.service.CustomUserDetailsService;
import lombok.extern.slf4j.Slf4j;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
@Slf4j
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
// 配置文件中的CAS服务器地址
@Value("${cas.server}")
private String casServerUrl;
// 配置文件中的本应用前端地址
@Value("${cas.client}")
private String casClientUrl;
private static final String[] PERMIT_URL = new String[]{"/login/cas", "/logout/cas"};
@Autowired
CustomUserDetailsService customUserDetailsService;
/**
* SpringSecurity配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 配置接口过滤网,放行/login/cas用于单点登录的验证
.authorizeRequests()
.antMatchers(PERMIT_URL).permitAll()
.anyRequest().authenticated()
.and().httpBasic()
// 配置自定义的用户认证入口类(用于处理未登录或登录超时的逻辑)
.authenticationEntryPoint(casAuthenticationEntryPoint())
.and()
// 配置自定义的CAS用户认证入口类
.addFilter(casAuthenticationFilter())
// 配置CAS需要用到的其他类
.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class)
.addFilterBefore(casLogoutFilter(), LogoutFilter.class)
// 禁用CORS
// 禁用CSRF
.csrf().disable();
}
/**
* CAS配置(AuthenticationProvider)
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
auth.authenticationProvider(casAuthenticationProvider());
}
/**
* CAS:认证入口
*/
@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
casAuthenticationEntryPoint.setLoginUrl(casServerUrl + "/login");
casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
return casAuthenticationEntryPoint;
}
/**
* CAS:服务配置
*/
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
// 此处填入前端登录页面的地址
serviceProperties.setService(casClientUrl + "/#/login/cas");
serviceProperties.setAuthenticateAllArtifacts(true);
return serviceProperties;
}
/**
* CAS:配置自定义的CAS用户认证入口类
*/
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
casAuthenticationFilter.setFilterProcessesUrl("/login/cas");
casAuthenticationFilter.setServiceProperties(serviceProperties());
// 重要:此处为配置ticket验证成功后的逻辑,默认为重定向到首页,因前后端分离,仅需要返回成功即可。
casAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = response.getWriter();
out.write("{\"status\":" + "\"200\"" + "}");
});
casAuthenticationFilter.setAuthenticationFailureHandler((request, response, e) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
out.write("{\"code\":401" + ",\"message\":\"Ticket verified failed!\"}");
log.error("单点登录验证失败", e);
});
return casAuthenticationFilter;
}
/**
* CAS:CAS的核心,CasAuthenticationProvider
*/
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setAuthenticationUserDetailsService(customUserDetailsService());
casAuthenticationProvider.setServiceProperties(serviceProperties());
casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
casAuthenticationProvider.setKey("EXAMPLE_CAS_PROVIDER");
return casAuthenticationProvider;
}
/**
* CAS:自定义的用户认证入口类(用于处理未登录或登录超时的逻辑)
*/
@Bean
public AuthenticationUserDetailsService<CasAssertionAuthenticationToken> customUserDetailsService() {
return customUserDetailsService;
}
/**
* CAS:ticket验证类
*/
@Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
return new Cas20ServiceTicketValidator(casServerUrl);
}
/**
* CAS:SingleSignOutFilter
*/
@Bean
public SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
singleSignOutFilter.setIgnoreInitConfiguration(true);
return singleSignOutFilter;
}
/**
* CAS:LogoutFilter
*/
@Bean
public LogoutFilter casLogoutFilter() {
LogoutFilter logoutFilter = new LogoutFilter(casServerUrl + "/logout?service=" + casClientUrl,
new SecurityContextLogoutHandler());
logoutFilter.setFilterProcessesUrl("/logout/cas");
return logoutFilter;
}
}
```
这个类是一个配置类,它使用CAS来进行单点登录(SSO)认证。下面详细解释这个配置类中的核心代码及其功能:
- 通过 CasAuthenticationEntryPoint配置了CAS服务的登录URL,并通过设置服务属性定义了如何与CAS服务器进行交互。
- ServiceProperties设置服务的回调URL,即CAS登录成功后重定向到客户端的地址,并确保所有artifact都经过认证。
- CasAuthenticationFilter设置认证管理器处理认证过程,并配置成功和失败的处理器,定制认证成功或失败后的行为。
- CasAuthenticationProvider 配置了用来加载用户特定数据的 AuthenticationUserDetailsService和票据验证器 Cas20ServiceTicketValidator 用于校验从CAS服务器返回的票据。
- SingleSignOutFilter用于处理CAS单点登出,保证从CAS服务登出时客户端会话也能同步失效。
- LogoutFilter定义了CAS服务的登出过程,确保正确重定向到CAS服务器进行登出。
后端的集成就完成了,是不是很简单。
4.3.2. 前端代码
前端代码我就不贴了(前端不熟),结尾两个参考文章都有前端代码。
5. 结语
通过本文的示例,我们成功完成了 Spring Security 集成 CAS 单点登录的基础配置,实现了用户在多应用间无缝切换的认证体验。Spring Security 与 CAS 的结合不仅提升了系统的安全性和扩展性,也简化了复杂应用环境下的用户管理流程。这种集成方案对于分布式系统或微服务架构尤其适用。
下一期,我们将进一步探讨进阶版内容,涵盖更复杂的应用场景——将小程序的登录认证集成到这个单点登录系统中。届时,我们将深入讲解如何让移动端用户通过小程序完成统一的身份认证,敬请期待!

6. 参考链接
Spring Security整合CAS_spring-security-cas-CSDN博客
SprinBoot(SpringSecurity)+前后端分离 集成CAS单点登录 - 简书
版权归原作者 后端小肥肠 所有, 如有侵权,请联系我们删除。