- 后端
- 认证鉴权
- 单点登录
【免费下载链接】cas
Apereo CAS - Identity & Single Sign On for all earthlings and beyond.
导读
本文聚焦 Apereo CAS 无密码(Passwordless)认证体系中"用户账户存储"这一核心扩展点,讲解如何通过实现PasswordlessUserAccountStore接口、注册自定义 Spring Bean 来替换或扩展现有账户数据源。阅读本文后,你将掌握:PasswordlessUserAccountStore接口的定义与职责、CAS 默认提供的多种账户存储实现及加载机制、账户模型PasswordlessUserAccount的全部字段语义,以及如何在 CAS Overlay 中以源码级方式编写并注册自定义存储,让无密码登录直接对接你自有目录中的用户数据。
自定义用户账户存储:核心思路
在 CAS 的 Passwordless 认证流程中,当用户提交用户名后,CAS 需要一个"账户查找"环节来决定该用户是否可以发起无密码认证,以及应当向哪个联系方式(邮箱/手机号)发送一次性令牌。这一查找行为被抽象为PasswordlessUserAccountStore接口。
官方文档 Passwordless-Authentication-Storage-Custom.md 明确指出:你可以定义自己的用户账户存储,只需实现PasswordlessUserAccountStore接口,并以如下 Bean 定义将其注册到 CAS 运行时:
@Bean public PasswordlessUserAccountStore passwordlessUserAccountStore() { ... }这里有两个关键点需要留意:
- Bean 名称必须是
passwordlessUserAccountStore。该名称正是接口中声明的常量BEAN_NAME(见 PasswordlessUserAccountStore.java),CAS 的自动配置在装配默认存储时使用了@ConditionalOnMissingBean(name = PasswordlessUserAccountStore.BEAN_NAME)进行守卫(见 CasPasswordlessAuthenticationAutoConfiguration.java)。只要你的自定义 Bean 以同名注册,CAS 就会跳过内部默认组装逻辑,直接使用你的实现。 - 注册位置:自定义配置类需要能被 CAS 的
@AutoConfiguration机制扫描到。具体注册方法参见 Configuration-Management-Extensions.md:将配置类全限定名写入src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件即可。
接口定义与契约
PasswordlessUserAccountStore位于support/cas-server-support-passwordless-api模块,是一个标注了@FunctionalInterface的接口,源码见 PasswordlessUserAccountStore.java:
@FunctionalInterface public interface PasswordlessUserAccountStore { String BEAN_NAME = "passwordlessUserAccountStore"; Optional<? extends PasswordlessUserAccount> findUser(PasswordlessAuthenticationRequest request) throws Throwable; default void reload() { } }接口契约包含两部分:
findUser(PasswordlessAuthenticationRequest request):唯一必须实现的方法。入参是封装了用户名的PasswordlessAuthenticationRequest,返回Optional<? extends PasswordlessUserAccount>。返回Optional.empty()表示未找到该用户,CAS 将拒绝发起无密码认证;返回账户对象则表示命中,后续流程将据此发送令牌。reload():默认空实现,用于支持账户数据的运行时刷新。例如基于 JSON 文件的存储实现会通过它重读文件内容(见下文)。如果你的自定义存储有缓存或外部数据源变更场景,建议重写该方法。
请求对象:PasswordlessAuthenticationRequest
findUser的入参类型为 PasswordlessAuthenticationRequest.java,自 7.0.0 版本引入,字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
username | String | 用户在登录页面提交的用户名(必填) |
providedUsername | String | 额外提供的用户名信息(可为空) |
properties | Map<String, String> | 附加请求属性集合,可用于传递上下文信息 |
账户模型:PasswordlessUserAccount
返回的账户对象类型为 PasswordlessUserAccount.java,其字段决定了无密码认证的后续行为:
| 字段 | 类型 | 默认值 | 语义 |
|---|---|---|---|
username | String | - | 用户名 |
email | String | - | 邮箱,令牌将发送至此(需与phone至少提供一个) |
phone(JSON 字段名phoneNumber) | String | - | 手机号,令牌发送的备选渠道 |
name | String | - | 显示名称 |
attributes | Map<String, List<Object>> | 空LinkedHashMap | 用户属性,将并入最终认证主体(Principal) |
multifactorAuthenticationEligible | TriStateBoolean | UNDEFINED | 是否允许/要求多因素认证(三态布尔,可显式 true/false 或保持未定义) |
delegatedAuthenticationEligible | TriStateBoolean | UNDEFINED | 是否允许委托认证(如通过外部 IdP 登录) |
allowedDelegatedClients | List<String> | 空列表 | 允许使用的委托客户端白名单 |
requestPassword | boolean | false | 是否要求用户额外输入密码 |
allowSelectionMenu | boolean | false | 是否允许显示账户选择菜单 |
source | String | - | 账户来源标识 |
其中hasContactInformation()方法用于校验账户是否具备联系方式(email或phone至少一个非空),这是 CAS 判断能否发送令牌的依据。该模型使用 Jackson 序列化(@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)保留类型信息),因此从 REST 或 JSON 数据源反序列化时,返回结构需要与该字段集合兼容。
CAS 内置的账户存储实现
在动手编写自定义实现之前,先了解 CAS 自带的几种PasswordlessUserAccountStore实现,它们既可以直接使用,也可以作为自定义实现的参考模板。全部位于support/cas-server-support-passwordless-api/src/main/java/org/apereo/cas/impl/account/目录:
| 实现类 | 数据来源 | 触发配置 |
|---|---|---|
SimplePasswordlessUserAccountStore | 配置文件中的键值对 | cas.authn.passwordless.accounts.simple.* |
JsonPasswordlessUserAccountStore | JSON 文件 | cas.authn.passwordless.accounts.json.location |
GroovyPasswordlessUserAccountStore | Groovy 脚本 | cas.authn.passwordless.accounts.groovy.location |
RestfulPasswordlessUserAccountStore | REST 接口 | cas.authn.passwordless.accounts.rest.url |
组合器:ChainingPasswordlessAccountStore
需要注意,CAS 默认装配的passwordlessUserAccountStore并非上述某一个实现,而是一个组合存储ChainingPasswordlessAccountStore.java:自动配置会收集容器中所有PasswordlessUserAccountStore类型的 Bean(含内置的 JSON/Groovy/REST/Simple 存储与用户自定义存储),按AnnotationAwareOrderComparator排序后包装为链式查找。findUser依次询问每个子存储,返回第一个命中结果(见 CasPasswordlessAuthenticationAutoConfiguration.java)。
这意味着:
- 如果你自定义的存储 Bean使用了其他名称(而非
passwordlessUserAccountStore),它会被自动纳入默认链式存储中作为补充数据源; - 如果你自定义的 Bean使用标准名称
passwordlessUserAccountStore,则完全替换掉默认链式存储,此时你需要自行处理多数据源逻辑。
SimplePasswordlessUserAccountStore:配置驱动的极简实现
该实现内部维护Map<String, PasswordlessUserAccount> accounts,findUser时使用正则匹配:以配置中的 key 作为正则表达式,对请求用户名做RegexUtils.find(key, username)匹配,命中即返回对应账户(见 SimplePasswordlessUserAccountStore.java)。由于 key 被当作正则处理,你甚至可以用user.*之类的模式批量匹配用户。
其对应的测试用例 SimplePasswordlessUserAccountStoreTests.java 展示了最简配置形态:
cas.authn.passwordless.accounts.simple.casuser=1234567890 cas.authn.passwordless.tokens.crypto.enabled=false配置值会被自动识别为邮箱或手机号:EmailValidator判定为合法邮箱则设置email,否则设置phone(见 CasPasswordlessAuthenticationAutoConfiguration.java)。测试中casuser查询返回账户、other查询返回空,验证了查找与 miss 两条路径。
JsonPasswordlessUserAccountStore:文件热加载
该实现继承SimplePasswordlessUserAccountStore,从 JSON 文件读取账户 Map,并通过FileWatcherService监控文件变化,一旦文件被修改就触发reload()重新加载(见 JsonPasswordlessUserAccountStore.java)。JSON 文件结构与账户模型对应,例如:
{ "casuser": { "username": "casuser", "email": "casuser@example.org", "phoneNumber": "1234567890", "name": "CAS User", "attributes": { "memberOf": ["staff"] }, "requestPassword": false, "allowSelectionMenu": false } }GroovyPasswordlessUserAccountStore:脚本驱动
以 Groovy 脚本作为查找逻辑,脚本入参为[request, logger]两个对象,返回PasswordlessUserAccount或空(见 GroovyPasswordlessUserAccountStore.java)。脚本位置通过cas.authn.passwordless.accounts.groovy.location指定,适合快速实现复杂查找规则而无需重新编译 Java 代码。
RestfulPasswordlessUserAccountStore:REST 数据源
将查找行为委托给远程 REST 接口:CAS 以配置的 HTTP 方法请求url + "/" + username,附带username请求参数与自定义 headers,响应体按PasswordlessUserAccount.class反序列化(支持 HJSON 解析),返回2xx且有实体时即视为命中(见 RestfulPasswordlessUserAccountStore.java)。相关配置包括:
cas.authn.passwordless.accounts.rest.url=https://idm.example.org/api/passwordless/account cas.authn.passwordless.accounts.rest.method=GET cas.authn.passwordless.accounts.rest.basic-auth-username=user cas.authn.passwordless.accounts.rest.basic-auth-password=secret cas.authn.passwordless.accounts.rest.headers=... cas.authn.passwordless.accounts.rest.maximum-retry-attempts=3编写并注册自定义存储:完整实战
下面以"对接企业自有用户目录"为场景,演示完整的自定义实现步骤。假设你的用户目录存储在一个内存 Map 或数据库中,需要根据用户名返回PasswordlessUserAccount。
第一步:实现接口
在 CAS Overlay 中新建配置类与存储实现:
package org.example.cas.config; import org.apereo.cas.api.PasswordlessAuthenticationRequest; import org.apereo.cas.api.PasswordlessUserAccount; import org.apereo.cas.api.PasswordlessUserAccountStore; import org.apereo.cas.authentication.principal.PrincipalFactory; import java.util.Optional; public class MyDirectoryPasswordlessUserAccountStore implements PasswordlessUserAccountStore { private final UserDirectoryService userDirectoryService; public MyDirectoryPasswordlessUserAccountStore(final UserDirectoryService userDirectoryService) { this.userDirectoryService = userDirectoryService; } @Override public Optional<? extends PasswordlessUserAccount> findUser( final PasswordlessAuthenticationRequest request) throws Throwable { final String username = request.getUsername(); final UserRecord record = userDirectoryService.findByUsername(username); if (record == null) { return Optional.empty(); } return Optional.of(PasswordlessUserAccount.builder() .username(record.getUsername()) .email(record.getEmail()) .phone(record.getPhoneNumber()) .name(record.getDisplayName()) .attributes(Map.of("memberOf", List.of(record.getGroups()))) .requestPassword(false) .build()); } @Override public void reload() { userDirectoryService.refreshCache(); } }第二步:声明 Bean 并注册配置类
参照官方文档给出的 Bean 定义,并在配置类上补充@AutoConfiguration与@EnableConfigurationProperties:
package org.example.cas.config; import org.apereo.cas.api.PasswordlessUserAccountStore; import org.apereo.cas.configuration.CasConfigurationProperties; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ScopedProxyMode; @AutoConfiguration @EnableConfigurationProperties(CasConfigurationProperties.class) public class CustomPasswordlessAccountStoreConfiguration { @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT) @Bean(name = PasswordlessUserAccountStore.BEAN_NAME) public PasswordlessUserAccountStore passwordlessUserAccountStore( @Qualifier("userDirectoryService") final UserDirectoryService userDirectoryService) { return new MyDirectoryPasswordlessUserAccountStore(userDirectoryService); } }要点说明:
@Bean(name = PasswordlessUserAccountStore.BEAN_NAME):显式使用标准 Bean 名称,从而触发@ConditionalOnMissingBean(name = "passwordlessUserAccountStore")条件,使你的实现完全替换默认链式存储。@RefreshScope(proxyMode = ScopedProxyMode.DEFAULT):与 CAS 内部 Bean 定义保持一致,外部配置变更触发上下文刷新时 Bean 可被重建(CAS 官方文档 Configuration-Management-Extensions.md 亦推荐此写法)。- 若你希望自定义存储作为补充数据源参与默认链式查找,则去掉
name属性(改用如myPasswordlessUserAccountStore的自定义名称),并可通过@Order(...)控制查找顺序。
第三步:注册到 AutoConfiguration.imports
创建(或编辑)src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,加入配置类全限定名:
org.example.cas.config.CustomPasswordlessAccountStoreConfiguration重启 CAS 后,passwordlessUserAccountStore即为你自定义的实现。你可以通过 CAS 暴露的账户查询端点验证效果:GET /cas/actuator/passwordless/account?username=casuser(端点实现见 PasswordlessAuthenticationEndpoint.java)。命中时返回账户 JSON(200),未命中返回 404;同时端点会用默认PrincipalResolver解析用户并尝试将解析出的属性合并进账户attributes,方便你观察最终账户数据。
进阶:账户定制器(PasswordlessUserAccountCustomizer)
除存储实现外,CAS 还提供了PasswordlessUserAccountCustomizer扩展点,用于在存储查找完成后对账户做二次加工(见 PasswordlessUserAccountCustomizer.java)。它是一个Ordered接口,customize(Optional<? extends PasswordlessUserAccount>)返回处理后的账户,默认getOrder()为Ordered.HIGHEST_PRECEDENCE。
SimplePasswordlessUserAccountStore的findUser在命中账户后会对所有非代理的 customizer 依次调用(见 SimplePasswordlessUserAccountStore.java)。CAS 内置的GroovyPasswordlessUserAccountCustomizer(源码)允许通过cas.authn.passwordless.core.passwordless-account-customizer-script.location指定 Groovy 脚本来动态修改账户属性,脚本入参为[account, applicationContext, logger]。
如果你的自定义存储想复用 customizer 机制,可参考内置实现的做法:在构造器中注入List<PasswordlessUserAccountCustomizer>,在findUser命中后遍历调用。
存储查找链路与认证衔接
为了完整理解自定义存储在整个无密码认证中的位置,梳理关键调用链如下:
- 用户在登录页输入用户名,CAS 触发无密码认证流程;
PasswordlessUserAccountStore.findUser(request)被调用(可能经由ChainingPasswordlessAccountStore链式分发到你的自定义存储);- 命中账户后,CAS 依据账户的
email/phone发送一次性令牌,令牌由PasswordlessTokenRepository管理(默认内存实现,可配置 REST 后端,见 CasPasswordlessAuthenticationAutoConfiguration.java); - 用户提交令牌,
PasswordlessTokenAuthenticationHandler校验通过后完成认证,账户attributes并入最终 Principal。
从源码结构可以推断,passwordlessUserAccountStoreBean 同时被认证处理器、端点等组件通过@Qualifier(PasswordlessUserAccountStore.BEAN_NAME)引用,因此只要标准 Bean 存在且行为正确,整个认证链无需其他改动。
小结
自定义 Passwordless 用户账户存储是 CAS 无密码认证落地自有用户体系的标准做法,核心就三步:实现PasswordlessUserAccountStore接口、以标准 Bean 名称注册、通过AutoConfiguration.imports让 CAS 发现配置类。本文涉及的接口与实现均可在仓库中直接研读:接口契约见 PasswordlessUserAccountStore.java,账户模型见 PasswordlessUserAccount.java,自动装配逻辑见 CasPasswordlessAuthenticationAutoConfiguration.java,内置实现与测试见support/cas-server-support-passwordless-api/src/main/java/org/apereo/cas/impl/account/目录及 SimplePasswordlessUserAccountStoreTests.java。官方扩展机制的整体说明可参考 Configuration-Management-Extensions.md。
- 后端
- 认证鉴权
- 单点登录
【免费下载链接】cas
Apereo CAS - Identity & Single Sign On for all earthlings and beyond.
相关推荐
Apereo CAS 无密码认证账户存储(Passwordless Account Stores)完全指南
Apereo CAS 无密码认证账户存储(Passwordless Account Stores)完全指南 导读 本文以 Apereo CAS 官方文档 Pas
后端认证鉴权单点登录Apereo CAS LDAP 密码无感认证账户存储(Passwordless Authentication Storage)实战指南
Apereo CAS LDAP 密码无感认证账户存储(Passwordless Authentication Storage)实战指南 导读 本文讲解 Aper
后端认证鉴权单点登录Apereo CAS Surrogate 认证之 JSON 账户存储配置实战指南
Apereo CAS Surrogate 认证之 JSON 账户存储配置实战指南 Surrogate 认证(又称模拟/代管认证,即“Web 版 sudo”)允许
后端认证鉴权单点登录
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考