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.gradle 的 dependencies 部分添加:
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 客户端应用,添加 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>
配置应用属性:
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";
}
单点登出流程:
- CAS 服务器使票据失效
- 向所有注册客户端发送异步请求
- 客户端执行本地登出
在 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 获取。