ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Apereo CAS 配置 Slack 通知:cas-server-support-notifications-slack 模块实现原理与实战指南

Apereo CAS 配置 Slack 通知:cas-server-support-notifications-slack 模块实现原理与实战指南
  • 后端
  • 认证鉴权
  • 单点登录

【免费下载链接】cas

Apereo CAS - Identity & Single Sign On for all earthlings and beyond.

项目地址:https://gitcode.com/gh_mirrors/ca/cas
点击查看免费下载

本文基于 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 标识符(登录名)。无论来源如何,最终值都会被补上@前缀(若尚无该前缀)。支持多值属性

两点实现细节值得强调:

  1. 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(),不抛错、不阻断启动,通知调用会被静默吞掉。

  2. 两个 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.

项目地址:https://gitcode.com/gh_mirrors/ca/cas
点击查看免费下载

相关推荐

上一篇:Retrofit 官方 Wire Converter 使用与源码解析:基于 Protocol Buffers 的类型安全 HTTP 客户端序列化方案
下一篇:saramin-talent-search 合规边界解析:从 DISCLAIMER 到技能硬约束的完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进