0


【Spring Security系列】10分钟实现 SpringSecurity + CAS 完美单点登录方案

** 作者:后端小肥肠**

🍇 我写过的文章中的相关代码放到了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 的单点登录流程主要包括以下步骤:

  1. 用户访问应用系统:用户首先访问需要登录的应用系统,该应用系统发现用户尚未登录,于是将用户重定向到 CAS 认证中心。
  2. 用户登录 CAS 认证中心:用户在 CAS 认证中心输入用户名和密码进行登录。CAS 认证中心验证用户的身份信息,通过验证后,生成一个 **TGT(Ticket Granting Ticket)用于标识用户的登录状态,并生成一个ST**(Service Ticket)来授权用户访问目标应用系统。
  3. CAS 将用户重定向回应用系统:在用户登录成功后,CAS 认证中心将用户重定向回应用系统,并携带 ST 作为参数。
  4. 应用系统验证 Service Ticket:应用系统接收到 ST 后,将其发送到 CAS 认证中心进行验证。如果验证通过,应用系统认为用户已通过认证,从而允许用户访问系统资源。应用系统还会在本地创建用户的会话,方便用户的后续访问。
  5. 其他应用系统单点登录:当用户登录一个应用系统后,再次访问其他已配置为 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目录修改配置文件:

![](https://i-blog.csdnimg.cn/direct/9fed434b371340e0a53c441e3448ca7f.png)

**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包:

![](https://i-blog.csdnimg.cn/direct/9a24b6d00d394c7588c154c2e7c54ca4.png)

#### 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


输入用户名和密码后进入这个界面就是配置成功了:

![](https://i-blog.csdnimg.cn/direct/1e181cedeb264384ae0b527ae4871d48.png)

### 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)认证。下面详细解释这个配置类中的核心代码及其功能:

  1. 通过 CasAuthenticationEntryPoint配置了CAS服务的登录URL,并通过设置服务属性定义了如何与CAS服务器进行交互。
  2. ServiceProperties设置服务的回调URL,即CAS登录成功后重定向到客户端的地址,并确保所有artifact都经过认证。
  3. CasAuthenticationFilter设置认证管理器处理认证过程,并配置成功和失败的处理器,定制认证成功或失败后的行为。
  4. CasAuthenticationProvider 配置了用来加载用户特定数据的 AuthenticationUserDetailsService和票据验证器 Cas20ServiceTicketValidator 用于校验从CAS服务器返回的票据。
  5. SingleSignOutFilter用于处理CAS单点登出,保证从CAS服务登出时客户端会话也能同步失效。
  6. LogoutFilter定义了CAS服务的登出过程,确保正确重定向到CAS服务器进行登出。

后端的集成就完成了,是不是很简单。

4.3.2. 前端代码

前端代码我就不贴了(前端不熟),结尾两个参考文章都有前端代码。

5. 结语

通过本文的示例,我们成功完成了 Spring Security 集成 CAS 单点登录的基础配置,实现了用户在多应用间无缝切换的认证体验。Spring Security 与 CAS 的结合不仅提升了系统的安全性和扩展性,也简化了复杂应用环境下的用户管理流程。这种集成方案对于分布式系统或微服务架构尤其适用。

下一期,我们将进一步探讨进阶版内容,涵盖更复杂的应用场景——将小程序的登录认证集成到这个单点登录系统中。届时,我们将深入讲解如何让移动端用户通过小程序完成统一的身份认证,敬请期待!

6. 参考链接

Spring Security整合CAS_spring-security-cas-CSDN博客

SprinBoot(SpringSecurity)+前后端分离 集成CAS单点登录 - 简书


本文转载自: https://blog.csdn.net/c18213590220/article/details/143674721
版权归原作者 后端小肥肠 所有, 如有侵权,请联系我们删除。

“【Spring Security系列】10分钟实现 SpringSecurity + CAS 完美单点登录方案”的评论:

还没有评论