作者:后端小肥肠
🍇 我写过的文章中的相关代码放到了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. 前言
2. CAS单点登录原理
2.1. 核心概念
2.2. CAS单点登录流程
3. CAS Server 部署
3.1. 配置Tomcat支持HTTPS协议
3.2. CAS Server服务器搭建
3.2.1. CAS Server war下载
3.2.2. CAS Server war发布到tomcat
3.2.3. 配置数据源,数据库用户认证
3.2.4. 密码加密校验
4. SpringSecurity 整合 CAS
4.1. SpringSecurity集成CAS单点登录认证流程
4.2. 版本依赖
4.3. 核心代码
4.3.1. 后端代码
4.3.2. 前端代码
5. 结语
6. 参考链接
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单点登录的认证流程:
- 用户访问保护资源: 用户请求访问Spring Security保护的应用资源。如果用户未经验证,Spring Security的拦截器将拦截此请求。
- 重定向到CAS登录: Spring Security配置的CAS入口点(CasAuthenticationEntryPoint)将识别到用户未登录,并重定向用户到CAS服务器的登录页面。
- 用户在CAS登录: 用户在CAS登录页面输入凭据并提交。CAS服务器验证用户凭据,如果凭据正确,CAS将用户重定向回Spring Security应用,同时附带一个票据(通常是一个
Service Ticket
)。 - 票据验证: 用户返回到应用时,带有从CAS服务器获得的票据。Spring Security现在需要验证这个票据以确保它是有效的。这一步由配置的票据验证器(如Cas30ServiceTicketValidator)完成,它将与CAS服务器通信以验证票据的有效性。
- 创建Security Context: 一旦票据被验证为有效,CAS认证提供者(CasAuthenticationProvider)将基于从CAS服务器返回的用户数据创建一个认证对象。这个认证对象被用来填充Spring Security的Security Context,从而标记用户为已认证。
- 授权与资源访问: 用户现在被认为是经过验证的,Spring Security将根据用户的权限评估用户对请求资源的访问。如果用户有权访问,请求将继续处理;如果没有访问权限,将返回一个访问拒绝的错误。
- 后续请求与单点登录: 用户在初次登录后,随后的请求通常不需要重新认证。CAS和Spring Security支持会话的创建,所以用户可以在不再次输入凭据的情况下访问其他受保护的资源。
- 登出处理: 当用户从任一应用发起登出请求时,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单点登录 - 简书