- 后端
- 认证鉴权
- 单点登录
【免费下载链接】cas
Apereo CAS - Identity & Single Sign On for all earthlings and beyond.
本文基于 CAS 官方文档《Notifications - Slack》及其对应源码展开,介绍如何将 CAS 的用户通知能力对接到 Slack:包括cas-server-support-notifications-slack模块的引入方式、cas.slack-messaging下各配置参数的含义与取值规则、SlackNotificationSender的完整发送流程(消息体构造、Slack 用户解析、chat.postMessage调用与错误处理),以及通知发送器在 CAS 核心通知体系中的装配关系,帮助你在生产环境中把账号管理类业务事件以 Slack 私聊消息的形式推送给对应用户。
功能概述与适用场景
Apereo CAS 的核心通知框架允许把用户生命周期中的事件(如密码重置、账号状态变更等)以推送消息的形式送达用户。官方文档 Notifications - Slack 给出的能力描述是:Send notifications to users as Slack messages(以 Slack 消息的形式向用户发送通知)。
该能力通过以下模块启用:
- Group:
org.apereo.cas - Artifact:
cas-server-support-notifications-slack,其 build.gradle 中对自身能力的官方描述为 “Sends CAS notifications through Slack.”
需要注意的前提:CAS 的标准发行版(Docker/官方二进制发行包)不内置该模块。要使用 Slack 通知,需要从源码构建 CAS 并在构建时包含此模块,具体构建方式可参考 从源码构建 CAS。当前仓库gradle.properties中标注的版本为8.1.0-SNAPSHOT,本文描述的实现细节均以当前仓库代码为准。
通知体系中的定位:NotificationSender 契约
在深入 Slack 模块之前,先看它实现的接口契约。NotificationSender 是 CAS 推送通知的统一抽象:
@FunctionalInterface public interface NotificationSender extends Ordered { /** Bean name for the notification sender. */ String BEAN_NAME = "notificationSender"; /** Attribute name in the payload that indicates the message title. */ String ATTRIBUTE_NOTIFICATION_TITLE = "title"; /** Attribute name in the payload that indicates the message body. */ String ATTRIBUTE_NOTIFICATION_MESSAGE = "message"; static NotificationSender noOp() { return (principal, messageData) -> true; } boolean notify(Principal principal, Map<String, String> messageData); // default canSend() / getOrder() 省略 }几个关键事实(均由接口源码确认):
- 通知载荷(payload)是一个
Map<String, String>,其中固定使用title(消息标题)和message(消息正文)两个键,即ATTRIBUTE_NOTIFICATION_TITLE与ATTRIBUTE_NOTIFICATION_MESSAGE; - 接口自带
noOp()空实现,返回true但什么都不发送——这是后文 Slack 模块“未配置 token 时安全降级”的基础; - 实现类可继承
Ordered的getOrder()调整多个发送器之间的执行顺序。
Slack 模块提供的 SlackNotificationSender 正是该接口的一个实现(Javadoc 标注since 7.0.0)。
配置参数详解:cas.slack-messaging
文档页面中的casproperties片段(cas.slack-messaging)对应的 Java 模型类是 SlackMessagingProperties,它通过 CasConfigurationProperties 中声明的@NestedConfigurationProperty private SlackMessagingProperties slackMessaging字段挂接到全局cas.前缀之下。该模块共暴露两个参数:
| 属性 | 必填 | SpEL 支持 | 说明 |
|---|---|---|---|
cas.slack-messaging.api-token | 是 | 是 | 调用 Slack Web API 所需的 Bot/User Token。通常以xoxb-(bot token)或xoxp-(user token)开头,从安装了该 App 的 Slack workspace 获取 |
cas.slack-messaging.username-attribute | 否 | 是 | 指定使用哪个用户属性的值作为 Slack 用户名。留空时默认使用 principal 标识符(登录名)。无论来源如何,最终值都会被补上@前缀(若尚无该前缀)。支持多值属性 |
两点实现细节值得强调:
api-token标记了@RequiredProperty。在自动配置类 CasSlackMessagingAutoConfiguration 中,slackNotificationSenderBean 由BeanCondition.on("cas.slack-messaging.api-token")守护:private static final BeanCondition CONDITION = BeanCondition.on("cas.slack-messaging.api-token"); @Bean @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT) @ConditionalOnMissingBean(name = "slackNotificationSender") public NotificationSender slackNotificationSender(...) { return BeanSupplier.of(NotificationSender.class) .when(CONDITION.given(applicationContext.getEnvironment())) .supply(() -> new SlackNotificationSender(casProperties.getSlackMessaging())) .otherwise(NotificationSender::noOp) .get(); }即:配置了
cas.slack-messaging.api-token时装配真正的SlackNotificationSender;未配置时降级为NotificationSender.noOp(),不抛错、不阻断启动,通知调用会被静默吞掉。两个 Bean 均标注
@RefreshScope,意味着接入 Spring Cloud Config 等外部配置源后,修改 token 等配置可以动态刷新而无需重启。
典型的application.properties配置示例:
# 必填:Slack Bot Token(xoxb-... 或 xoxp-...),支持 SpEL 表达式 cas.slack-messaging.api-token=${SLACK_API_TOKEN} # 可选:用哪个属性值作为 Slack 用户名;不配置则用 principal id(登录名) cas.slack-messaging.username-attribute=slackId发送流程源码解析:SlackNotificationSender
SlackNotificationSender 的完整发送链路如下:
1. 构造期:初始化 Slack MethodsClient
public SlackNotificationSender(final SlackMessagingProperties slackProperties) { this.slackProperties = slackProperties; val config = new SlackConfig(); config.setPrettyResponseLoggingEnabled(true); val slack = Slack.getInstance(config); this.slackMethods = slack.methods( SpringExpressionLanguageValueResolver.getInstance().resolve(slackProperties.getApiToken())); }- 基于 Slack 官方 Java SDK(
com.slack.api,见 build.gradle 中的implementation libraries.slack); prettyResponseLoggingEnabled(true)会打印格式化的响应日志,便于排查;- token 在构造时经过
SpringExpressionLanguageValueResolver解析,这就是配置项“支持 SpEL”的落地位置——例如可以写#{'xoxb-...'}或引用环境变量/其他 Bean。
2. 运行期:用户名解析与逐人私聊
@Override public boolean notify(final Principal principal, final Map<String, String> messageData) { val body = buildNotificationMessageBody(messageData); val slackUsernames = FunctionUtils.doIfNotBlank(slackProperties.getUsernameAttribute(), () -> principal.getAttributes().get(slackProperties.getUsernameAttribute()), () -> List.of(principal.getId())); return slackUsernames.stream() .allMatch(Unchecked.predicate(slackUsername -> { val channel = Strings.CI.prependIfMissing(slackUsername.toString(), "@"); val messageRequest = ChatPostMessageRequest.builder() .channel(channel) .text(body) .linkNames(true) .mrkdwn(true) .build(); val response = slackMethods.chatPostMessage(messageRequest); ... return response.isOk(); })); }逐点说明:
- 接收者解析:
username-attribute非空时,从用户属性集合中取该属性值(Map.get返回的可能是多值列表,因此支持多值属性);为空则退化为单元素列表List.of(principal.getId()),即以登录名作为 Slack 用户; - 通道寻址:
Strings.CI.prependIfMissing(slackUsername, "@")保证用户名(大小写不敏感判断)前带上@,从而把消息投递到与用户的IM(一对一私聊)通道; - 消息发送:通过
chat.postMessage发送,linkNames(true)启用名字链接化,mrkdwn(true)启用 Slack 标记语法; - 整体结果:
allMatch语义——多值属性下,所有Slack 用户都发送成功才返回true,任一失败(response.isOk()为假)即返回false; - 错误处理:API 返回的
error会通过LoggingUtils.error记录“Error / Provided / Needed”三元组,warning记为 warn 日志;响应全文以 trace 级别输出。
3. 消息体构造
protected String buildNotificationMessageBody(final Map<String, String> messageData) { return "*%s*\n\n%s".strip().formatted( messageData.get(NotificationSender.ATTRIBUTE_NOTIFICATION_TITLE), messageData.get(NotificationSender.ATTRIBUTE_NOTIFICATION_MESSAGE)); }即消息体固定为“粗体标题+ 空行 + 正文”,标题取自 payload 的title键,正文取自message键。protected可见性允许子类覆写来自定义消息格式(例如改用 Slack Block Kit 结构)。
与核心通知模块的装配关系:多发送器聚合
Slack 并不是孤立的发送器。自动配置类 CasSlackMessagingAutoConfiguration 除了注册slackNotificationSender外,还注册了一个执行计划配置器:
@Bean @ConditionalOnMissingBean(name = "slackNotificationSenderExecutionPlanConfigurer") @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT) public NotificationSenderExecutionPlanConfigurer slackNotificationSenderExecutionPlanConfigurer( @Qualifier("slackNotificationSender") final NotificationSender slackNotificationSender) { return () -> slackNotificationSender; }NotificationSenderExecutionPlanConfigurer 是一个“返回某个 NotificationSender”的函数式接口。核心模块 CasCoreNotificationsAutoConfiguration 会收集容器中所有此类配置器,按getOrder()排序后聚合为一个默认发送器:
@Bean @ConditionalOnMissingBean(name = NotificationSender.BEAN_NAME) public NotificationSender notificationSender( final ObjectProvider<List<NotificationSenderExecutionPlanConfigurer>> configurerProviders) { val configurers = Optional.ofNullable(configurerProviders.getIfAvailable()).orElseGet(ArrayList::new); val results = configurers.stream() .map(cfg -> { LOGGER.trace("Configuring notification sender [{}]", cfg.getName()); return cfg.configureNotificationSender(); }) .sorted(Comparator.comparingInt(NotificationSender::getOrder)) .collect(Collectors.toList()); return new DefaultNotificationSender(results); }聚合后的 DefaultNotificationSender 的语义是“任一可发送的发送器成功即成功”:
@Override public boolean notify(final Principal principal, final Map<String, String> messageData) { return notificationSenders.stream().anyMatch(sender -> { if (sender.canSend()) { sender.notify(principal, messageData); return true; } return false; }); }从源码结构看,由此可以推断出完整的调用链:业务模块(如密码重置等)注入名为notificationSender的默认发送器 →DefaultNotificationSender遍历按顺序排序的各渠道发送器(Slack 是其中之一)→ 命中的SlackNotificationSender解析 Slack 用户名、拼装*标题* + 正文消息并调用chat.postMessage逐人私聊。
另外,整个自动配置类标注了@ConditionalOnFeatureEnabled(feature = CasFeatureModule.FeatureCatalog.Notifications, module = "slack"),即该模块受 CAS 特性开关(feature-toggles)体系管控,Notifications特性目录下的slack子模块被启用时才会生效。
测试验证方式
模块自带单元测试 SlackNotificationSenderTests,其做法值得参考:
@SpringBootTest(classes = { CasSlackMessagingAutoConfiguration.class, BaseNotificationTests.SharedTestConfiguration.class }, properties = "cas.slack-messaging.api-token=xoxb-1234567890") class SlackNotificationSenderTests { @Autowired @Qualifier("slackNotificationSender") private NotificationSender notificationSender; @Test void verifyOperation() { assertFalse(notificationSender.notify(RegisteredServiceTestUtils.getPrincipal("cas"), Map.of(NotificationSender.ATTRIBUTE_NOTIFICATION_TITLE, "Apereo CAS Notification", NotificationSender.ATTRIBUTE_NOTIFICATION_MESSAGE, "Hello, this message is from _Apereo CAS_"))); } }测试用假 token(xoxb-1234567890)驱动真实 SDK 调用,由于 token 无效,chat.postMessage会返回错误,断言notify(...)为false——这正好验证了“API 报错时发送器如实返回失败”的行为,同时证明整个链路无需真实 Slack workspace 即可在 CI 中回归。该用例打上了@Tag("Simple")标签,可直接在本地运行验证装配与发送逻辑。
小结
| 关注点 | 结论 |
|---|---|
| 启用模块 | org.apereo.cas:cas-server-support-notifications-slack(需从源码构建引入) |
| 核心配置 | cas.slack-messaging.api-token(必填、SpEL 支持)、cas.slack-messaging.username-attribute(可选、多值属性支持) |
| 未配置 token 时 | 自动降级为NotificationSender.noOp(),不影响启动 |
| 投递方式 | 以@用户名寻址用户私聊通道,调用chat.postMessage,mrkdwn开启 |
| 消息格式 | *标题*+ 空行 + 正文,标题/正文分别取 payload 的title/message |
| 多值属性 | 逐人发送,全部成功才返回true |
| 配置刷新 | 相关 Bean 均为@RefreshScope,支持动态刷新 |
| 验证手段 | 假 token 单测断言发送失败(SlackNotificationSenderTests) |
关键源码文件索引:SlackNotificationSender、CasSlackMessagingAutoConfiguration、SlackMessagingProperties、NotificationSender、DefaultNotificationSender、CasCoreNotificationsAutoConfiguration。
- 后端
- 认证鉴权
- 单点登录
【免费下载链接】cas
Apereo CAS - Identity & Single Sign On for all earthlings and beyond.
相关推荐
Apereo CAS 推送通知 Apple 设备实战:cas-server-support-notifications-apn 模块的配置详解与 APNs 推送实现原理
Apereo CAS 推送通知 Apple 设备实战:cas server support notifications apn 模块的配置详解与 APNs 推送
后端认证鉴权单点登录Apereo CAS 中 MongoDB 监控模块的配置与实现原理
Apereo CAS 中 MongoDB 监控模块的配置与实现原理 本篇技术指南围绕 Apereo CAS 官方监控文档 Configuring Monitor
后端认证鉴权单点登录Insyde BIOS深度调优:如何安全解锁厂商隐藏的硬件配置选项?
Insyde BIOS深度调优:如何安全解锁厂商隐藏的硬件配置选项? 当你的笔记本电脑无法安装黑苹果系统,或者性能始终无法完全释放时,是否想过这可能是BIOS层
固件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考