1. 概述

本教程将深入探讨 Apereo 中央认证服务(CAS),并演示如何让 Spring Boot 服务利用它进行身份认证。CAS 是一个开源的企业级单点登录(SSO)解决方案。

2. CAS 服务器搭建

2.1. CAS 安装与依赖

服务器采用 Maven(Gradle)War Overlay 方式简化部署:

git clone https://github.com/apereo/cas-overlay-template.git cas-server

此命令会将 cas-overlay-template 克隆到 cas-server 目录。

我们将涉及 JSON 服务注册和 JDBC 数据库连接,需在 build.gradledependencies 部分添加:

compile "org.apereo.cas:cas-server-support-json-service-registry:${casServerVersion}"
compile "org.apereo.cas:cas-server-support-jdbc:${casServerVersion}"

请务必检查 casServer 的最新版本。

2.2. CAS 服务器配置

启动 CAS 服务器前需添加基础配置。创建 cas-server/src/main/resources 文件夹,并在其中创建 application.properties

server.port=8443
spring.main.allow-bean-definition-overriding=true
server.ssl.key-store=classpath:/etc/cas/thekeystore
server.ssl.key-store-password=changeit

生成密钥库(避免 SSL 握手错误时使用 localhost 作为名称):

keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore -storepass changeit -validity 360 -keysize 2048

将密钥库导入 JDK/JRE:

keytool -importkeystore -srckeystore thekeystore -destkeystore $JAVA11_HOME/jre/lib/security/cacerts

启动服务器:

./gradlew[.bat] run -Dorg.gradle.java.home=$JAVA11_HOME

**启动成功后终端会显示 "READY"**,服务器可通过 https://localhost:8443 访问。

2.3. CAS 用户配置

尚未配置用户无法登录。CAS 支持多种配置管理方式,这里使用独立模式。创建配置文件夹 cas-server/src/main/resources/etc/cas/config,并在其中添加 cas.properties

cas.authn.accept.users=casuser::Mellon

需告知 CAS 配置文件位置。修改 tasks.gradle 以支持命令行参数传递:

task run(group: "build", description: "Run the CAS web application in embedded container mode") {
    dependsOn 'build'
    doLast {
        def casRunArgs = new ArrayList<>(Arrays.asList(
          "-server -noverify -Xmx2048M -XX:+TieredCompilation -XX:TieredStopAtLevel=1".split(" ")))
        if (project.hasProperty('args')) {
            casRunArgs.addAll(project.args.split('\\s+'))
        }
        javaexec {
            main = "-jar"
            jvmArgs = casRunArgs
            args = ["build/libs/${casWebApplicationBinaryName}"]
            logger.info "Started ${commandLine}"
        }
    }
}

运行命令:

./gradlew run
  -Dorg.gradle.java.home=$JAVA11_HOME
  -Pargs="-Dcas.standalone.configurationDirectory=/cas-server/src/main/resources/etc/cas/config"

⚠️ 注意 cas.standalone.configurationDirectory 必须使用绝对路径。现在可通过 https://localhost:8443 使用用户名 casuser 和密码 Mellon 登录。

3. CAS 客户端搭建

使用 Spring Initializr 生成 Spring Boot 客户端应用,添加 WebSecurityFreemarkerDevTools 依赖。在 pom.xml 中额外添加 Spring Security CAS 依赖:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-cas</artifactId>
    <versionId>5.3.0.RELEASE</versionId>
</dependency>

配置应用属性:

server.port=8900
spring.freemarker.suffix=.ftl

4. CAS 服务注册

客户端应用必须预先在 CAS 服务器注册。CAS 支持 YAML、JSON、MongoDB 和 LDAP 等注册方式。

本教程使用 JSON Service Registry。创建文件夹 cas-server/src/main/resources/etc/cas/services 用于存放注册文件。

创建客户端定义文件 casSecuredApp-8900.json(命名规则:serviceName-Id.json):

{
  "@class" : "org.apereo.cas.services.RegexRegisteredService",
  "serviceId" : "http://localhost:8900/login/cas",
  "name" : "casSecuredApp",
  "id" : 8900,
  "logoutType" : "BACK_CHANNEL",
  "logoutUrl" : "http://localhost:8900/exit/cas"
}

关键配置说明:

  • serviceId:定义客户端 URL 的正则模式
  • id:必须唯一,重复会导致配置冲突

启用 JSON 注册(在 application.properties 添加):

cas.serviceRegistry.initFromJson=true
cas.serviceRegistry.json.location=classpath:/etc/cas/services

5. 客户端单点登录配置

接下来配置 Spring Security 与 CAS 服务器协作。建议先了解完整的CAS 交互流程

在 Spring Boot 应用的 CasSecuredApplication 类中添加以下 Bean:

@Bean
public CasAuthenticationFilter casAuthenticationFilter(
  AuthenticationManager authenticationManager,
  ServiceProperties serviceProperties) throws Exception {
    CasAuthenticationFilter filter = new CasAuthenticationFilter();
    filter.setAuthenticationManager(authenticationManager);
    filter.setServiceProperties(serviceProperties);
    return filter;
}

@Bean
public ServiceProperties serviceProperties() {
    logger.info("service properties");
    ServiceProperties serviceProperties = new ServiceProperties();
    serviceProperties.setService("http://cas-client:8900/login/cas");
    serviceProperties.setSendRenew(false);
    return serviceProperties;
}

@Bean
public TicketValidator ticketValidator() {
    return new Cas30ServiceTicketValidator("https://localhost:8443");
}

@Bean
public CasAuthenticationProvider casAuthenticationProvider(
  TicketValidator ticketValidator,
  ServiceProperties serviceProperties) {
    CasAuthenticationProvider provider = new CasAuthenticationProvider();
    provider.setServiceProperties(serviceProperties);
    provider.setTicketValidator(ticketValidator);
    provider.setUserDetailsService(
      s -> new User("[email protected]", "Mellon", true, true, true, true,
      AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
    provider.setKey("CAS_PROVIDER_LOCALHOST_8900");
    return provider;
}

WebSecurityConfig 中配置安全规则和认证入口点:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().antMatchers( "/secured", "/login").authenticated()
      .and()
      .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())
      .and()
      .addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class)
}

6. 客户端单点登出配置

现在处理 CAS 单点登出(SLO)。客户端应用有两种登出方式:

  • 本地登出:仅影响当前应用
  • CAS 服务器登出:影响所有关联应用

实现本地登出控制器:

@GetMapping("/logout")
public String logout(
  HttpServletRequest request, 
  HttpServletResponse response, 
  SecurityContextLogoutHandler logoutHandler) {
    Authentication auth = SecurityContextHolder
      .getContext().getAuthentication();
    logoutHandler.logout(request, response, auth );
    new CookieClearingLogoutHandler(
      AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY)
      .logout(request, response, auth);
    return "auth/logout";
}

单点登出流程:

  1. CAS 服务器使票据失效
  2. 向所有注册客户端发送异步请求
  3. 客户端执行本地登出

CasSecuredApplication 中添加登出相关 Bean:

@Bean
public SecurityContextLogoutHandler securityContextLogoutHandler() {
    return new SecurityContextLogoutHandler();
}

@Bean
public LogoutFilter logoutFilter() {
    LogoutFilter logoutFilter = new LogoutFilter("https://localhost:8443/logout",
      securityContextLogoutHandler());
    logoutFilter.setFilterProcessesUrl("/logout/cas");
    return logoutFilter;
}

@Bean
public SingleSignOutFilter singleSignOutFilter() {
    SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
    singleSignOutFilter.setLogoutCallbackPath("/exit/cas");
    singleSignOutFilter.setIgnoreInitConfiguration(true);
    return singleSignOutFilter;
}

7. 连接数据库

配置 CAS 服务器从 MySQL 读取凭证。假设本地 MySQL 运行 test 数据库,修改 cas-server/src/main/resources/application.yml

cas:
    authn:
        accept:
            users:
        jdbc:
            query[0]:
                sql: SELECT * FROM users WHERE email = ?
                url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
                dialect: org.hibernate.dialect.MySQLDialect
                user: root
                password: root
                ddlAuto: none
                driverClass: com.mysql.cj.jdbc.Driver
                fieldPassword: password
                passwordEncoder:
                    type: NONE

在客户端实现自定义 UserDetailsService

@Bean
public CasUserDetailsService getUser(){
    return new CasUserDetailsService();
}

@Bean
public CasAuthenticationProvider casAuthenticationProvider(
  TicketValidator ticketValidator,
  ServiceProperties serviceProperties) {
    CasAuthenticationProvider provider = new CasAuthenticationProvider();
    provider.setServiceProperties(serviceProperties);
    provider.setTicketValidator(ticketValidator);
    provider.setUserDetailsService(getUser());
    provider.setKey("CAS_PROVIDER_LOCALHOST_8900");
    return provider;
}

自定义 CasUserDetailsService 实现:

public class CasUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库获取用户
        CasUser casUser = getUserFromDatabase(username);

        // 构建 UserDetails 对象
        UserDetails userDetails = new User(
            casUser.getEmail(),
            casUser.getPassword(),
           Collections.singletonList(new SimpleGrantedAuthority("ROLE_ADMIN")));

        return userDetails;
    }

    private CasUser getUserFromDatabase(String username) {
       return userRepository.findByEmail(username);
    }
}

✅ 关键点:

  • CasAuthenticationProvider 不使用密码认证,但用户名必须匹配
  • MySQL 需运行在 localhost:3306,用户名/密码均为 root

重启服务器和应用后,即可使用数据库凭证认证。

8. 总结

我们完整演示了 CAS SSO 与 Spring Security 的集成过程,包括核心配置文件。CAS SSO 还支持主题、协议类型、认证策略等丰富配置,详见官方文档

CAS 服务器Spring Boot 客户端 源码可在 GitHub 获取。


原始标题:CAS SSO With Spring Security