使用Spring Security与CAS SSO

2025/04/03

1. 概述

在本教程中,我们将研究Apereo CAS,并了解Spring Boot服务如何使用它进行身份验证。CAS是一种企业单点登录(SSO)解决方案,也是开源的。

什么是SSO?当你使用相同的凭据登录YouTube、Gmail和地图时,这就是单点登录。我们将通过设置CAS服务器和Spring Boot应用来演示这一点。Spring Boot应用将使用CAS进行身份验证。

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.gradle文件的依赖部分:

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

让我们继续创建上面配置中引用的密钥库文件。首先,我们需要在cas-server/src/main/resources中创建文件夹/etc/cas和/etc/cas/config。

然后,我们需要将目录更改为cas-server/src/main/resources/etc/cas并运行命令来生成keystore:

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

为了避免出现SSL握手错误,我们应该使用localhost作为名字和姓氏的值,组织名称和单位也应使用相同的值。此外,我们需要将thekeystore导入到我们将用于运行客户端应用程序的JDK/JRE中:

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

源和目标密钥库的密码都是changeit。在Unix系统上,我们可能必须以管理员(sudo)权限运行此命令。导入后,我们应该重新启动正在运行的所有Java实例或重新启动系统。

我们使用JDK 11,因为CAS版本6.1.x需要它。此外,我们定义了指向其主目录的环境变量$JAVA11_HOME。现在我们可以启动CAS服务器:

./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服务器以使设置生效,让我们更新task.gradle,以便我们可以从命令行将位置作为JVM参数传递:

task run(group: "build", description: "Run theCASweb 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客户端应用程序,它将具有Web、Security、Freemarker和DevTools依赖。此外,我们还将在其pom.xml中添加Spring Security CAS模块的依赖:

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

最后,让我们添加以下Spring Boot属性来配置应用程序:

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

4. CAS服务器服务注册

客户端应用程序必须在任何身份验证之前向CAS服务器注册,CAS服务器支持使用YAML、JSON、MongoDB和LDAP客户端注册表。

在本教程中,我们将使用JSON服务注册表方法。让我们再创建另一个文件夹cas-server/src/main/resources/etc/cas/services,这个文件夹将存放服务注册表JSON文件。

我们将创建一个包含客户端应用程序定义的JSON文件,文件的名称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模式,该模式应与客户端应用程序的URL匹配。

id属性应具有唯一性,换句话说,不应有两个或多个具有相同id的服务注册到同一个CAS服务器,id重复会导致冲突和配置覆盖。

我们还将注销类型配置为BACK_CHANNEL,将URL配置为http://localhost:8900/exit/cas,以便我们稍后可以进行单点注销

在CAS服务器可以使用我们的JSON配置文件之前,我们必须在cas.properties中启用JSON注册表:

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

5. CAS客户端单点登录配置

下一步是配置Spring Security以与CAS服务器配合使用,我们还应该检查完整的交互流程,称为CAS序列。

让我们将以下Bean配置添加到Spring Boot应用程序的CasSecuredApplication类中:

@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("test@test.com", "Mellon", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
    provider.setKey("CAS_PROVIDER_LOCALHOST_8900");
    return provider;
}

ServiceProperties Bean具有与casSecuredApp-8900.json中的serviceId相同的URL,这很重要,因为它向CAS服务器标识此客户端。

ServiceProperties的sendRenew属性设置为false,这意味着用户只需向服务器出示一次登录凭据。

AuthenticationEntryPoint Bean将处理身份验证异常,因此,它将把用户重定向到CAS服务器的登录URL进行身份验证。

总而言之,身份验证流程如下:

  1. 用户尝试访问安全页面,从而触发身份验证异常
  2. 异常触发AuthenticationEntryPoint;作为响应,AuthenticationEntryPoint将把用户带到CAS服务器登录页面-https://localhost:8443/login
  3. 身份验证成功后,服务器将使用票证重定向回客户端
  4. CasAuthenticationFilter将获取重定向并调用CasAuthenticationProvider
  5. CasAuthenticationProvider将使用TicketValidator来确认CAS服务器上提供的票证
  6. 如果票证有效,用户将被重定向到所请求的安全URL

最后,让我们配置HttpSecurity以保护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客户端单点注销配置

到目前为止,我们已经处理了单点登录;现在让我们考虑CAS单点注销(SLO)。

使用CAS管理用户身份验证的应用程序可以从两个地方注销用户:

  • 客户端应用程序可以在本地注销用户-这不会影响用户在使用同一CAS服务器的其他应用程序中的登录状态。
  • 客户端应用程序还可以从CAS服务器注销用户-这将导致用户从连接到同一CAS服务器的所有其他客户端应用程序中注销。

我们将首先在客户端应用程序上实现注销,然后将其扩展为CAS服务器上的单点注销。

为了使幕后发生的事情一目了然,我们将创建一个logout()方法来处理本地注销。成功后,它会将我们重定向到一个带有单点注销链接的页面:

@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";
}

在单点注销过程中,CAS服务器会先让用户的票证过期。然后向所有注册的客户端应用发送异步请求,每个收到此信号的客户端应用都会执行本地注销,从而达到一次注销的目的,从而导致所有客户端都注销。

话虽如此,让我们向客户端应用程序添加一些Bean配置,具体来说,在CasSecuredApplication中:

@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;
}

logoutFilter将拦截对/logout/cas的请求并将应用程序重定向到CAS服务器,SingleSignOutFilter将拦截来自CAS服务器的请求并执行本地注销。

7. 将CAS服务器连接到数据库

我们可以配置CAS服务器以从MySQL数据库读取凭据,我们将使用在本地计算机中运行的MySQL服务器的测试数据库,让我们更新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

另外,在cas-secured-app cas-secured-app/src/main/resources/application.properties中进行相同的配置:

spring.jpa.generate-ddl=false
spring.datasource.url= jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

我们将cas.authn.accept.users设置为空白,这将停用CAS服务器对静态用户存储库的使用

根据上面的SQL,用户的凭据存储在users表中,email列代表用户的主体(username)。

请务必检查支持的数据库、可用驱动程序和方言列表。我们还将密码编码器类型设置为NONE,其他加密机制及其特殊属性也可用。

注意CAS服务器数据库中的用户必须与客户端应用程序数据库中的用户相同。

让我们更新CasAuthenticationProvider以拥有与CAS服务器相同的用户名:

@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;
}

CasAuthenticationProvider需要UserDetailsService来根据CAS票证加载用户详细信息,UserDetailsService负责从数据源(例如数据库)检索用户信息,在UserDetailsService实现的loadUserByUsername方法中,你可以自定义逻辑以根据提供的用户名加载用户详细信息。

public class CasUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // Get the user from the database.
        CasUser casUser = getUserFromDatabase(username);

        // Create a UserDetails object.
        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);
    }
}

loadUserByUsername方法是CasUserDetailsService类的一部分,此方法负责根据用户名加载用户的详细信息,你可以找到有关使用数据库支持的UserDetailsService进行身份验证的更多信息。

一旦CAS票证被验证并且用户详细信息被加载,CasAuthenticationProvider就会创建一个经过身份验证的Authentication对象,然后可用于应用程序中的授权和访问控制。

CasAuthenticationProvider不使用密码进行身份验证,尽管如此,其用户名必须与CAS服务器的用户名匹配,身份验证才能成功。CAS服务器要求MySQL服务器在localhost的3306端口上运行,用户名和密码应为root。

再次重新启动CAS服务器和Spring Boot应用程序,然后使用新的凭据进行身份验证。

8. 总结

我们已经了解了如何将CAS SSO与Spring Security结合使用以及所涉及的许多配置文件。CAS SSO还有许多其他方面可以配置,从主题和协议类型到身份验证策略。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章