- 后端
- 认证鉴权
- 单点登录
【免费下载链接】cas
Apereo CAS - Identity & Single Sign On for all earthlings and beyond.
导读
GUA(Graphical User Authentication,图形用户认证),也叫"登录图片"(login images),是 Apereo CAS 提供的一种轻量级第二因子验证方案:用户在创建账户时预选一张图片作为"账户秘密",登录时 CAS 先让用户输入用户名,然后展示该用户预设的图片并要求用户继续输入密码——如果展示的图片与用户记忆中预选的图片不一致,用户应拒绝提交剩余凭据,从而有效抵御钓鱼网站冒充合法站点。本文以 GUA-Authentication.md 为主体,结合仓库中cas-server-support-gua模块的源码、配置模型与测试用例,完整讲解 GUA 的原理、启用方式、静态资源(Resource)与 LDAP 两种图片存储方案的配置,以及底层 Webflow 登录流程的实现细节。
GUA 是什么:把"图片"变成账户秘密
图形用户认证在本质上是一种"共享秘密"(shared secret)式的登录校验手段。它的核心思想是:
- 在账户创建阶段,用户从站点提供的图片池中预先选择一张属于自己的图片,这张图片与用户名绑定,作为账户的专属标识;
- 登录时,站点只要求用户输入用户名,随即返回一页展示该用户名对应的预选图片,同时提供密码输入框;
- 用户被训练为:只有当页面展示的图片确实是自己当初选择的那张时,才继续提交密码;否则应当立即停止操作并离开该站点。
正如 GUA-Authentication.md 中所强调的,图片是"与用户名绑定的账户秘密"(an "account secret" tied to the username),它"不应该被钓鱼攻击轻易复现"(should not be easily reproduced by a phishing campaign attempting to impersonate a legitimate website)。钓鱼网站即使伪造出与合法站点完全一致的登录页面,也无法预知受害者预选了哪张图片,因此无法通过"图片确认"这一关。
需要说明的是,GUA 通常被定位为第二因子(second factor)性质的校验:它不替代用户名密码认证,而是在正式认证之前增加一道"图片确认"环节,属于 认证(Authentication) 类别下的可选增强能力。
启用 GUA:引入 cas-server-support-gua 模块
GUA 支持通过在 CAS overlay 项目中引入以下模块来启用(Gradle 坐标来自官方文档的模块说明):
implementation "org.apereo.cas:cas-server-support-gua"该模块在仓库中的物理位置为 support/cas-server-support-gua,其自动配置入口为 CasGraphicalUserAuthenticationAutoConfiguration.java,类上的注解体现了两个关键前提:
@ConditionalOnFeatureEnabled(feature = CasFeatureModule.FeatureCatalog.Authentication, module = "gua"):GUA 属于 Authentication 特性域下的gua子特性,需在全局特性开关启用时才会生效;@AutoConfiguration:Spring Boot 自动装配,模块引入后无需手动注册 Bean。
模块引入后,CAS 会在登录 Webflow 中自动注入 GUA 相关动作与状态(详见下文"登录流程"一节)。
图片存储方案一:静态资源(Static Resource)
适用场景与原理
静态资源方案 的定位非常明确:主要用于演示(demo)和测试(testing)目的。它允许 CAS 把一个"全局且静态"的图片资源作为用户标识加载到登录流程中。
"全局且静态"的含义是:这种方式不区分图片的所属用户——配置中通过一个以用户名为键、图片资源路径为值的 Map 来为不同用户绑定不同的图片,配置简单直接,适合开发调试、演示环境和自动化测试,但不适合生产环境大规模管理用户图片。
配置方式
在application.properties(或 YAML)中,通过cas.authn.gua.simple配置项指定用户名与图片资源的映射:
cas.authn.gua.simple.casuser=file:/etc/cas/config/images/casuser.jpg cas.authn.gua.simple.alice=classpath:images/alice.png cas.authn.gua.simple.bob=https://example.org/images/bob.jpgcas.authn.gua.simple在配置模型中的定义见 GraphicalUserAuthenticationProperties.java:
/** * Locate GUA settings and images from a static image per user. * This is treated as a {@link Map} where the key is the user id * and the value should be the graphical resource. */ private Map<String, String> simple = new LinkedHashMap<>();也就是说,simple是一个Map<String, String>:key 是用户标识,value 是图片资源位置。value 支持 Spring 的资源定位语法,包括:
| 前缀 | 含义 | 示例 |
|---|---|---|
file: | 文件系统路径 | file:/etc/cas/images/casuser.jpg |
classpath: | 类路径资源(如打成 jar 内嵌的图片) | classpath:images/casuser.jpg |
http(s):// | 远程 URL 资源 | https://example.org/images/casuser.jpg |
图片资源会被解析为 Spring 的Resource对象。从 CasGraphicalUserAuthenticationAutoConfiguration.java 可以看到,当gua.getSimple()非空时,自动配置会优先选择静态资源仓库:
@Bean @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT) @ConditionalOnMissingBean(name = "userGraphicalAuthenticationRepository") public UserGraphicalAuthenticationRepository userGraphicalAuthenticationRepository( final CasConfigurationProperties casProperties) { val gua = casProperties.getAuthn().getGua(); if (!gua.getSimple().isEmpty()) { val accounts = gua.getSimple().entrySet().stream().map(Unchecked.function(entry -> { val res = ResourceUtils.getResourceFrom(entry.getValue()); return Pair.of(entry.getKey(), (Resource) res); })).collect(Collectors.toMap(Pair::getKey, Pair::getValue)); return new StaticUserGraphicalAuthenticationRepository(accounts); } // ...LDAP 分支... throw new BeanCreationException("A repository instance must be configured to locate user-defined graphics"); }底层实现:StaticUserGraphicalAuthenticationRepository
静态图片的读取逻辑位于 StaticUserGraphicalAuthenticationRepository.java。它实现了统一的仓库接口 UserGraphicalAuthenticationRepository(该接口为函数式接口,核心方法只有一个ByteSource getGraphics(String username)):
@Override public ByteSource getGraphics(final String username) { try (val resourceStream = graphicResource.get(username).getInputStream(); val bos = new ByteArrayOutputStream()) { IOUtils.copy(resourceStream, bos); return ByteSource.wrap(bos.toByteArray()); } catch (final Exception e) { LoggingUtils.error(LOGGER, e); } return ByteSource.empty(); }实现要点:
- 以用户名为 key 从
Map<String, Resource>中取出图片资源,读取为字节流并包装成 Guava 的ByteSource; - 如果用户不存在、资源缺失或读取失败,返回
ByteSource.empty(),不会抛出异常; - 对应的单元测试 StaticUserGraphicalAuthenticationRepositoryTests.java 验证了"存在图片返回非空"与"图片缺失返回空"两种行为:
verifyImage:以Map.of("casuser", new ClassPathResource("image.jpg"))构造仓库,断言getGraphics("casuser")非空;verifyBadImage:以不存在的missing.jpg构造,断言返回空。
在测试基类 AbstractGraphicalAuthenticationTests.java 中,静态方案的实际配置写法为:
cas.authn.gua.simple.casuser=classpath:image.jpg这正好可以作为最小可运行的参考配置。
图片存储方案二:LDAP 二进制属性
适用场景与原理
LDAP 方案 允许 CAS 从 LDAP 目录中定位用户的二进制图片属性(binary image attribute),将该二进制属性值作为用户标识加载到登录流程。相比静态资源方案,LDAP 方案把图片作为用户目录数据的一部分统一管理,更贴近生产环境,适合已有 LDAP/AD 目录的机构。
配置方式
LDAP 方案通过cas.authn.gua.ldap配置项启用。核心配置包括:
cas.authn.gua.ldap.ldap-url=ldap://localhost:10389 cas.authn.gua.ldap.base-dn=dc=example,dc=org cas.authn.gua.ldap.search-filter=cn={user} cas.authn.gua.ldap.image-attribute=jpegPhoto cas.authn.gua.ldap.bind-dn=cn=Directory Manager cas.authn.gua.ldap.bind-credential=password参数说明:
| 配置项 | 是否必填 | 说明 |
|---|---|---|
cas.authn.gua.ldap.ldap-url | 是 | LDAP 服务器地址,多个地址可用空格或逗号分隔,支持ACTIVE_PASSIVE、ROUND_ROBIN、RANDOM、DNS_SRV等连接策略 |
cas.authn.gua.ldap.base-dn | 是 | 搜索的基础 DN,可配置多个子树并用\|分隔(如subtreeA,dc=example,dc=net\|subtreeC,dc=example,dc=net) |
cas.authn.gua.ldap.search-filter | 是 | 用户搜索过滤器,语法为cn={user}或cn={0};也支持file:/path/to/GroovyScript.groovy外部脚本动态构造过滤器 |
cas.authn.gua.ldap.image-attribute | 是 | 存放用户图片的条目属性名(如jpegPhoto),该属性必须是二进制类型 |
cas.authn.gua.ldap.bind-dn/bind-credential | 是 | 连接 LDAP 的绑定凭据;置空表示匿名操作,设为*表示 fast-bind 策略 |
cas.authn.gua.ldap.subtree-search | 否(默认true) | 是否允许子树搜索 |
cas.authn.gua.ldap.page-size | 否 | 分页请求大小,用于规避服务器结果集大小限制,负值/零值禁用分页 |
cas.authn.gua.ldap.use-start-tls | 否(默认false) | 是否启用 StartTLS |
cas.authn.gua.ldap.connect-timeout/response-timeout | 否(默认PT5S) | 连接与响应超时 |
cas.authn.gua.ldap.trust-certificates、trust-store等 | 否 | LDAPS/StartTLS 场景下的证书信任配置 |
image-attribute是 LDAP 方案独有且必填的属性,定义见 LdapGraphicalUserAuthenticationProperties.java:
/** * Entry attribute that holds the user image. */ @RequiredProperty private String imageAttribute;其余 LDAP 搜索与连接参数继承自 AbstractLdapSearchProperties 与 AbstractLdapProperties,CAS 其他 LDAP 相关模块(认证、属性仓库、服务注册等)也复用同一套参数体系。
底层实现:LdapUserGraphicalAuthenticationRepository
LDAP 图片的检索逻辑位于 LdapUserGraphicalAuthenticationRepository.java:
@Override public ByteSource getGraphics(final String username) { val gua = casProperties.getAuthn().getGua(); val response = searchForId(username); if (LdapUtils.containsResultEntry(response)) { val entry = response.getEntry(); val attribute = entry.getAttribute(gua.getLdap().getImageAttribute()); if (attribute != null && attribute.isBinary()) { return ByteSource.wrap(attribute.getBinaryValue()); } } return ByteSource.empty(); } private SearchResponse searchForId(final String id) { return FunctionUtils.doUnchecked(() -> { val gua = casProperties.getAuthn().getGua(); val filter = LdapUtils.newLdaptiveSearchFilter(gua.getLdap().getSearchFilter(), LdapUtils.LDAP_SEARCH_FILTER_DEFAULT_PARAM_NAME, CollectionUtils.wrap(id)); return connectionFactory.executeSearchOperation( gua.getLdap().getBaseDn(), filter, gua.getLdap().getPageSize(), new String[]{gua.getLdap().getImageAttribute()}, ReturnAttributes.ALL_USER.value()); }); }实现要点:
- 使用配置中的
searchFilter(cn={user}形式)与baseDn发起搜索,返回属性限定为imageAttribute; - 检索到条目后,读取
imageAttribute属性的二进制值(attribute.isBinary()检查)并包装为ByteSource;若用户不存在或属性缺失,同样返回空; - 该类同时实现
DisposableBean,在销毁时关闭LdapConnectionFactory释放连接池资源; - 从 CasGraphicalUserAuthenticationAutoConfiguration.java 可见,LDAP 分支的启用条件是
ldapUrl、searchFilter、baseDn、imageAttribute四个属性全部非空;若simple与 LDAP 配置都不满足,启动时抛出BeanCreationException("A repository instance must be configured to locate user-defined graphics")。
测试验证:jpegPhoto 属性的端到端验证
仓库中的集成测试 LdapUserGraphicalAuthenticationRepositoryTests.java 提供了完整的验证思路(该测试依赖本机10389端口可用的 LDAP 服务,由@EnabledIfListeningOnPort(port = 10389)控制):
- 使用与上面一致的 LDAP 配置初始化 Spring 上下文;
createLdapEntry方法向目录中添加一个inetOrgPerson条目,其jpegPhoto属性写入image.jpg的字节内容;- 断言
getGraphics(cn)返回非空,而getGraphics("bad-user")返回空。
这从测试层面印证了:LDAP 条目中的二进制图片属性(典型如jpegPhoto)就是 GUA 图片的承载介质,目录管理员只需保证用户条目存在且图片属性为二进制格式即可。
GUA 在登录 Webflow 中的位置:三段式流程
引入模块后,GraphicalUserAuthenticationWebflowConfigurer.java 会把 GUA 流程织入 CAS 登录流程(login flow),整体流程为:
登录表单(initLoginForm) --GUA_PREPARE_LOGIN--> 输入用户名(casGuaGetUserIdView) --> 展示图片+密码框(casGuaDisplayUserGraphicsView) --> 接受图片(AcceptUserGraphics) --> 回到原认证流程具体的织入逻辑(doInitialize方法):
- 在
initLoginForm状态的动作列表中前置追加ACTION_ID_GUA_PREPARE_LOGIN动作; - 为登录表单状态新增一条
GUA_GET_USERID转换,指向新建的视图状态gua/casGuaGetUserIdView(即"先输用户名"页面); - 从用户名视图提交后进入
gua/casGuaDisplayUserGraphicsView(即"展示图片+密码"页面),并在该视图的渲染动作列表中注册DISPLAY_USER_GRAPHICS_BEFORE_AUTHENTICATION动作; - 图片页提交后进入
ACCEPT_GUA动作状态(ACCEPT_USER动作),随后默认转换回到原登录表单状态原本的 success 目标状态,继续执行常规的 Username/Password 认证。
三个核心 Action 的职责
1. PrepareForGraphicalAuthenticationAction(流程入口)
PrepareForGraphicalAuthenticationAction.java 在登录表单渲染前执行:
- 通过
WebUtils.putGraphicalUserAuthenticationEnabled(requestContext, Boolean.TRUE)标记 GUA 已启用; - 若当前流程上下文中还没有 GUA 用户名,则触发
GUA_GET_USERID转换,把用户导向"仅输入用户名"的页面;否则放行继续。
2. DisplayUserGraphicsBeforeAuthenticationAction(取图与校验)
DisplayUserGraphicsBeforeAuthenticationAction.java 是 GUA 的核心校验动作:
@Override protected @Nullable Event doExecuteInternal(final RequestContext requestContext) throws Exception { val username = requestContext.getRequestParameters().get("username"); if (StringUtils.isBlank(username)) { throw UnauthorizedServiceException.denied("Denied"); } val graphics = repository.getGraphics(username); if (graphics == null || graphics.isEmpty()) { throw UnauthorizedServiceException.denied("Denied"); } val image = EncodingUtils.encodeBase64ToByteArray(graphics.read()); WebUtils.putGraphicalUserAuthenticationUsername(requestContext, username); WebUtils.putGraphicalUserAuthenticationImage(requestContext, new String(image, StandardCharsets.UTF_8)); return success(); }要点:
- 从请求参数中取出用户名,为空直接抛出
UnauthorizedServiceException.denied("Denied"); - 调用仓库接口
getGraphics(username)取图,图片为空同样直接拒绝——这意味着用户名错误或没有绑定图片的用户无法通过 GUA 环节; - 取图成功后,将图片字节流做Base64 编码,连同用户名一起放入 Webflow 请求上下文,供视图层渲染。
对应的测试 DisplayUserGraphicsBeforeAuthenticationActionTests.java 验证了"正常返回 success 且上下文中包含图片与用户名"以及"缺少用户名时抛出 UnauthorizedServiceException"两个分支。
3. AcceptUserGraphicsForAuthenticationAction(衔接主认证)
AcceptUserGraphicsForAuthenticationAction.java 在用户确认图片并提交密码页后执行:
- 以当前用户名构造
UsernamePasswordCredential(username, null)放入 Webflow 上下文(密码由后续密码页收集); - 再次记录 GUA 用户名,返回 success,流程回到登录表单的原始 success 目标状态继续常规认证。
从源码结构看,GUA 的定位是"主认证之前的图片确认闸门":图片校验通过后,认证依旧走标准的用户名密码路径,因此 GUA 与 LDAP/JDBC/静态等认证处理器天然兼容。
前端视图与多语言提示
GUA 的两个页面视图由 thymeleaf 模块提供:
- 用户名输入页 casGuaGetUserIdView.html:一个只含用户名字段的表单,提交事件为
_eventId_submit; - 图片展示页 casGuaDisplayUserGraphicsView.html:
<h2 class="text-center" th:text="${guaUsername}">guaUsername</h2> <div id="guaInfo" class="banner banner-danger alert alert-danger d-flex m-4 p-4" role="alert"> <i class="mdi mdi-alert-octagon fas fa-exclamation-circle" aria-hidden="true"></i> <strong th:utext="#{screen.gua.confirm.message}"> If you do not recognize this image as yours, do NOT continue.</strong> </div> <img id="guaImage" style="width:130px;height:130px;" th:src="@{'data:image/jpeg;base64,' + ${guaUserImage}}" alt="User graphic" />值得注意的细节:
- 图片通过
data:image/jpeg;base64,...内联 data URI渲染,这正是前面DisplayUserGraphicsBeforeAuthenticationAction中 Base64 编码的原因——图片不经过额外 HTTP 请求,直接内嵌在页面中; - 页面在图片旁以醒目的警示样式展示提示语:"If you do not recognize this image as yours, do NOT continue."(如果你不确认这张图片是你自己的,请不要继续)。这正是 GUA 对抗钓鱼的核心用户教育话术;
- 该提示语通过
screen.gua.confirm.message国际化键管理,仓库在 messages.properties 以及messages_zh_CN.properties、messages_fr.properties等 30 余种语言文件中均提供了翻译(如中文:"如果您无法识别该图像是您的,请不要继续。"),多语言环境开箱即用; - 图片展示页同样提交
_eventId_submit(Continue),由AcceptUserGraphicsForAuthenticationAction承接。
配置速查与注意事项
两种方案的启用逻辑总结
| 方案 | 配置前缀 | 判定条件(自动配置) | 典型用途 |
|---|---|---|---|
| 静态资源 | cas.authn.gua.simple | simpleMap 非空即启用 | 演示、测试、开发调试 |
| LDAP | cas.authn.gua.ldap | ldapUrl、searchFilter、baseDn、imageAttribute全部非空 | 生产环境、已有目录服务 |
两者的优先级(见 CasGraphicalUserAuthenticationAutoConfiguration.java):静态方案优先;两者都未配置时启动直接失败并提示必须配置图片仓库。
使用建议
- 静态资源方案中的 value 建议优先使用
file:绝对路径或classpath:资源,避免生产环境对远程 URL 的强依赖; - LDAP 方案的
image-attribute必须指向二进制类型的属性(实现代码中显式检查了attribute.isBinary()),最典型的是jpegPhoto(OpenLDAP/389ds 等目录的常用照片属性); - 图片展示页的
<img>标签固定使用data:image/jpeg;base64前缀,因此建议图片统一使用 JPEG 格式,以兼容该内联渲染方式; - GUA 环节中用户名缺失或图片缺失都会被拒绝(
UnauthorizedServiceException.denied),因此请确保启用 GUA 前所有目标用户的图片数据完整; - 若同时启用 GUA 与其他登录方式,GUA 仅作为登录流程的前置步骤,不影响 CAS 既有的认证处理器、MFA 等后续环节的编排。
小结
GUA 是 Apereo CAS 提供的一种轻量、易落地的钓鱼防御手段:它把"预选图片"转化为绑定用户名的账户秘密,在密码提交前增加一道图片确认关卡。本文从官方文档出发,结合仓库源码完整梳理了它的概念模型、模块引入方式、静态资源与 LDAP 两种图片存储方案的配置与底层实现(仓库接口UserGraphicalAuthenticationRepository与两个实现类)、Webflow 三段式登录流程(GUA_PREPARE_LOGIN→DISPLAY_USER_GRAPHICS_BEFORE_AUTHENTICATION→ACCEPT_USER)、Base64 内联渲染机制与多语言提示,并给出了测试用例作为配置正确性的可验证依据。无论是搭建演示环境(cas.authn.gua.simple)还是对接生产目录(cas.authn.gua.ldap+jpegPhoto),都可以参照上文配置快速落地。
- 后端
- 认证鉴权
- 单点登录
【免费下载链接】cas
Apereo CAS - Identity & Single Sign On for all earthlings and beyond.
相关推荐
Apereo CAS 静态资源图形用户认证(GUA)配置与源码原理指南
Apereo CAS 静态资源图形用户认证(GUA)配置与源码原理指南 GUA(Graphical User Authentication)是 Apereo C
后端认证鉴权单点登录Opik Python SDK GEval 指标详解:LLM-as-Judge 通用评估指标的机制、参数与实战
Opik Python SDK GEval 指标详解:LLM as Judge 通用评估指标的机制、参数与实战 GEval 是 Opik Python SDK
后端认证鉴权单点登录Apereo CAS 集成 Apache Cassandra 认证:配置详解与源码剖析
Apereo CAS 集成 Apache Cassandra 认证:配置详解与源码剖析 本文基于 Cassandra Authentication.md htt
后端认证鉴权单点登录
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考