Skip to content

CAS Docker 容器化与 CI/CD 实践:从手动 Dockerfile 到 Jib 云原生镜像构建

作者: 必码 | bima.cc


前言

在当今企业级应用架构中,单点登录(Single Sign-On,SSO)系统是统一身份认证的核心基础设施。Apereo CAS(Central Authentication Service)作为全球范围内应用最广泛的开源 SSO 解决方案之一,已经在教育、金融、政务、医疗等众多行业领域得到了深度部署。从 CAS 5.3 的 Spring Boot 2.x 时代,到 CAS 6.6 的成熟稳定期,再到 CAS 7.3 的云原生全面拥抱,CAS 的容器化之路映射了整个 Java 生态从传统部署向云原生演进的完整历程。

本文基于我们团队在实际 CAS Overlay 项目中的工程实践,覆盖了 CAS 5.3、6.6、7.3 三个大版本的容器化方案。我们将从最基础的手动 Dockerfile 两阶段构建讲起,逐步深入到 Jib 云原生镜像构建、CycloneDX SBOM 软件物料清单、多平台镜像支持,再到完整的 CI/CD 流水线设计与生产环境部署最佳实践。文章中的所有配置示例均来自实际生产项目的简化教学版本,力求让读者既能理解技术原理,又能直接应用于实际工程。

无论你是正在评估 CAS 容器化方案的技术决策者,还是负责落地实施的基础设施工程师,抑或是对云原生 Java 应用构建感兴趣的开发者,本文都将为你提供一份详尽的技术参考。


第一章 CAS 容器化概述

1.1 CAS Overlay 项目的容器化需求

Apereo CAS 采用 Overlay(覆盖层)的架构模式进行项目定制开发。所谓 Overlay,是指开发者不需要直接修改 CAS 的源代码,而是通过创建一个独立的 Overlay 项目,利用 Gradle 或 Maven 的依赖管理机制引入 CAS 的核心模块,然后通过覆盖配置文件、模板文件、静态资源等方式实现定制化。这种架构设计带来了极高的可维护性——当 CAS 官方发布新版本时,只需在 gradle.propertiespom.xml 中修改版本号即可完成升级,定制的业务逻辑和配置完全保留。

然而,这种 Overlay 架构在容器化场景下面临着一系列独特的挑战:

依赖管理的复杂性。 CAS Overlay 项目依赖大量的 CAS 核心模块和第三方库。以我们实际的 CAS 7.3 Gradle 项目为例,build.gradle 中声明了超过 40 个 CAS 模块依赖(如 cas-server-corecas-server-support-oauthcas-server-support-redis-ticket-registry 等),加上 Spring Boot Starter、MyBatis、MySQL 驱动、Pac4j 等间接依赖,最终构建出的 Fat JAR 文件通常在 200MB 以上。如何在 Docker 镜像中高效地打包这些依赖,同时保持镜像的可维护性,是容器化的首要课题。

构建工具链的多样性。 CAS 官方同时支持 Gradle 和 Maven 两种构建工具。我们的项目中,CAS 5.3 和 6.6 版本同时维护了 Gradle 和 Maven 两套构建配置,而 CAS 7.3 版本虽然以 Gradle 为主构建工具,但 Maven 版本同样需要支持。这意味着容器化方案必须兼容两种构建工具链,或者在 CI/CD 流水线中提供统一的抽象层。

运行环境的版本敏感性。 CAS 对 JDK 版本有严格的要求:CAS 5.3 基于 Spring Boot 2.x,需要 JDK 8 或 JDK 11;CAS 6.6 同样基于 Spring Boot 2.7.x,官方推荐 JDK 11;CAS 7.3 则基于 Spring Boot 3.5.x,强制要求 JDK 21。这种版本敏感性要求容器化方案必须精确控制基础镜像的 JDK 版本,同时考虑不同版本间的迁移路径。

配置与密钥的外部化。 CAS 的配置体系非常复杂,涉及 application.ymlbootstrap.properties、服务注册 JSON 文件、SSL 密钥库、SAML 证书等。在生产环境中,这些敏感配置和密钥材料绝不应该打包进镜像,而应该通过环境变量、挂载卷、配置中心等方式在运行时注入。容器化方案需要为这些外部化需求提供优雅的支撑。

服务注册与发现。 CAS 作为 SSO 系统的核心,需要与大量下游应用进行集成。每个下游应用都需要在 CAS 中注册为服务(Service),CAS 通过服务注册文件(JSON 格式)来管理这些集成关系。在容器化环境中,服务注册文件的管理方式需要重新设计——是打包进镜像、通过 ConfigMap 注入、还是通过 JSON Service Registry 动态加载?

1.2 单体应用容器化 vs 微服务容器化

CAS 本质上是一个单体应用(Monolith)。虽然 CAS 7.x 开始在架构层面引入了一些模块化的改进,但它仍然以一个统一的 WAR/JAR 包进行部署。这种单体架构的容器化与微服务架构的容器化有着本质的区别。

单体应用容器化的特征:

  • 单一镜像: 整个 CAS 应用被打包为一个 Docker 镜像,包含所有功能模块(认证、授权、OAuth、SAML、REST API 等)。
  • 统一扩缩容: 当负载增加时,通过增加 CAS 容器的副本数来水平扩展,所有副本共享相同的功能集。
  • 简单部署: 只需要管理一个镜像和一个部署配置,运维复杂度较低。
  • 耦合发布: 任何功能的变更都需要重新构建和部署整个应用,无法独立发布某个模块。

微服务容器化的特征(对比参考):

  • 多镜像: 每个微服务有独立的 Docker 镜像,例如认证服务、Token 服务、用户目录服务各自独立。
  • 独立扩缩容: 可以针对负载较高的服务单独扩容,资源利用率更高。
  • 复杂编排: 需要服务网格(Service Mesh)、服务发现、分布式追踪等基础设施支撑。
  • 独立发布: 各服务可以独立构建、测试和发布,发布节奏更灵活。

对于 CAS 来说,选择单体容器化是合理的。原因在于:第一,SSO 系统的核心价值在于统一认证,各功能模块之间有大量的内部状态共享(如 Ticket Registry、Session 管理),拆分为微服务会引入不必要的分布式复杂度;第二,CAS 的部署规模通常不大(几个到几十个实例),单体架构足以满足性能需求;第三,CAS 的升级频率相对较低(通常按季度或半年度),统一发布的成本可控。

然而,这并不意味着 CAS 的容器化方案可以简单粗暴。恰恰相反,正因为 CAS 是一个功能密集的单体应用,其容器化方案需要在镜像体积、启动速度、配置灵活性、可观测性等方面做更精细的优化。

1.3 容器化对 SSO 系统的影响

将 CAS 从传统的虚拟机或物理机部署迁移到 Docker 容器,会对 SSO 系统的多个维度产生深远影响。这些影响需要在架构设计阶段就充分考虑,否则可能在生产环境中引发严重问题。

1.3.1 网络层面

在传统部署中,CAS 通常部署在独立的服务器上,拥有稳定的网络标识(IP 地址或域名)。客户端浏览器通过 DNS 解析找到 CAS 服务器,建立 HTTPS 连接完成认证。这种模式下,网络拓扑相对固定。

容器化之后,网络模型发生了根本性变化:

  • 容器网络是动态的: Docker 容器的 IP 地址在每次重启后可能发生变化。虽然可以通过 Docker 网络的 DNS 服务实现容器名解析,但对于外部客户端(浏览器)来说,CAS 的访问地址仍然需要一个稳定的入口。
  • 反向代理成为必需: 在生产环境中,通常需要在 CAS 容器前面部署 Nginx 或 Traefik 作为反向代理和负载均衡器。反向代理负责 TLS 终止、请求路由、健康检查等。
  • 跨主机通信: 当 CAS 实例分布在不同 Docker 主机上时,需要使用 Overlay 网络或 Kubernetes 的 CNI 插件来实现跨主机通信。这对 Ticket Registry(特别是 Redis Ticket Registry)的网络延迟和可靠性提出了更高要求。
  • Service Mesh 集成: 在 Kubernetes 环境中,可以引入 Istio 或 Linkerd 等 Service Mesh,为 CAS 提供流量管理、熔断、重试等能力。但需要注意,Service Mesh 的 mTLS 可能与 CAS 的 SSL/TLS 配置产生冲突。

1.3.2 存储层面

CAS 在运行过程中需要访问多种类型的存储:

  • Ticket Registry 存储: CAS 的核心数据结构是 Ticket(票据),包括 TGT(Ticket-Granting Ticket)和 ST(Service Ticket)。在容器化环境中,Ticket Registry 必须使用外部存储(如 Redis、Memcached、Hazelcast),而不能依赖内存存储。原因在于容器是无状态的,随时可能被销毁和重建,内存中的 Ticket 数据会随之丢失。
  • 服务注册存储: CAS 需要管理下游应用的服务注册信息。在容器化环境中,推荐使用 JSON Service Registry,将服务注册文件放在外部存储(如 Git 仓库、S3 对象存储)中,通过文件监听机制自动加载更新。
  • 配置存储: CAS 的配置文件(application.ymlcas.properties 等)应该通过环境变量、ConfigMap 或配置中心(如 Spring Cloud Config、Nacos)进行管理,避免将配置固化在镜像中。
  • 日志存储: 容器的文件系统是临时的,日志必须输出到标准输出(stdout/stderr),由容器运行时(Docker Engine、Kubernetes)收集后发送到集中式日志系统(如 ELK、Loki)。

1.3.3 会话层面

CAS 的会话管理在容器化环境中面临特殊挑战:

  • Sticky Session 问题: 如果使用基于内存的 Session 复制,在多实例部署时需要配置负载均衡器的会话亲和性(Sticky Session)。但这会导致负载不均衡,且在实例宕机时会丢失会话。
  • 分布式 Session: 推荐使用 Spring Session 结合 Redis 实现分布式 Session。CAS 从 5.x 版本开始就支持 Redis Ticket Registry,可以将 Ticket 和 Session 数据存储在 Redis 中,实现真正的无状态部署。
  • Cookie 域名问题: CAS 的 TGT Cookie(CASTGC)的域名设置需要与反向代理的域名一致。在容器化环境中,需要确保 Cookie 的 domainpath 属性配置正确,否则浏览器可能无法正确传递 Cookie。

1.4 CAS 版本演进与容器化策略变迁

在我们团队的实际项目中,CAS 的容器化策略经历了三个阶段的演进,对应 CAS 的三个大版本。理解这一演进过程,有助于读者把握 Java 应用容器化的技术趋势。

维度CAS 5.3 (2019)CAS 6.6 (2022)CAS 7.3 (2025)
JDK 版本8 / 111121
Spring Boot2.7.x2.7.x3.5.x
Gradle 版本7.57.59.1.0
构建产物Fat JARFat JARFat JAR
容器化方式手动 Dockerfile手动 Dockerfile + JibJib + Docker Plugin
基础镜像azul/zulu-openjdk:8/11azul/zulu-openjdk:11azul/zulu-openjdk:21
SBOM 支持CycloneDX 3.2.0
多平台镜像不支持配置项存在但未启用amd64:linux, arm64:linux
配置缓存Gradle Configuration Cache
JDK 自动下载Foojay Plugin 1.0.0

从表中可以清晰地看到,CAS 的容器化策略从最初的手动 Dockerfile 逐步演进到以 Jib 为核心的云原生镜像构建方案。这一演进不仅仅是工具的替换,更是构建理念的转变——从"在容器中构建"到"为容器构建"。


第二章 CAS 5.3/6.6 手动 Dockerfile 深度解析

2.1 两阶段构建架构设计

CAS 5.3 和 6.6 的容器化方案采用了 Docker 的多阶段构建(Multi-Stage Build)技术。多阶段构建是 Docker 17.05 引入的特性,允许在一个 Dockerfile 中定义多个构建阶段,每个阶段可以使用不同的基础镜像,最终只将必要的产物复制到最终的运行镜像中。

对于 CAS Overlay 项目来说,两阶段构建的核心动机是镜像瘦身。CAS 的 Gradle/Maven 构建过程需要完整的 JDK(用于编译 Java 源代码)、构建工具本身(Gradle 或 Maven)、以及大量的构建依赖缓存。如果将这些全部打包进运行镜像,最终的镜像体积可能超过 1GB。而实际运行 CAS 只需要 JRE(Java Runtime Environment)和构建产物(Fat JAR),加上必要的配置文件和密钥库。

两阶段构建的架构如下图所示:

+-------------------+     +-------------------+
|   阶段 1:构建    |     |   阶段 2:运行    |
|                   |     |                   |
| gradle:7.5-jdk8   |     | azul/zulu-openjdk |
| 或 maven:3.9-jdk8 |     | :8 或 :11         |
|                   |     |                   |
| - 源代码编译      |     | - Fat JAR         |
| - 依赖下载        | --> | - 密钥库          |
| - Fat JAR 打包    |     | - 服务注册文件    |
| - 构建缓存        |     | - entrypoint.sh   |
|                   |     | - 最小化 JRE      |
| 体积: ~800MB      |     | 体积: ~400MB      |
+-------------------+     +-------------------+
         |                         |
         v                         v
    [丢弃,不进入最终镜像]    [最终运行镜像]

这种设计的优势非常明显:

  1. 镜像体积减少约 50%: 从包含完整 JDK 和构建工具的 800MB+ 减少到只包含 JRE 和应用代码的 400MB 左右。
  2. 攻击面缩小: 运行镜像中不包含编译器、构建工具和构建依赖,减少了潜在的安全攻击向量。
  3. 构建缓存优化: 构建阶段的层缓存可以独立于运行阶段进行管理,源代码变更不会导致基础工具层的重新构建。

2.2 构建阶段详解

让我们以 CAS 5.3 的 Gradle 版本 Dockerfile 为例,逐行解析构建阶段的实现细节。

dockerfile
# 阶段1:构建
FROM gradle:7.5-jdk8 AS builder
WORKDIR /app

基础镜像选择 gradle:7.5-jdk8,这是一个官方维护的镜像,预装了 Gradle 7.5 和 JDK 8。选择官方镜像的好处是包含了常见的安全补丁和优化。AS builder 为这个阶段命名,以便后续阶段通过 COPY --from=builder 引用其产物。

dockerfile
# 优化 Gradle 构建配置
RUN mkdir -p ~/.gradle \
    && echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties \
    && echo "org.gradle.configureondemand=true" >> ~/.gradle/gradle.properties

这一步对 Gradle 构建配置进行优化。在 Docker 容器中,Gradle Daemon 是不必要的——容器是一次性执行的,Daemon 进程会在容器退出后成为孤儿进程,浪费资源。因此通过 org.gradle.daemon=false 禁用 Daemon。org.gradle.configureondemand=true 则启用了按需配置,只配置当前任务所需的项目,减少配置时间。

为什么这些配置很重要? 在 Docker 构建过程中,每一个 RUN 指令都会创建一个新的镜像层。Gradle Daemon 的启动和退出会产生大量的临时文件和进程,不仅浪费构建时间,还可能导致缓存失效。禁用 Daemon 可以让构建过程更加确定和可预测。

dockerfile
# 复制项目文件
COPY . .

将整个项目目录复制到容器的 /app 工作目录。这里有一个优化空间——可以通过 .dockerignore 文件排除不需要的文件(如 .gradlebuild.gitbin 等),减少构建上下文的大小,加速 COPY 操作。

一个推荐的 .dockerignore 文件内容如下:

.git
.gitignore
.gradle
build
bin
.idea
*.iml
.settings
.classpath
.project
node_modules
dockerfile
# 构建:强制生成 Spring Boot 可执行 JAR
RUN chmod 750 ./gradlew \
    && ./gradlew clean bootJar -x test --parallel --no-daemon

这是构建阶段的核心命令。bootJar 是 Spring Boot Gradle Plugin 提供的任务,用于生成可执行的 Fat JAR(也称为 Uber JAR)。-x test 跳过测试(测试应该在 CI/CD 流水线的独立阶段执行,而不是在镜像构建阶段),--parallel 启用并行构建,--no-daemon 再次确保不使用 Gradle Daemon。

关于 Fat JAR 的技术细节: Spring Boot 的 bootJar 任务会生成一个特殊的 JAR 文件,其内部结构如下:

cas-overlay.jar
├── META-INF/
│   └── MANIFEST.MF          # 包含 Main-Class 和 Start-Class
├── BOOT-INF/
│   ├── classes/              # 应用代码和资源
│   │   ├── application.yml
│   │   ├── cc/bima/cas/...
│   │   └── ...
│   └── lib/                  # 所有依赖 JAR
│       ├── cas-server-core-5.3.16.jar
│       ├── spring-boot-2.7.18.jar
│       ├── ...
│       └── (数百个依赖 JAR)
└── org/springframework/boot/loader/  # Spring Boot Loader
    ├── JarLauncher.class
    ├── LaunchedURLClassLoader.class
    └── ...

Spring Boot Loader 是一个特殊的类加载器,它能够在单个 JAR 文件中正确加载嵌套的依赖 JAR,使得 java -jar cas-overlay.jar 就能启动整个应用。

dockerfile
# 核心兜底:打印 JAR 列表,取第一个 JAR
RUN \
    echo "=== 构建产物 JAR 列表 ===" \
    && ls -l /app/build/libs/ \
    && mkdir -p /app/cas \
    && first_jar=$(ls /app/build/libs/*.jar | head -n 1) \
    && cp "$first_jar" /app/cas/cas.jar \
    && echo "=== 复制后的 CAS JAR ===" \
    && ls -l /app/cas/

这段代码体现了工程实践中的防御性编程思想。Gradle 的 bootJar 任务可能生成多个文件(如 cas-overlay.jarcas-overlay.jar.original),通过 ls | head -n 1 取第一个 JAR 文件,确保后续的 COPY --from=builder 操作有明确的文件路径。同时,打印 JAR 列表有助于在构建失败时快速定位问题。

注意: 在实际项目中,我们通过 build.gradle 中的 archiveFileName = 'cas-overlay.jar' 配置确保生成的 JAR 文件名是确定的。但这个兜底逻辑在构建配置被意外修改时提供了额外的安全保障。

2.3 运行阶段详解

运行阶段的目标是创建一个最小化的运行环境,只包含 CAS 运行所需的组件。

dockerfile
# 阶段2:纯运行
FROM azul/zulu-openjdk:8 AS runner
WORKDIR /docker/cas/jar

基础镜像选择 azul/zulu-openjdk:8,这是 Azul Systems 提供的 OpenJDK 发行版。选择 Azul Zulu 的原因包括:

  • 经过 TCK 认证: Azul Zulu 是通过 Java Technology Compatibility Kit(TCK)认证的 OpenJDK 构建,确保了与 Java 规范的完全兼容。
  • 长期支持: Azul 为每个 JDK 版本提供长期的安全更新和 bug 修复。
  • 镜像体积优化: 相比 openjdk:8 官方镜像,Azul Zulu 镜像通常更小,且包含更少的非必要组件。

为什么选择 JDK 而不是 JRE 基础镜像? 虽然理论上可以使用 JRE 基础镜像来进一步减小体积,但在实际操作中,许多 CAS 依赖(如某些监控工具、JMX 客户端)可能需要 JDK 中的工具类(如 tools.jar)。此外,从 JDK 11 开始,Oracle 不再提供单独的 JRE 下载,JRE 和 JDK 的界限逐渐模糊。因此,使用 JDK 基础镜像是一个更安全的选择。

dockerfile
# 1. 创建 CAS 所需目录,并赋予读写权限
RUN mkdir -p /etc/cas/config \
    && mkdir -p /etc/cas/services \
    && mkdir -p /etc/cas/saml \
    && chmod -R 777 /etc/cas

CAS 在运行时需要三个关键目录:

  • /etc/cas/config:存放 CAS 的配置文件(如 application.ymlcas.properties)。在容器化环境中,这个目录通常通过 Docker Volume 或 Kubernetes ConfigMap 进行挂载。
  • /etc/cas/services:存放服务注册 JSON 文件。CAS 通过这些文件来识别和管理下游应用。
  • /etc/cas/saml:存放 SAML 相关的证书和元数据文件(如果启用了 SAML 协议支持)。

chmod -R 777 /etc/cas 赋予了所有用户对这些目录的完全读写权限。虽然在生产环境中这不是最佳实践(应该使用更精细的权限控制),但在开发和测试环境中可以避免因权限问题导致的启动失败。

生产环境建议: 在生产环境中,应该创建专用的非 root 用户来运行 CAS,并使用更精细的权限控制。例如:

dockerfile
RUN groupadd -r casuser && useradd -r -g casuser -d /docker/cas casuser \
    && mkdir -p /etc/cas/config /etc/cas/services /etc/cas/saml \
    && chown -R casuser:casuser /etc/cas /docker/cas
USER casuser
dockerfile
# 2. 复制 keystore
COPY src/main/resources/etc/ssl/keystore.jks /etc/cas/thekeystore

将 SSL/TLS 密钥库复制到容器的固定路径 /etc/cas/thekeystore。CAS 使用这个密钥库来提供 HTTPS 服务和签名/验证 Ticket。

安全警示: 将密钥库直接打包进镜像是一种便捷但不安全的做法。在生产环境中,密钥库应该通过 Docker Volume 挂载或 Kubernetes Secret 注入,而不是打包进镜像。一旦镜像被推送到公开的 Registry,其中的密钥库就可能被提取和滥用。本文后面将详细讨论密钥库的安全管理方案。

dockerfile
# 3. 复制 services 目录
COPY src/main/resources/services /etc/cas/services

将服务注册文件目录复制到容器中。每个 JSON 文件定义了一个下游应用的访问策略。

dockerfile
# 4. 创建默认服务配置文件
RUN echo '{"@class":"org.apereo.cas.services.RegexRegisteredService","serviceId":"^https://.*","name":"HTTPS Services","id":1,"evaluationOrder":1}' > /etc/cas/services/HTTPS-10000001.json

创建一个默认的服务注册文件,允许所有 HTTPS 协议的下游应用接入 CAS。这是一个宽松的默认配置,仅用于开发和测试环境。在生产环境中,应该为每个下游应用创建独立的服务注册文件,明确指定允许的 Service ID、属性释放策略、认证要求等。

dockerfile
# 复制构建产物
COPY --from=builder /app/cas/cas.jar /docker/cas/jar/

从构建阶段复制 Fat JAR 到运行阶段。--from=builder 引用了前面定义的构建阶段。

dockerfile
# 复制 entrypoint.sh
COPY src/main/jib/docker/entrypoint.sh /docker/entrypoint.sh

# 设置权限 + 环境变量
RUN chmod +x /docker/entrypoint.sh
ENV PATH $PATH:$JAVA_HOME/bin:/docker/cas/jar

复制并设置启动脚本的执行权限,同时将 Java 可执行文件路径和 CAS 工作目录添加到 PATH 环境变量中。

2.4 端口暴露策略

dockerfile
# 暴露端口
EXPOSE 80 443 8080 8443 8444 8761 8888 5000

CAS 在容器化环境中暴露了 8 个端口,每个端口都有特定的用途:

端口协议用途说明
80HTTPHTTP 入口通常由反向代理转发,生产环境应禁用
443HTTPSHTTPS 入口CAS 的主要访问端口
8080HTTP备用 HTTP 端口Spring Boot 默认端口
8443HTTPS备用 HTTPS 端口常用于内网通信
8444HTTPSCAS 内部通信端口用于集群节点间通信
8761HTTPEureka 服务注册Spring Cloud Eureka Server 端口
8888HTTP管理端口Spring Boot Actuator 端口
5000TCP远程调试端口JVM 远程调试(JDWP)

端口暴露的最佳实践:

在生产环境中,不应该暴露所有端口。推荐的做法是:

  1. 只暴露必要的端口: 通常只需要 443(HTTPS)和 8443(内网 HTTPS)。
  2. 使用 Docker 网络隔离: 管理端口(8888)和调试端口(5000)应该只在 Docker 内部网络中可访问。
  3. 通过 Kubernetes Service 管理端口: 在 K8s 环境中,通过 Service 的 ports 定义来精确控制端口暴露。

2.5 密钥库挂载策略

CAS 的 SSL/TLS 密钥库(keystore)是安全基础设施的关键组件。在我们的实际项目中,密钥库的管理经历了从"打包进镜像"到"外部挂载"的演进。

方案一:打包进镜像(开发/测试环境)

这是 Dockerfile 中默认采用的方式:

dockerfile
COPY src/main/resources/etc/ssl/keystore.jks /etc/cas/thekeystore

优点是简单直接,不需要额外的配置。缺点是密钥库与镜像耦合,每次更新密钥库都需要重新构建镜像。更重要的是,如果镜像被推送到公开的 Registry,密钥库就会泄露。

方案二:Docker Volume 挂载(小型生产环境)

bash
docker run -d \
  -v /path/to/keystore.jks:/etc/cas/thekeystore:ro \
  -e CAS_SERVER_SSL_KEYSTORE_PASSWORD=changeit \
  apereo/cas:7.3.4

通过 -v 参数将宿主机上的密钥库文件以只读模式挂载到容器中。:ro 后缀确保容器不能修改密钥库文件。

方案三:Kubernetes Secret 注入(中大型生产环境)

yaml
apiVersion: v1
kind: Secret
metadata:
  name: cas-keystore
  namespace: cas
type: Opaque
data:
  thekeystore: <base64-encoded-keystore-content>
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cas-server
  namespace: cas
spec:
  template:
    spec:
      containers:
      - name: cas
        image: apereo/cas:7.3.4
        volumeMounts:
        - name: keystore-volume
          mountPath: /etc/cas/thekeystore
          subPath: thekeystore
          readOnly: true
        env:
        - name: CAS_SERVER_SSL_KEYSTORE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: cas-keystore-password
              key: password
      volumes:
      - name: keystore-volume
        secret:
          secretName: cas-keystore

Kubernetes Secret 以 Base64 编码存储密钥库内容,通过 Volume 挂载到容器中。密钥库密码通过另一个 Secret 注入为环境变量。

方案四:HashiCorp Vault(企业级安全环境)

对于安全要求极高的环境,可以使用 HashiCorp Vault 来管理密钥库:

yaml
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - name: cas
        image: apereo/cas:7.3.4
        env:
        - name: CAS_SERVER_SSL_KEYSTORE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: cas-vault-secret
              key: keystore-password
      initContainers:
      - name: vault-agent
        image: hashicorp/vault:1.15
        # Vault Agent 注入密钥库到共享卷

2.6 服务注册文件注入

CAS 的服务注册文件(JSON 格式)定义了哪些应用可以接入 SSO 系统。在容器化环境中,服务注册文件的管理方式需要仔细设计。

JSON 服务注册文件格式:

json
{
  "@class": "org.apereo.cas.services.RegexRegisteredService",
  "serviceId": "^https://app\\.example\\.com/.*",
  "name": "Example Application",
  "id": 10000001,
  "description": "示例应用的 SSO 集成配置",
  "evaluationOrder": 100,
  "attributeReleasePolicy": {
    "@class": "org.apereo.cas.services.ReturnAllowedAttributeReleasePolicy",
    "allowedAttributes": ["cn", "mail", "displayName"]
  },
  "accessStrategy": {
    "@class": "org.apereo.cas.services.DefaultRegisteredServiceAccessStrategy",
    "enabled": true,
    "ssoEnabled": true,
    "unauthorizedRedirectUrl": "https://app.example.com/unauthorized"
  },
  "properties": {
    "@class": "java.util.HashMap",
    "theme": {
      "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty",
      "values": ["java.util.HashSet", ["example-theme"]]
    }
  }
}

注入方式一:打包进镜像

dockerfile
COPY src/main/resources/services /etc/cas/services

适用于服务注册不频繁变更的场景。每次新增或修改服务都需要重新构建镜像。

注入方式二:Docker Volume 挂载

bash
docker run -d \
  -v /path/to/cas/services:/etc/cas/services:ro \
  apereo/cas:7.3.4

适用于服务注册需要频繁变更的场景。修改宿主机上的 JSON 文件后,重启容器即可生效。

注入方式三:Kubernetes ConfigMap

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: cas-services
  namespace: cas
data:
  HTTPS-10000001.json: |
    {
      "@class": "org.apereo.cas.services.RegexRegisteredService",
      "serviceId": "^https://app\\.example\\.com/.*",
      "name": "Example Application",
      "id": 10000001,
      "evaluationOrder": 100
    }
  OAUTH-10000002.json: |
    {
      "@class": "org.apereo.cas.services.OAuthRegisteredService",
      "serviceId": "^https://oauth\\.example\\.com/.*",
      "name": "OAuth Client",
      "id": 10000002,
      "clientSecret": "encrypted-secret",
      "supportedGrantTypes": ["java.util.HashSet", ["authorization_code", "refresh_token"]]
    }
---
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - name: cas
        volumeMounts:
        - name: services-volume
          mountPath: /etc/cas/services
          readOnly: true
      volumes:
      - name: services-volume
        configMap:
          name: cas-services

ConfigMap 方式支持通过 kubectl apply 动态更新服务注册,无需重新构建镜像或重启 Pod(CAS 的 JSON Service Registry 支持文件监听和自动重载)。

2.7 CAS 6.6 新增时区设置

在 CAS 6.6 的 Dockerfile 中,我们新增了时区设置:

dockerfile
# 设置上海时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
    && echo $TZ > /etc/timezone

为什么时区设置对 CAS 很重要?

CAS 作为认证系统,涉及大量与时间相关的操作:

  1. Ticket 过期时间: TGT 和 ST 都有严格的过期时间设置。如果容器时区不正确,Ticket 的过期判断可能出现偏差,导致用户频繁被要求重新登录,或者 Ticket 长期不过期带来安全风险。
  2. 审计日志时间戳: CAS 的审计日志记录了每次认证操作的时间戳。时区不一致会导致日志分析困难,影响安全事件的追溯。
  3. Kerberos 票据: 如果 CAS 与 Kerberos 集成,时间偏差不能超过 5 分钟(Kerberos 的默认时钟偏移容忍度)。
  4. SAML 断言: SAML 断言中的 IssueInstantNotOnOrAfter 属性使用 UTC 时间,但 CAS 在生成这些时间值时可能受到系统时区的影响。
  5. OAuth Token 过期: OAuth2.0 的 Access Token 和 Refresh Token 的过期时间同样依赖正确的系统时间。

CAS 5.3 为什么没有设置时区? 这是一个疏忽。在 CAS 5.3 的部署中,我们曾经遇到过因容器时区为 UTC 而导致的 Ticket 过期时间异常问题。在 CAS 6.6 中,我们通过 ENV TZ=Asia/Shanghai 修复了这个问题。

更通用的时区设置方案: 除了 TZ 环境变量外,还可以通过以下方式设置时区:

dockerfile
# 方案一:通过 tzdata 包(推荐)
RUN apt-get update && apt-get install -y tzdata \
    && ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
    && echo "Asia/Shanghai" > /etc/timezone \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

# 方案二:通过 JVM 参数
ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai"

# 方案三:通过 Spring Boot 配置
# application.yml
spring:
  jackson:
    time-zone: Asia/Shanghai
    date-format: yyyy-MM-dd HH:mm:ss

2.8 Gradle 与 Maven 构建的 Dockerfile 差异

在我们的实际项目中,CAS 5.3 和 6.6 同时维护了 Gradle 和 Maven 两套构建配置。对应的 Dockerfile 在构建阶段有所不同,但运行阶段完全一致。

Gradle 版本的构建阶段:

dockerfile
FROM gradle:7.5-jdk8 AS builder
WORKDIR /app

RUN mkdir -p ~/.gradle \
    && echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties \
    && echo "org.gradle.configureondemand=true" >> ~/.gradle/gradle.properties

COPY . .

RUN chmod 750 ./gradlew \
    && ./gradlew clean bootJar -x test --parallel --no-daemon

Maven 版本的构建阶段:

dockerfile
FROM maven:3.9.9-eclipse-temurin-8 AS builder
WORKDIR /app

RUN mkdir -p ~/.m2 \
    && echo "<settings><localRepository>/tmp/m2repo</localRepository><offline>false</offline><mirrors></mirrors></settings>" > ~/.m2/settings.xml

COPY . .

RUN chmod 755 ./mvnw || true \
    && (if [ -f ./mvnw ]; then ./mvnw clean package -DskipTests -T 1C; else mvn clean package -DskipTests -T 1C; fi)

关键差异分析:

差异点Gradle 版本Maven 版本
基础镜像gradle:7.5-jdk8maven:3.9.9-eclipse-temurin-8
构建工具配置~/.gradle/gradle.properties~/.m2/settings.xml
构建命令./gradlew clean bootJar -x test --parallel./mvnw clean package -DskipTests -T 1C
并行构建参数--parallel-T 1C(每个 CPU 核心一个线程)
Wrapper 支持./gradlew./mvnw(带 fallback)
产物目录build/libs/*.jartarget/*.jar

Maven 版本的防御性设计: 注意 Maven 版本中的 chmod 755 ./mvnw || trueif [ -f ./mvnw ]; then ... else mvn ...; fi。这种设计确保了即使 Maven Wrapper(mvnw)不存在或不可执行,也能回退到系统安装的 mvn 命令。这在 CI/CD 环境中特别有用,因为不同的构建 Agent 可能预装了不同版本的 Maven。

Maven 本地仓库隔离: Maven 版本通过 <localRepository>/tmp/m2repo</localRepository> 将本地仓库设置到 /tmp 目录下。这是一个 Docker 构建优化——在多阶段构建中,构建阶段的文件系统最终会被丢弃,将本地仓库放在 /tmp 下可以避免与宿主机的 Maven 缓存产生冲突。

2.9 entrypoint.sh 启动脚本解析

entrypoint.sh 是 CAS 容器的启动入口脚本,负责设置 JVM 参数并启动 CAS 应用。以下是 CAS 6.6/7.3 版本的启动脚本完整解析:

bash
#!/bin/sh

ENTRYPOINT_DEBUG=${ENTRYPOINT_DEBUG:-false}
JVM_DEBUG=${JVM_DEBUG:-false}
JVM_DEBUG_PORT=${JVM_DEBUG_PORT:-5000}
JVM_DEBUG_SUSPEND=${JVM_DEBUG_SUSPEND:-n}
# 优化内存参数(适配容器场景)
JVM_MEM_OPTS=${JVM_MEM_OPTS:--Xms512m -Xmx2048M}
# 修复1:移除 JDK21 废弃的 -noverify 参数 + 强制禁用 WatcherService(核心)
JVM_EXTRA_OPTS=${JVM_EXTRA_OPTS:--server -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Dcas.serviceRegistry.watcherEnabled=false -Dcas.serviceRegistry.json.location=file:/etc/cas/services}

环境变量默认值设计: 使用 ${VAR:-default} 语法为所有参数设置默认值,使得容器可以通过环境变量进行灵活配置,同时保证在不设置任何环境变量的情况下也能正常启动。

JVM 内存参数选择: -Xms512m -Xmx2048M 设置了初始堆内存为 512MB,最大堆内存为 2GB。这个配置适用于中等规模的单机部署。在容器环境中,需要确保 Xmx 值不超过容器的内存限制,否则容器可能被 OOM Killer 终止。

TieredCompilation 优化: -XX:+TieredCompilation -XX:TieredStopAtLevel=1 启用了分层编译,但只使用 C1 编译器(客户端编译器)。C1 编译器的编译速度更快,适用于启动时间敏感的容器场景。虽然 C1 编译器生成的代码性能不如 C2 编译器(服务端编译器),但对于大多数 CAS 部署场景来说,C1 的性能已经足够。

服务注册文件路径: -Dcas.serviceRegistry.json.location=file:/etc/cas/services 明确指定了服务注册文件的存储位置。-Dcas.serviceRegistry.watcherEnabled=false 禁用了文件监听器——在容器环境中,如果服务注册文件通过 ConfigMap 挂载,文件监听可能无法正常工作(ConfigMap 更新是通过符号链接实现的),禁用监听器可以避免不必要的错误日志。

bash
if [ $JVM_DEBUG = "true" ]; then
  # 修复:JDK9+ 改用 -agentlib:jdwp 替代 -Xdebug/-Xrunjdwp
  JVM_EXTRA_OPTS="${JVM_EXTRA_OPTS} -agentlib:jdwp=transport=dt_socket,address=*:${JVM_DEBUG_PORT},server=y,suspend=${JVM_DEBUG_SUSPEND}"
fi

远程调试支持。通过设置 JVM_DEBUG=true 环境变量启用 JDWP 远程调试。注意使用了 -agentlib:jdwp 而非旧版的 -Xdebug/-Xrunjdwp,因为后者在 JDK 9+ 中已被废弃。address=*:${JVM_DEBUG_PORT} 中的 * 表示监听所有网络接口,这在容器环境中是必要的(因为容器的 IP 地址不是固定的)。

bash
echo "\nRunning CAS @ /docker/cas/jar/cas.jar"
exec java $JVM_EXTRA_OPTS $JVM_MEM_OPTS -jar /docker/cas/jar/cas.jar "$@"

使用 exec 启动 Java 进程。exec 的作用是让 Java 进程替换 Shell 进程,成为容器的 PID 1 进程。这确保了 Docker 的信号处理(如 docker stop 发送的 SIGTERM)能够直接传递给 Java 进程,实现优雅关闭。

为什么 PID 1 很重要? 在 Linux 中,PID 1 进程(init 进程)有特殊的信号处理行为:它不会响应默认的信号处理器。如果 Shell 脚本是 PID 1,那么 docker stop 发送的 SIGTERM 信号会被 Shell 忽略,Docker 最终会等待超时后发送 SIGKILL 强制终止容器,导致 CAS 无法执行清理操作(如注销 Ticket、关闭数据库连接等)。使用 exec 可以避免这个问题。


第三章 CAS 7.3 现代容器化方案

3.1 Jib Gradle Plugin 深度解析

CAS 7.3 代表了容器化策略的一次根本性转变——从手动编写 Dockerfile 转向使用 Jib Gradle Plugin 进行云原生镜像构建。Jib 是 Google 开源的 Java 容器镜像构建工具,其核心理念是"不需要 Docker 守护进程,不需要 Dockerfile,就能构建 OCI 标准的容器镜像"。

在我们的 CAS 7.3 项目中,Jib 的版本为 3.5.3:

groovy
// build.gradle
buildscript {
    dependencies {
        classpath "com.google.cloud.tools:jib-gradle-plugin:${project.jibVersion}"
    }
}

apply plugin: "com.google.cloud.tools.jib"

对应 gradle.properties 中的版本定义:

properties
jibVersion=3.5.3

Jib 的核心优势:

  1. 无需 Docker 守护进程: Jib 直接与 Registry 通信,不需要本地安装 Docker。这在 CI/CD 环境中特别有用——构建 Agent 不需要 Docker-in-Docker(DinD)或 Docker Socket 挂载,降低了安全风险和运维复杂度。
  2. 分层优化: Jib 自动将 Java 应用拆分为多个层,包括基础层(依赖 JAR)、资源层(应用资源)和应用层(应用类文件)。这种分层策略利用了 Docker 的层缓存机制——当只有应用代码变更时,依赖层和资源层可以复用缓存,大幅加速构建。
  3. 可重现构建: Jib 的构建结果是确定性的——相同的项目源代码和配置总是生成相同的镜像。这有助于安全审计和合规性检查。
  4. OCI 标准兼容: Jib 生成的镜像符合 OCI(Open Container Initiative)镜像格式规范,可以推送到任何支持 OCI 的 Registry(Docker Hub、ECR、GCR、ACR 等)。

Jib 的工作原理:

Jib 的构建过程可以分为以下几个步骤:

1. 分析项目结构
   ├── 读取 build.gradle 中的依赖配置
   ├── 解析 Spring Boot Fat JAR 的内部结构
   └── 确定分层策略

2. 构建镜像层
   ├── 基础层:依赖 JAR(变化频率最低)
   ├── 资源层:应用资源文件(变化频率中等)
   └── 应用层:编译后的类文件(变化频率最高)

3. 生成镜像清单
   ├── 创建 OCI Image Manifest
   ├── 创建 Image Configuration
   └── 计算层的 Digest

4. 推送到 Registry
   ├── 认证(Docker Hub / ECR / GCR)
   ├── 上传镜像层(支持并行上传)
   └── 上传镜像清单

与传统的 Docker 构建相比,Jib 的最大区别在于它不需要启动一个容器来执行构建。Docker 的 docker build 命令实际上是在一个容器中执行 Dockerfile 中的指令,而 Jib 是在 JVM 中直接构建镜像的各层,然后通过 Registry 的 HTTP API 直接上传。

3.2 多平台镜像支持

CAS 7.3 的 Jib 配置支持多平台镜像构建,这是通过 platforms 配置实现的:

groovy
jib {
    from {
        image = project.baseDockerImage
        platforms {
            imagePlatforms.each {
                def given = it.split(":")
                platform {
                    architecture = given[0]
                    os = given[1]
                }
            }
        }
    }
}

对应 gradle.properties 中的配置:

properties
# 多平台镜像支持,逗号分隔
dockerImagePlatform=amd64:linux
# 也可以设置为: dockerImagePlatform=amd64:linux,arm64:linux

多平台镜像的实际意义:

随着 ARM 架构服务器(如 AWS Graviton、Apple Silicon)的普及,构建同时支持 x86_64 和 ARM64 的镜像变得越来越重要。在我们的项目中,dockerImagePlatform 默认设置为 amd64:linux,但可以轻松扩展为 amd64:linux,arm64:linux

多平台构建的注意事项:

  1. 基础镜像必须支持多平台: baseDockerImage 指定的镜像必须在所有目标平台上都有对应的版本。例如,azul/zulu-openjdk:21 同时提供 amd64 和 arm64 版本。
  2. Jib 的跨平台构建: Jib 本身不执行跨平台编译(它只是构建镜像清单),真正的跨平台构建需要借助 docker buildx 或 QEMU 模拟器。在 CI/CD 环境中,推荐使用 docker buildx 进行多平台构建。
  3. Registry 支持: 目标 Registry 必须支持 OCI 镜像索引(Image Index)或多架构清单(Manifest List)。Docker Hub、ECR、GCR 等主流 Registry 都支持这一特性。

3.3 Gradle Docker Plugin 集成

除了 Jib 之外,CAS 7.3 还集成了 Gradle Docker Plugin(版本 9.4.0),提供了传统的 Docker 构建和推送能力:

groovy
// build.gradle
buildscript {
    dependencies {
        classpath "com.bmuschko:gradle-docker-plugin:${project.gradleDockerPluginVersion}"
    }
}

apply plugin: "com.bmuschko.docker-remote-api"

对应 gradle.properties

properties
gradleDockerPluginVersion=9.4.0

casBuildDockerImage 任务:

groovy
import com.bmuschko.gradle.docker.tasks.image.*

tasks.register("casBuildDockerImage", DockerBuildImage) {
    dependsOn("build")

    def imageTag = "${project.'cas.version'}"
    inputDir = project.projectDir
    images.add("apereo/cas:${imageTag}${imageTagPostFix}")
    images.add("apereo/cas:latest${imageTagPostFix}")
    if (dockerUsername != null && dockerPassword != null) {
        username = dockerUsername
        password = dockerPassword
    }
    doLast {
        out.withStyle(Style.Success).println("Built CAS images successfully.")
    }
}

casPushDockerImage 任务:

groovy
tasks.register("casPushDockerImage", DockerPushImage) {
    dependsOn("casBuildDockerImage")

    def imageTag = "${project.'cas.version'}"
    images.add("apereo/cas:${imageTag}${imageTagPostFix}")
    images.add("apereo/cas:latest${imageTagPostFix}")

    if (dockerUsername != null && dockerPassword != null) {
        username = dockerUsername
        password = dockerPassword
    }
    doLast {
        out.withStyle(Style.Success).println("Pushed CAS images successfully.")
    }
}

Jib 与 Docker Plugin 的互补关系:

在我们的项目中,Jib 和 Docker Plugin 并不是互斥的,而是互补的:

场景推荐工具原因
CI/CD 自动化构建Jib无需 Docker 守护进程,构建更快更安全
本地开发和调试Docker Plugin可以直接使用项目中的 Dockerfile
多平台镜像Docker Plugin + buildxJib 的多平台支持有限
快速推送镜像Jib直接推送到 Registry,无需中间步骤
需要自定义构建步骤Docker PluginDockerfile 提供了更大的灵活性

使用方式:

bash
# 使用 Jib 构建并推送到 Docker Hub
./gradlew jib \
  -DdockerUsername=myuser \
  -DdockerPassword=mypassword

# 使用 Jib 构建到本地 Docker
./gradlew jibDockerBuild

# 使用 Docker Plugin 构建
./gradlew casBuildDockerImage

# 使用 Docker Plugin 构建并推送
./gradlew casPushDockerImage \
  -DdockerUsername=myuser \
  -DdockerPassword=mypassword

3.4 CycloneDX Gradle Plugin 与 SBOM

CAS 7.3 引入了 CycloneDX Gradle Plugin(版本 3.2.0),用于生成软件物料清单(Software Bill of Materials,SBOM)。这是 CAS 容器化方案中一个非常重要的安全和合规性增强。

groovy
// build.gradle
buildscript {
    dependencies {
        classpath "org.cyclonedx:cyclonedx-gradle-plugin:${project.gradleCyclonePluginVersion}"
    }
}

apply plugin: "org.cyclonedx.bom"

对应 gradle.properties

properties
# 创建所有 CAS 项目依赖的聚合 SBOM,
# 用于应用安全上下文和供应链组件分析
gradleCyclonePluginVersion=3.2.0

SBOM 与 bootJar 的集成:

在 CAS 7.3 的 springboot.gradle 中,CycloneDX 的 BOM 生成任务被集成到 bootJar 流程中:

groovy
// springboot.gradle
def wireOverlayDependency = { Task t ->
    ["processTestResources", "compileJava", "compileGraalJava"]
        .collect { tasks.findByName(it) }
        .findAll { it != null }
        .each { t.dependsOn(it) }
}
tasks.named("cyclonedxBom").configure { wireOverlayDependency(delegate) }
tasks.named("cyclonedxDirectBom").configure { wireOverlayDependency(delegate) }

bootJar {
    dependsOn cyclonedxBom
    // ...
}

这意味着每次执行 bootJar 构建时,都会自动生成 SBOM 文件。SBOM 文件通常输出到 build/reports/cyclonedx/ 目录下,包含 bom.jsonbom.xml 两种格式。

SBOM 的详细内容将在第五章深入讨论。

3.5 Foojay Gradle Plugin 与 JDK 自动下载

CAS 7.3 引入了 Foojay Gradle Plugin(版本 1.0.0),用于自动下载和管理 JDK 工具链:

groovy
// settings.gradle
plugins {
    id "org.gradle.toolchains.foojay-resolver-convention" version "${gradleFoojayPluginVersion}"
}

对应 gradle.properties

properties
# 控制 Gradle 工具链所需的 JDK 分发版的发现和下载
gradleFoojayPluginVersion=1.0.0

Foojay Plugin 的工作原理:

Foojay Labs 维护了一个全球性的 JDK 分发版元数据服务(foojay.io),Foojay Gradle Plugin 通过查询这个服务来发现和下载指定版本和供应商的 JDK。当 Gradle 的 Java Toolchain 需要一个本地不存在的 JDK 版本时,Foojay Plugin 会自动从 foojay.io 下载对应的 JDK 发行版。

与 Jib 的协同: Foojay Plugin 解决的是"构建时"的 JDK 问题(确保构建环境有正确的 JDK 版本),而 Jib 解决的是"运行时"的基础镜像问题(确保镜像中有正确的 JRE/JDK)。两者协同工作,实现了从构建到运行的全链路 JDK 版本管理。

支持的 JVM 供应商:

在我们的 gradle.properties 中,jvmVendor 设置为 AMAZON

properties
jvmVendor=AMAZON

Foojay Plugin 支持的 JVM 供应商包括:AMAZON(Corretto)、ADOPTIUM(Eclipse Temurin)、JETBRAINS、MICROSOFT、ORACLE、SAP、BELLSOFT(Liberica)等。

3.6 Jib 配置详解

CAS 7.3 的 Jib 配置非常完整,涵盖了镜像构建的各个方面。让我们逐块解析:

3.6.1 基础镜像配置(from)

groovy
jib {
    from {
        image = project.baseDockerImage
        platforms {
            imagePlatforms.each {
                def given = it.split(":")
                platform {
                    architecture = given[0]
                    os = given[1]
                }
            }
        }
    }
}

对应 gradle.properties

properties
baseDockerImage=azul/zulu-openjdk:21
dockerImagePlatform=amd64:linux

基础镜像选择 azul/zulu-openjdk:21,与 CAS 7.3 要求的 JDK 21 匹配。platforms 块通过解析 dockerImagePlatform 属性动态生成平台列表。

教学简化版配置:

groovy
jib {
    from {
        image = 'azul/zulu-openjdk:21'
        platforms {
            platform {
                architecture = 'amd64'
                os = 'linux'
            }
        }
    }
}

3.6.2 目标镜像配置(to)

groovy
    to {
        image = "${project.'containerImageOrg'}/${project.'containerImageName'}:${project.version}"
        credHelper = "osxkeychain"
        if (dockerUsername != null && dockerPassword != null) {
            auth {
                username = "${dockerUsername}"
                password = "${dockerPassword}"
            }
        }
        tags = [project.version]
    }

对应 gradle.properties

properties
containerImageOrg=apereo
containerImageName=cas

目标镜像的完整名称为 apereo/cas:7.3.4credHelper = "osxkeychain" 使用 macOS 的 Keychain 来存储 Docker Registry 的认证信息。在生产环境中,通常使用用户名密码认证或 IAM 角色。

支持的目标 Registry 类型:

groovy
// Amazon ECR
credHelper = "ecr-login"

// Google GCR
credHelper = "gcr"

// Docker Hub(macOS)
credHelper = "osxkeychain"

// 通用用户名密码认证
auth {
    username = "myuser"
    password = "mypassword"
}

3.6.3 容器配置(container)

groovy
    container {
        creationTime = "USE_CURRENT_TIMESTAMP"
        entrypoint = ['/docker/entrypoint.sh']
        ports = ['80', '443', '8080', '8443', '8444', '8761', '8888', '5000']
        labels = [version:project.version, name:project.name, group:project.group, org:project.containerImageOrg]
        workingDirectory = '/docker/cas/war'
    }

关键配置说明:

  • creationTime = "USE_CURRENT_TIMESTAMP":使用当前时间作为镜像的创建时间。默认情况下,Jib 使用固定的时间戳(基于项目构建时间),这会导致镜像层缓存失效。使用 USE_CURRENT_TIMESTAMP 可以避免这个问题。
  • entrypoint = ['/docker/entrypoint.sh']:指定容器的启动命令。注意这里使用的是 JSON 数组格式(exec form),而不是 Shell 字符串格式(shell form)。exec form 的信号处理更可靠。
  • ports:声明容器暴露的端口列表。与 Dockerfile 的 EXPOSE 指令等效。
  • labels:为镜像添加元数据标签。这些标签可以用于镜像的搜索、过滤和管理。
  • workingDirectory = '/docker/cas/war':设置容器的工作目录。注意这里是 /docker/cas/war 而不是 Dockerfile 中的 /docker/cas/jar,这可能是 CAS 7.3 的一个命名变更(从 JAR 切换到 WAR 的术语遗留)。

3.6.4 额外目录配置(extraDirectories)

groovy
    extraDirectories {
        paths {
          path {
            from = file('src/main/jib')
          }
          path {
            from = file('etc/cas')
            into = '/etc/cas'
          }
          path {
            from = file("build/libs")
            into = "/docker/cas/war"
          }
        }
        permissions = [
            '/docker/entrypoint.sh': '755'
        ]
    }

extraDirectories 是 Jib 中最强大的配置之一,它允许将额外的文件和目录包含到镜像中。在我们的项目中,包含了三个目录:

  1. src/main/jib:包含 entrypoint.sh 启动脚本和可能的 Docker 配置文件。这个目录的内容会被复制到镜像的根目录。
  2. etc/cas:包含 CAS 的配置文件和服务注册文件,复制到镜像的 /etc/cas 目录。
  3. build/libs:包含构建产物(Fat JAR),复制到镜像的 /docker/cas/war 目录。

permissions 块设置了特定文件的权限。/docker/entrypoint.sh: '755' 确保启动脚本有执行权限。

教学简化版配置:

groovy
jib {
    extraDirectories {
        paths {
            path {
                from = file('src/main/jib')
            }
            path {
                from = file('etc/cas')
                into = '/etc/cas'
            }
            path {
                from = file('build/libs')
                into = '/docker/cas/war'
            }
        }
        permissions = [
            '/docker/entrypoint.sh': '755'
        ]
    }
}

3.6.5 安全配置

groovy
    allowInsecureRegistries = project.allowInsecureRegistries

对应 gradle.properties

properties
allowInsecureRegistries=false

默认禁止使用不安全的(HTTP)Registry。在生产环境中,应该始终使用 HTTPS 与 Registry 通信。

3.7 配置缓存兼容性处理

CAS 7.3 的构建配置中包含了一段关于 Gradle Configuration Cache 兼容性的处理代码:

groovy
def configurationCacheRequested = services.get(BuildFeatures).configurationCache.requested.getOrElse(true)

['jibDockerBuild', 'jibBuildTar', 'jib'].each { taskName ->
    getTasksByName(taskName, true).each(it -> {
        it.notCompatibleWithConfigurationCache("Jib is not compatible with configuration cache");
        it.enabled = !configurationCacheRequested
    })
}

什么是 Gradle Configuration Cache?

Gradle Configuration Cache 是 Gradle 7.x 引入的性能优化特性。它将构建的配置阶段(Configuration Phase)结果缓存到磁盘,在后续构建中直接复用缓存,跳过配置阶段的执行。对于大型项目,这可以显著减少构建时间。

Jib 与 Configuration Cache 的冲突:

Jib 插件在配置阶段会读取系统属性(如 dockerUsernamedockerPassword),这些动态值无法被 Configuration Cache 正确序列化和恢复。因此,Jib 目前不兼容 Configuration Cache。

CAS 7.3 的处理策略:

  1. 检测 Configuration Cache 是否被启用。
  2. 如果启用,则禁用所有 Jib 相关任务(jibDockerBuildjibBuildTarjib),并输出提示信息。
  3. 用户可以通过 --no-configuration-cache 命令行选项临时禁用 Configuration Cache 来执行 Jib 任务。

对应的 gradle.properties 配置:

properties
org.gradle.unsafe.configuration-cache=false
org.gradle.unsafe.configuration-cache-problems=warn

实际使用中的建议:

bash
# 日常构建(启用 Configuration Cache,加速构建)
./gradlew build

# 构建并推送 Docker 镜像(需要禁用 Configuration Cache)
./gradlew jib --no-configuration-cache \
  -DdockerUsername=myuser \
  -DdockerPassword=mypassword

# 或者直接关闭 Configuration Cache
./gradlew jib -DdockerUsername=myuser -DdockerPassword=mypassword

3.8 CAS 7.3 Gradle 构建体系全貌

综合以上分析,CAS 7.3 的 Gradle 构建体系包含以下关键组件:

CAS 7.3 Gradle 构建体系
├── 构建工具
│   ├── Gradle 9.1.0
│   ├── Spring Boot Gradle Plugin 3.5.6
│   ├── FreeFair Gradle Plugin 9.2.0
│   └── Lombok Plugin 1.18.42

├── 容器化工具
│   ├── Jib Gradle Plugin 3.5.3(云原生镜像构建)
│   ├── Gradle Docker Plugin 9.4.0(传统 Docker 构建)
│   └── Foojay Plugin 1.0.0(JDK 自动下载)

├── 安全工具
│   └── CycloneDX Plugin 3.2.0(SBOM 生成)

├── 辅助工具
│   ├── Gradle Download Task 5.6.0
│   └── CAS Configuration Metadata

└── 构建流程
    ├── compileJava → 编译 Java 源代码
    ├── processResources → 处理资源文件
    ├── cyclonedxBom → 生成 SBOM
    ├── bootJar → 生成可执行 JAR(依赖 cyclonedxBom)
    ├── jib → 构建并推送 OCI 镜像
    ├── casBuildDockerImage → 传统 Docker 构建
    └── casPushDockerImage → 传统 Docker 推送

JDK 版本管理:

CAS 7.3 的 JDK 版本管理通过 Gradle Toolchain + Foojay Plugin 实现:

groovy
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(project.targetCompatibility)

        def chosenJvmVendor = null
        if (project.jvmVendor != null) {
            try {
                chosenJvmVendor = JvmVendorSpec."${project.jvmVendor?.toUpperCase()}"
            } catch (MissingPropertyException e) {
                try {
                    chosenJvmVendor = JvmVendorSpec.of(project.jvmVendor?.toUpperCase())
                } catch (MissingMethodException ex) {
                    // 回退处理
                }
            }
        }
        if (chosenJvmVendor != null) {
            vendor = chosenJvmVendor
        }
    }
}

这段代码兼容了 Gradle 7.x 和 8.x 的 JvmVendorSpec API 差异,确保在不同版本的 Gradle 上都能正确设置 JVM 供应商。


第四章 Jib vs Dockerfile vs Buildpacks 对比

在 CAS 的容器化实践中,我们经历了从 Dockerfile 到 Jib 的演进。Spring Boot 生态还提供了第三种选择——Buildpacks(通过 spring-boot:build-image)。本节将对这三种方案进行全面对比。

4.1 构建速度对比

测试场景: 在相同的硬件环境(8 核 CPU、16GB 内存、SSD)下,对 CAS 7.3 Overlay 项目进行首次构建和增量构建。

方案首次构建增量构建(仅代码变更)增量构建(依赖变更)
Dockerfile(两阶段)~8 分钟~5 分钟~7 分钟
Jib~3 分钟~30 秒~2 分钟
Buildpacks~5 分钟~2 分钟~4 分钟

分析:

Jib 在构建速度上有显著优势,原因在于:

  1. 无需启动容器: Dockerfile 的每个 RUN 指令都需要启动一个容器来执行,容器启动和停止的开销不可忽视。
  2. 分层缓存更高效: Jib 将依赖和应用代码分为不同的层,只有变更的层需要重新构建和推送。Dockerfile 的层缓存基于指令级别的匹配,粒度较粗。
  3. 并行上传: Jib 支持并行上传镜像层到 Registry,而 Docker 的 docker push 是串行的。

Buildpacks 的速度介于两者之间。 Buildpacks 使用了层缓存机制,但每次构建都需要运行检测(detect)和构建(build)阶段,有一定的固定开销。

4.2 镜像体积对比

方案基础镜像最终镜像体积层数
Dockerfile(azul/zulu-openjdk:21)~450MB~650MB12-15 层
Jib(azul/zulu-openjdk:21)~450MB~620MB6-8 层
Buildpacks(Paketo BellSoft Liberica)~400MB~580MB8-10 层

分析:

Jib 的镜像体积略小,层数更少。这是因为 Jib 只创建必要的层(基础层、依赖层、资源层、应用层),而 Dockerfile 的每个 RUNCOPYENV 指令都会创建一个新的层。

Buildpacks 的基础镜像(BellSoft Liberica)比 Azul Zulu 略小,因此最终镜像体积最小。但 Buildpacks 的基础镜像选择有限,可能不适用于所有场景。

4.3 安全性对比

安全维度DockerfileJibBuildpacks
不需要 root 权限构建否(需要 Docker daemon)否(需要 Docker daemon)
不需要 Docker daemon
不需要 Dockerfile
镜像层最小化需要手动优化自动优化自动优化
可重现构建需要额外配置内置支持需要额外配置
镜像签名需要额外工具需要额外工具部分支持

分析:

Jib 在安全性方面有显著优势:

  1. 无需 Docker daemon: 在 CI/CD 环境中,Docker daemon 通常需要 --privileged 模式或 Docker Socket 挂载,这带来了容器逃逸的风险。Jib 不需要 Docker daemon,可以在一个普通的容器中安全运行。
  2. 无需 Dockerfile: Dockerfile 中的 RUN 指令可能执行任意命令,存在供应链攻击的风险。Jib 通过声明式配置替代了命令式指令,减少了攻击面。
  3. 可重现构建: Jib 的构建结果是确定性的,相同输入总是产生相同输出。这对于安全审计和合规性检查非常重要。

4.4 多平台支持对比

方案多平台支持实现方式复杂度
Dockerfile原生支持docker buildx build --platform中等
Jib有限支持platforms 配置 + docker buildx较高
Buildpacks原生支持pack build --platform

分析:

Buildpacks 在多平台支持方面最为友好,通过 --platform 参数即可指定目标平台,底层自动处理交叉编译和基础镜像选择。

Dockerfile 通过 docker buildx 插件支持多平台构建,需要预先创建 buildx builder 实例。

Jib 的多平台支持需要配合 docker buildx 使用,配置相对复杂。Jib 本身只生成单平台镜像,多平台镜像索引需要通过 docker buildxdocker manifest 手动创建。

4.5 CI/CD 集成对比

集成维度DockerfileJibBuildpacks
Jenkins 集成需要安装 Docker无需 Docker需要安装 Pack CLI
GitHub Actions内置支持内置支持内置支持
GitLab CI内置支持内置支持内置支持
缓存利用Docker 层缓存Jib 缓存Buildpacks 缓存
构建产物管理需要手动管理自动管理自动管理

分析:

Jib 在 CI/CD 集成方面最为简洁。由于不需要 Docker daemon,Jib 可以在任何支持 JVM 的构建 Agent 上运行,无需额外的系统级依赖。这大大简化了 CI/CD 流水线的配置和维护。

4.6 综合评估与选型建议

评估维度DockerfileJibBuildpacks
构建速度★★★★★★★★★★★★
镜像体积★★★★★★★★★★★★
安全性★★★★★★★★★★★
多平台支持★★★★★★★★★★★★
CI/CD 集成★★★★★★★★★★★★
灵活性★★★★★★★★★★★
学习曲线★★★★★★★★★★★
社区生态★★★★★★★★★★★★★

选型建议:

  • 如果你是 CAS Overlay 项目的新用户: 推荐使用 Jib。CAS 7.3 官方已经预配置了 Jib,开箱即用。Jib 的声明式配置更易于理解和维护。
  • 如果你需要高度定制化的构建过程: 使用 Dockerfile。Dockerfile 提供了最大的灵活性,可以在构建过程中执行任意命令。
  • 如果你追求极致的镜像优化和标准化: 使用 Buildpacks。Buildpacks 由 Spring Boot 团队维护,与 Spring Boot 生态深度集成。
  • 在生产环境中,推荐组合使用: 使用 Jib 作为主要的镜像构建工具,同时保留 Dockerfile 用于本地开发和调试。

第五章 SBOM(软件物料清单)

5.1 CycloneDX 格式规范

CycloneDX 是一个开源的软件物料清单标准,由 OWASP(Open Web Application Security Project)维护。它定义了一个轻量级的 BOM(Bill of Materials)格式,用于描述软件组件的组成、依赖关系和元数据。

CycloneDX 的核心概念:

  1. Component(组件): 软件的一个组成部分,可以是库、框架、模块或服务。每个组件有唯一的标识(group、name、version)。
  2. Dependency(依赖关系): 组件之间的依赖关系,形成依赖图。
  3. Vulnerability(漏洞): 与组件相关的安全漏洞信息。
  4. Service(服务): 如果软件以服务形式提供,可以描述服务的端点、认证方式等。

CycloneDX JSON 格式示例(简化版):

json
{
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
  "version": 1,
  "metadata": {
    "timestamp": "2026-04-09T12:00:00.000Z",
    "tools": [
      {
        "vendor": "CycloneDX",
        "name": "cyclonedx-gradle-plugin",
        "version": "3.2.0"
      }
    ],
    "component": {
      "group": "org.apereo.cas",
      "name": "cas-overlay",
      "version": "7.3.4",
      "type": "application"
    }
  },
  "components": [
    {
      "group": "org.apereo.cas",
      "name": "cas-server-core",
      "version": "7.3.4",
      "type": "library",
      "purl": "pkg:maven/org.apereo.cas/cas-server-core@7.3.4"
    },
    {
      "group": "org.springframework.boot",
      "name": "spring-boot-starter-web",
      "version": "3.5.6",
      "type": "library",
      "purl": "pkg:maven/org.springframework.boot/spring-boot-starter-web@3.5.6"
    }
  ],
  "dependencies": [
    {
      "ref": "pkg:maven/org.apereo.cas/cas-overlay@7.3.4",
      "dependsOn": [
        "pkg:maven/org.apereo.cas/cas-server-core@7.3.4",
        "pkg:maven/org.springframework.boot/spring-boot-starter-web@3.5.6"
      ]
    }
  ]
}

purl(Package URL)格式: CycloneDX 使用 purl 作为组件的唯一标识符。purl 的格式为 pkg:type/namespace/name@version,例如 pkg:maven/org.apereo.cas/cas-server-core@7.3.4。purl 是一个跨生态系统的标准,支持 Maven、npm、PyPI、NuGet 等多种包管理器。

5.2 供应链安全要求

近年来,软件供应链安全成为了行业关注的焦点。2021 年的 SolarWinds 事件和 Log4Shell 漏洞(CVE-2021-44228)暴露了软件供应链中的安全风险。各国政府和行业组织纷纷出台了相关法规和标准:

  • 美国: EO 14028(改善国家网络安全)要求所有向联邦政府销售的软件必须提供 SBOM。
  • 欧盟: 网络和信息安全指令(NIS2)要求关键基础设施运营商管理其供应链安全风险。
  • 中国: 《网络安全标准实践指南——软件物料清单(SBOM)构建指南》提供了 SBOM 的构建指导。

SBOM 在供应链安全中的作用:

  1. 漏洞管理: 当一个新的安全漏洞被披露时(如 Log4Shell),SBOM 可以快速确定哪些应用受到了影响。
  2. 许可证合规: SBOM 包含每个组件的许可证信息,可以帮助组织确保其软件不违反任何开源许可证。
  3. 版本追踪: SBOM 记录了每个组件的精确版本,有助于追踪和管理组件的升级。
  4. 审计与合规: SBOM 是安全审计和合规性检查的重要输入。

5.3 CAS 7.3 中 CycloneDX 集成到 bootJar 流程

CAS 7.3 的一个重要改进是将 CycloneDX SBOM 生成集成到了 bootJar 构建流程中。这意味着每次构建 CAS 的可执行 JAR 时,都会自动生成对应的 SBOM。

集成实现:

groovy
// springboot.gradle
def wireOverlayDependency = { Task t ->
    ["processTestResources", "compileJava", "compileGraalJava"]
        .collect { tasks.findByName(it) }
        .findAll { it != null }
        .each { t.dependsOn(it) }
}
tasks.named("cyclonedxBom").configure { wireOverlayDependency(delegate) }
tasks.named("cyclonedxDirectBom").configure { wireOverlayDependency(delegate) }

bootJar {
    dependsOn cyclonedxBom
    def executable = project.hasProperty("executable") && Boolean.valueOf(project.getProperty("executable"))
    if (executable) {
        logger.info "Including launch script for executable JAR artifact"
        launchScript()
    }
    archiveFileName = "cas.jar"
    archiveBaseName = "cas"
    entryCompression = ZipEntryCompression.STORED
}

任务依赖链:

compileJava → processTestResources → cyclonedxBom → bootJar

cyclonedxBom 任务依赖于 compileJavaprocessTestResources,确保在生成 SBOM 时所有依赖已经解析完毕。bootJar 依赖于 cyclonedxBom,确保 JAR 构建完成后 SBOM 也已生成。

生成的 SBOM 文件:

执行 ./gradlew bootJar 后,SBOM 文件生成在以下位置:

build/
├── reports/
│   └── cyclonedx/
│       ├── bom.json          # CycloneDX JSON 格式
│       └── bom.xml           # CycloneDX XML 格式
├── libs/
│   └── cas.jar               # CAS 可执行 JAR

SBOM 内容分析:

对于 CAS 7.3 Overlay 项目,生成的 SBOM 通常包含以下组件类别:

组件类别数量(约)示例
CAS 核心模块40+cas-server-core, cas-server-support-oauth
Spring Boot 组件20+spring-boot-starter-web, spring-boot-autoconfigure
Spring Framework15+spring-core, spring-web, spring-security
第三方库100+jackson, tomcat-embed, mybatis, mysql-connector
Webjars10+jquery, bootstrap, font-awesome

5.4 SBOM 在安全审计中的应用

场景一:快速漏洞响应

当一个新的 CVE 被披露时,安全团队可以使用 SBOM 快速评估影响范围:

bash
# 假设 CVE-2024-XXXXX 影响 jackson-databind 2.13.x
# 检查 SBOM 中是否包含受影响的版本
jq '.components[] | select(.name == "jackson-databind")' build/reports/cyclonedx/bom.json

# 输出示例:
{
  "group": "com.fasterxml.jackson.core",
  "name": "jackson-databind",
  "version": "2.13.4.2",
  "type": "library",
  "purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.4.2"
}

场景二:许可证合规检查

bash
# 提取所有组件的许可证信息
jq '.components[] | {name, version, licenses: .licenses}' build/reports/cyclonedx/bom.json

# 检查是否包含 GPL 许可证的组件(可能存在合规风险)
jq '.components[] | select(.licenses[]?.id == "GPL-3.0")' build/reports/cyclonedx/bom.json

场景三:与 Dependency-Track 集成

Dependency-Track 是 OWASP 提供的持续漏洞分析平台,可以自动导入 CycloneDX SBOM 并持续监控组件的安全状态。

yaml
# Jenkins Pipeline 中上传 SBOM 到 Dependency-Track
pipeline {
    stages {
        stage('Upload SBOM') {
            steps {
                sh 'curl -X POST \
                  -H "X-API-Key: ${DT_API_KEY}" \
                  -H "Content-Type: multipart/form-data" \
                  -F "project=cas-overlay" \
                  -F "bom=@build/reports/cyclonedx/bom.json" \
                  https://dependency-track.example.com/api/v1/bom'
            }
        }
    }
}

场景四:镜像构建时嵌入 SBOM

在云原生环境中,SBOM 不仅可以作为独立的文件存在,还可以嵌入到 OCI 镜像中。Jib 支持将 SBOM 作为镜像层的一部分:

groovy
jib {
    extraDirectories {
        paths {
            path {
                from = file('build/reports/cyclonedx')
                into = '/sbom'
            }
        }
    }
}

这样,SBOM 就成为了镜像的一部分,可以通过 docker cpkubectl cp 提取出来进行审计。


第六章 CI/CD 流水线设计

6.1 Jenkins Pipeline 设计

基于我们实际的 CAS Overlay 项目,以下是一个完整的 Jenkins Pipeline 设计,覆盖了从代码提交到生产部署的全流程。

groovy
// Jenkinsfile
pipeline {
    agent {
        kubernetes {
            yaml '''
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: gradle
    image: gradle:9.1.0-jdk21
    command: ['sleep', '99d']
    resources:
      limits:
        memory: "4Gi"
        cpu: "2"
    volumeMounts:
    - name: gradle-cache
      mountPath: /home/gradle/.gradle
  - name: docker
    image: docker:24.0
    command: ['sleep', '99d']
    volumeMounts:
    - name: docker-sock
      mountPath: /var/run/docker.sock
  volumes:
  - name: gradle-cache
    persistentVolumeClaim:
      claimName: gradle-cache-pvc
  - name: docker-sock
    hostPath:
      path: /var/run/docker.sock
'''
        }
    }

    environment {
        REGISTRY = 'registry.example.com'
        IMAGE_NAME = 'cas/cas-overlay'
        IMAGE_TAG = "${env.BUILD_NUMBER}"
        CREDENTIALS_ID = 'docker-registry-credentials'
    }

    parameters {
        choice(name: 'CAS_VERSION', choices: ['5.3', '6.6', '7.3'], description: 'CAS 版本')
        choice(name: 'DEPLOY_TARGET', choices: ['dev', 'staging', 'production'], description: '部署目标')
        booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: '跳过测试')
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Build') {
            steps {
                container('gradle') {
                    sh '''
                        chmod +x gradlew
                        ./gradlew clean bootJar \
                            --no-daemon \
                            --parallel \
                            -Dcas.version=${CAS_VERSION}
                    '''
                }
            }
        }

        stage('Test') {
            when {
                expression { return !params.SKIP_TESTS }
            }
            steps {
                container('gradle') {
                    sh './gradlew test --no-daemon'
                }
            }
            post {
                always {
                    junit '**/build/test-results/test/*.xml'
                }
            }
        }

        stage('SBOM Generation') {
            when {
                expression { return params.CAS_VERSION == '7.3' }
            }
            steps {
                container('gradle') {
                    sh './gradlew cyclonedxBom --no-daemon'
                }
            }
        }

        stage('Docker Build (Jib)') {
            steps {
                container('gradle') {
                    withCredentials([usernamePassword(
                        credentialsId: env.CREDENTIALS_ID,
                        usernameVariable: 'DOCKER_USERNAME',
                        passwordVariable: 'DOCKER_PASSWORD'
                    )]) {
                        sh '''
                            ./gradlew jib \
                                --no-daemon \
                                --no-configuration-cache \
                                -DdockerUsername=${DOCKER_USERNAME} \
                                -DdockerPassword=${DOCKER_PASSWORD} \
                                -DdockerImageTagPostfix=-${DEPLOY_TARGET}
                        '''
                    }
                }
            }
        }

        stage('Security Scan') {
            steps {
                container('gradle') {
                    sh '''
                        # Trivy 镜像安全扫描
                        trivy image \
                            --severity HIGH,CRITICAL \
                            --exit-code 1 \
                            ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}
                    '''
                }
            }
        }

        stage('Deploy to K8s') {
            steps {
                container('docker') {
                    sh """
                        kubectl set image \
                            deployment/cas-server \
                            cas=${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} \
                            -n cas-${DEPLOY_TARGET}
                    """
                }
            }
        }
    }

    post {
        success {
            echo "CAS ${params.CAS_VERSION} 部署到 ${params.DEPLOY_TARGET} 成功"
        }
        failure {
            echo "构建或部署失败,请检查日志"
        }
        always {
            cleanWs()
        }
    }
}

6.2 构建阶段:Gradle/Maven 构建

构建阶段是 CI/CD 流水线的核心环节,负责将 CAS Overlay 项目编译打包为可执行的 Fat JAR。

Gradle 构建优化策略:

bash
# CAS 7.3 的推荐构建命令
./gradlew clean bootJar \
    --no-daemon \
    --parallel \
    --build-cache \
    --configuration-cache

关键参数说明:

  • --no-daemon:在 CI/CD 环境中禁用 Gradle Daemon。Daemon 进程在 CI Agent 上可能不会被正确清理,导致资源泄漏。
  • --parallel:启用并行构建,利用多核 CPU 加速编译。
  • --build-cache:启用构建缓存。Gradle 会将编译结果缓存到本地或远程缓存服务器,后续构建可以复用缓存。
  • --configuration-cache:启用配置缓存。对于 CAS 7.3 项目,配置缓存可以减少约 30% 的配置时间。

Maven 构建优化策略:

bash
# CAS 5.3/6.6 Maven 版本的推荐构建命令
./mvnw clean package \
    -DskipTests \
    -T 1C \
    -o \
    --batch-mode
  • -T 1C:每个 CPU 核心一个线程,启用并行构建。
  • -o:离线模式,使用本地仓库缓存,不远程下载依赖。
  • --batch-mode:批处理模式,禁用交互式输入和进度日志。

Gradle 构建缓存的远程配置:

在多 Agent 的 CI/CD 环境中,推荐配置远程构建缓存:

groovy
// settings.gradle
buildCache {
    local {
        enabled = true
        directory = "${rootProject.buildDir}/.gradle/build-cache"
    }
    remote<HttpBuildCache> {
        enabled = true
        url = 'https://cache.example.com/cache/'
        push = System.getenv('CI') != null  // 只在 CI 环境中推送缓存
        credentials {
            username = System.getenv('CACHE_USERNAME')
            password = System.getenv('CACHE_PASSWORD')
        }
    }
}

6.3 测试阶段:单元测试 + 集成测试

CAS Overlay 项目的测试分为两个层次:

单元测试:

bash
./gradlew test --no-daemon

CAS 的单元测试主要覆盖以下领域:

  • 认证处理器(Authentication Handler)测试
  • 属性释放策略(Attribute Release Policy)测试
  • 服务注册匹配(Service Matching)测试
  • Ticket 生命周期管理测试

集成测试:

CAS 的集成测试通常需要外部依赖(如数据库、Redis、LDAP)。在 CI/CD 环境中,可以使用 Testcontainers 自动管理这些依赖:

groovy
// build.gradle (测试依赖)
dependencies {
    testImplementation "org.springframework.boot:spring-boot-starter-test"
    testImplementation "org.testcontainers:junit-jupiter"
    testImplementation "org.testcontainers:mysql"
    testImplementation "org.testcontainers:redis"
}

// 使用 Testcontainers 的集成测试示例
@Testcontainers
class CasIntegrationTest {
    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
        .withDatabaseName("cas_db")
        .withUsername("cas")
        .withPassword("cas");

    @Container
    static GenericContainer<?> redis = new GenericContainer<>("redis:7")
        .withExposedPorts(6379);

    @Test
    void testAuthenticationWithDatabase() {
        // 使用 Testcontainers 提供的 MySQL 和 Redis 进行集成测试
    }
}

测试报告与质量门禁:

groovy
// Jenkins Pipeline 中的测试报告
post {
    always {
        junit '**/build/test-results/test/*.xml'
    }
}

推荐设置测试覆盖率阈值:

groovy
// build.gradle 中的 JaCoCo 配置
jacocoTestCoverageVerification {
    violationRules {
        rule {
            limit {
                minimum = 0.60  // 最低 60% 的代码覆盖率
            }
        }
    }
}

6.4 镜像构建:Jib/Docker

在 CI/CD 流水线中,镜像构建是连接"构建"和"部署"的关键环节。

使用 Jib 构建镜像(推荐):

bash
# 构建并推送到 Docker Registry
./gradlew jib \
    --no-daemon \
    --no-configuration-cache \
    -DdockerUsername=${REGISTRY_USERNAME} \
    -DdockerPassword=${REGISTRY_PASSWORD}

# 构建到本地 Docker(用于本地测试)
./gradlew jibDockerBuild --no-daemon

# 构建为 tar 文件(用于离线传输)
./gradlew jibBuildTar --no-daemon

使用 Docker Plugin 构建镜像(备选):

bash
# 使用 Dockerfile 构建
./gradlew casBuildDockerImage

# 构建并推送
./gradlew casPushDockerImage \
    -DdockerUsername=${REGISTRY_USERNAME} \
    -DdockerPassword=${REGISTRY_PASSWORD}

镜像标签策略:

推荐使用语义化版本标签 + Git Commit Hash 标签的组合:

bash
# 标签格式:{version}-{git_hash}-{build_number}
IMAGE_TAG="7.3.4-abc1234-42"

# 同时打 latest 标签(仅用于开发环境)
./gradlew jib -DdockerImageTagPostfix=-dev

6.5 镜像推送:Docker Registry/ECR/GCR

Docker Registry 推送:

bash
# 推送到私有 Docker Registry
./gradlew jib \
    -DdockerUsername=admin \
    -DdockerPassword=admin123

对应的 Jib 配置:

groovy
jib {
    to {
        image = "registry.example.com/cas/cas-overlay:7.3.4"
        auth {
            username = "${dockerUsername}"
            password = "${dockerPassword}"
        }
    }
}

Amazon ECR 推送:

groovy
jib {
    to {
        image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/cas:7.3.4"
        credHelper = "ecr-login"
    }
}

ECR 的认证通过 amazon-ecr-credential-helper 自动处理,无需手动配置用户名密码。

Google GCR 推送:

groovy
jib {
    to {
        image = "gcr.io/my-project/cas:7.3.4"
        credHelper = "gcr"
    }
}

GCR 的认证通过 docker-credential-gcr 自动处理。

6.6 部署阶段:K8s/Docker Compose

Kubernetes 部署:

yaml
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cas-server
  namespace: cas
  labels:
    app: cas-server
    version: "7.3.4"
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: cas-server
  template:
    metadata:
      labels:
        app: cas-server
        version: "7.3.4"
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - cas-server
              topologyKey: kubernetes.io/hostname
      containers:
      - name: cas
        image: registry.example.com/cas/cas-overlay:7.3.4
        ports:
        - containerPort: 8443
          name: https
        - containerPort: 8080
          name: http
        env:
        - name: JAVA_OPTS
          value: "-Xms1g -Xmx2g -XX:+UseG1GC"
        - name: SPRING_PROFILES_ACTIVE
          value: "production"
        - name: CAS_SERVER_SSL_KEYSTORE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: cas-secrets
              key: keystore-password
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: 8080
          initialDelaySeconds: 120
          periodSeconds: 30
          timeoutSeconds: 5
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: 8080
          initialDelaySeconds: 60
          periodSeconds: 10
          timeoutSeconds: 5
        resources:
          requests:
            memory: "1Gi"
            cpu: "500m"
          limits:
            memory: "2Gi"
            cpu: "2"
        volumeMounts:
        - name: config-volume
          mountPath: /etc/cas/config
        - name: services-volume
          mountPath: /etc/cas/services
        - name: keystore-volume
          mountPath: /etc/cas/thekeystore
          subPath: thekeystore
          readOnly: true
      volumes:
      - name: config-volume
        configMap:
          name: cas-config
      - name: services-volume
        configMap:
          name: cas-services
      - name: keystore-volume
        secret:
          secretName: cas-keystore
---
apiVersion: v1
kind: Service
metadata:
  name: cas-server
  namespace: cas
spec:
  type: ClusterIP
  ports:
  - port: 443
    targetPort: 8443
    protocol: TCP
    name: https
  selector:
    app: cas-server

Docker Compose 部署(开发/测试环境):

yaml
# docker-compose.yml
version: '3.8'
services:
  cas:
    image: apereo/cas:7.3.4
    ports:
      - "8443:8443"
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=dev
      - JVM_MEM_OPTS=-Xms512m -Xmx1024M
    volumes:
      - ./config:/etc/cas/config:ro
      - ./services:/etc/cas/services:ro
    depends_on:
      - mysql
      - redis
    networks:
      - cas-network

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: cas_db
      MYSQL_USER: cas
      MYSQL_PASSWORD: cas123
    volumes:
      - mysql-data:/var/lib/mysql
    networks:
      - cas-network

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass redis123
    volumes:
      - redis-data:/data
    networks:
      - cas-network

volumes:
  mysql-data:
  redis-data:

networks:
  cas-network:
    driver: bridge

6.7 环境变量管理

CAS 的配置可以通过环境变量进行外部化管理。Spring Boot 的松散绑定(Relaxed Binding)机制允许将 application.yml 中的配置项映射为环境变量。

配置映射规则:

Spring Boot 的环境变量映射规则如下:

application.yml环境变量
cas.server.nameCAS_SERVER_NAME
cas.server.ssl.enabledCAS_SERVER_SSL_ENABLED
spring.datasource.urlSPRING_DATASOURCE_URL
cas.ticket.registry.redis.hostCAS_TICKET_REGISTRY_REDIS_HOST

环境变量管理的最佳实践:

  1. 使用 Kubernetes ConfigMap 管理非敏感配置:
yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: cas-config
  namespace: cas
data:
  CAS_SERVER_NAME: "sso.example.com"
  CAS_SERVER_PREFIX: "https://sso.example.com"
  SPRING_PROFILES_ACTIVE: "production"
  CAS_TICKET_REGISTRY_REDIS_HOST: "redis-master.cas.svc"
  CAS_TICKET_REGISTRY_REDIS_PORT: "6379"
  1. 使用 Kubernetes Secret 管理敏感配置:
yaml
apiVersion: v1
kind: Secret
metadata:
  name: cas-secrets
  namespace: cas
type: Opaque
stringData:
  CAS_SERVER_SSL_KEYSTORE_PASSWORD: "your-keystore-password"
  SPRING_DATASOURCE_PASSWORD: "your-db-password"
  CAS_TICKET_REGISTRY_REDIS_PASSWORD: "your-redis-password"
  1. 使用外部配置中心(适用于大规模部署):
yaml
# bootstrap.yml
spring:
  cloud:
    config:
      uri: https://config-server.example.com
      name: cas-server
      profile: production
      label: main

第七章 生产环境部署最佳实践

7.1 密钥库管理

在生产环境中,SSL/TLS 密钥库的管理必须遵循最小权限原则和密钥轮换策略。

密钥轮换策略:

bash
# 1. 生成新的密钥对
keytool -genkeypair \
    -alias cas-new \
    -keyalg RSA \
    -keysize 4096 \
    -keypass ${NEW_KEY_PASSWORD} \
    -storepass ${STORE_PASSWORD} \
    -keystore keystore-new.jks \
    -dname "CN=sso.example.com,OU=IT,O=Example Inc,C=CN" \
    -ext "SAN=dns:sso.example.com,dns:*.sso.example.com" \
    -storetype PKCS12 \
    -validity 365

# 2. 导出新证书
keytool -exportcert \
    -alias cas-new \
    -storepass ${STORE_PASSWORD} \
    -keystore keystore-new.jks \
    -file cas-new.crt

# 3. 更新 Kubernetes Secret
kubectl create secret generic cas-keystore-new \
    --from-file=thekeystore=keystore-new.jks \
    -n cas

# 4. 滚动更新 CAS Pod
kubectl set image deployment/cas-server cas=registry.example.com/cas:7.3.4-newkey -n cas

自动化密钥轮换(使用 cert-manager):

yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: cas-server-cert
  namespace: cas
spec:
  secretName: cas-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  commonName: sso.example.com
  dnsNames:
  - sso.example.com
  - "*.sso.example.com"
  renewBefore: 720h  # 证书到期前 30 天自动续期
  keystores:
    jks:
      create: true
      passwordSecretRef:
        name: cas-keystore-password
        key: password

7.2 配置外部化

CAS 的配置外部化遵循以下层次结构:

优先级从高到低:
1. 命令行参数(java -jar cas.jar --cas.server.name=xxx)
2. 环境变量(CAS_SERVER_NAME=xxx)
3. ConfigMap/Secret 挂载的配置文件
4. 打包在 JAR 中的 application.yml
5. CAS 默认配置

推荐的配置外部化方案:

yaml
# Kubernetes Deployment 中的配置挂载
spec:
  containers:
  - name: cas
    envFrom:
    - configMapRef:
        name: cas-config          # 非敏感配置
    - secretRef:
        name: cas-secrets         # 敏感配置
    volumeMounts:
    - name: config-volume
      mountPath: /etc/cas/config  # 配置文件目录
    - name: services-volume
      mountPath: /etc/cas/services  # 服务注册目录
  volumes:
  - name: config-volume
    configMap:
      name: cas-config-files
  - name: services-volume
    configMap:
      name: cas-services

Spring Cloud Config 集成(适用于多环境管理):

yaml
# bootstrap.yml
spring:
  cloud:
    config:
      uri: https://config-server.example.com
      name: cas-server
      profile: ${SPRING_PROFILES_ACTIVE:production}
      label: main
      fail-fast: true
      retry:
        max-attempts: 10
        multiplier: 1.5

7.3 健康检查配置

CAS 基于 Spring Boot Actuator 提供了丰富的健康检查端点。在容器化环境中,正确配置健康检查对于自动扩缩容和自愈至关重要。

Spring Boot Actuator 配置:

yaml
# application.yml
management:
  endpoint:
    health:
      enabled: true
      show-details: when-authorized
      probes:
        enabled: true
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  health:
    livenessState:
      enabled: true
    readinessState:
      enabled: true
    db:
      enabled: true
    redis:
      enabled: true
    diskspace:
      enabled: true
      threshold: 1GB

Kubernetes 健康检查配置:

yaml
spec:
  containers:
  - name: cas
    livenessProbe:
      httpGet:
        path: /actuator/health/liveness
        port: 8080
      initialDelaySeconds: 120  # CAS 启动较慢,需要足够的初始延迟
      periodSeconds: 30
      timeoutSeconds: 5
      failureThreshold: 3
    readinessProbe:
      httpGet:
        path: /actuator/health/readiness
        port: 8080
      initialDelaySeconds: 60
      periodSeconds: 10
      timeoutSeconds: 5
      failureThreshold: 3
    startupProbe:
      httpGet:
        path: /actuator/health
        port: 8080
      initialDelaySeconds: 30
      periodSeconds: 10
      failureThreshold: 30  # 最多等待 5 分钟(30 * 10s)

为什么需要 startupProbe? CAS 作为一个大型的 Spring Boot 应用,启动时间通常在 1-3 分钟之间(取决于硬件配置和依赖数量)。如果没有 startupProbe,livenessProbe 可能在 CAS 启动过程中就判定应用不健康并触发重启,导致 CAS 永远无法启动(启动-失败-重启的死循环)。startupProbe 在应用启动期间替代 livenessProbe,给应用足够的启动时间。

7.4 日志收集(ELK/Loki)

CAS 日志配置(Log4j2):

xml
<!-- log4j2.xml -->
<Configuration status="WARN">
    <Appenders>
        <!-- 控制台输出(容器标准输出) -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout
                pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>

        <!-- JSON 格式输出(便于 ELK 解析) -->
        <Console name="JsonConsole" target="SYSTEM_OUT">
            <JsonLayout compact="true" eventEol="true">
                <KeyValuePair key="app" value="cas-server"/>
                <KeyValuePair key="version" value="${env:CAS_VERSION:-unknown}"/>
            </JsonLayout>
        </Console>
    </Appenders>

    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="JsonConsole"/>
        </Root>
        <Logger name="org.apereo.cas" level="INFO"/>
        <Logger name="org.springframework" level="WARN"/>
    </Loggers>
</Configuration>

Docker 日志驱动配置:

json
// /etc/docker/daemon.json
{
  "log-driver": "fluentd",
  "log-opts": {
    "fluentd-address": "localhost:24224",
    "tag": "cas.{{.ID}}"
  }
}

Kubernetes 日志收集(Filebeat + Elasticsearch):

yaml
# Filebeat DaemonSet 配置
apiVersion: v1
kind: ConfigMap
metadata:
  name: filebeat-config
data:
  filebeat.yml: |-
    filebeat.inputs:
    - type: container
      paths:
        - /var/log/containers/cas-*.log
      processors:
        - decode_json_fields:
            fields: ["message"]
            target: "cas"
            overwrite_keys: true
    output.elasticsearch:
      hosts: ["elasticsearch.logging.svc:9200"]
      index: "cas-logs-%{+yyyy.MM.dd}"

Grafana Loki 日志收集:

yaml
# Promtail 配置
scrape_configs:
  - job_name: cas
    kubernetes_sd_configs:
    - role: pod
    namespaces:
      names:
      - cas
    pipeline_stages:
    - json:
        expressions:
          level: level
          logger: logger
          message: msg
          timestamp: "@timestamp"
    - labels:
        level:
        logger:
    - timestamp:
        source: timestamp
        format: RFC3339

7.5 监控告警(Prometheus + Grafana)

CAS Prometheus 端点配置:

yaml
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus,metrics
  metrics:
    export:
      prometheus:
        enabled: true
    tags:
      application: cas-server
    distribution:
      percentiles-histogram:
        http.server.requests: true

Prometheus ServiceMonitor 配置:

yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: cas-server
  namespace: cas
  labels:
    release: prometheus
spec:
  selector:
    matchLabels:
      app: cas-server
  endpoints:
  - port: http
    path: /actuator/prometheus
    interval: 15s
    scrapeTimeout: 10s

关键监控指标:

指标PromQL告警阈值
JVM 堆内存使用率jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"}> 85%
HTTP 请求延迟 P99histogram_quantile(0.99, http_server_requests_seconds_bucket)> 5s
HTTP 错误率rate(http_server_requests_seconds_count{status=~"5.."}[5m]) / rate(http_server_requests_seconds_count[5m])> 1%
Ticket 创建速率rate(cas_tickets_created_total[5m])异常波动
认证成功率rate(cas_authentications_total{result="success"}[5m]) / rate(cas_authentications_total[5m])< 95%
GC 暂停时间rate(jvm_gc_pause_seconds_sum[5m])> 1s/min

Grafana Dashboard 推荐:

  1. JVM (Micrometer) Dashboard: 监控 JVM 的内存、GC、线程等指标。
  2. Spring Boot Statistics Dashboard: 监控 Spring Boot 的 HTTP 请求、数据源连接池等指标。
  3. CAS Custom Dashboard: 自定义 CAS 特有的指标(Ticket 统计、认证统计等)。

7.6 滚动更新与回滚策略

Kubernetes 滚动更新配置:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cas-server
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1          # 滚动更新时最多多创建 1 个 Pod
      maxUnavailable: 0    # 滚动更新时不允许有 Pod 不可用
  revisionHistoryLimit: 10  # 保留最近 10 个版本用于回滚

滚动更新流程:

初始状态:[Pod-1(v1)] [Pod-2(v1)] [Pod-3(v1)]

步骤1:创建新 Pod    [Pod-1(v1)] [Pod-2(v1)] [Pod-3(v1)] [Pod-4(v2)]
步骤2:等待就绪      [Pod-1(v1)] [Pod-2(v1)] [Pod-3(v1)] [Pod-4(v2-Ready)]
步骤3:删除旧 Pod    [Pod-1(v1)] [Pod-2(v1)] [---]         [Pod-4(v2-Ready)]
步骤4:创建新 Pod    [Pod-1(v1)] [Pod-2(v1)] [Pod-5(v2)]   [Pod-4(v2-Ready)]
步骤5:等待就绪      [Pod-1(v1)] [Pod-2(v1)] [Pod-5(v2-R)]  [Pod-4(v2-Ready)]
步骤6:继续...      [Pod-1(v1)] [---]        [Pod-5(v2-R)]  [Pod-4(v2-Ready)]
...
最终状态:            [---]        [---]        [Pod-5(v2-R)]  [Pod-4(v2-Ready)]
                     [Pod-6(v2-R)] [Pod-7(v2-R)] [Pod-5(v2-R)]  [Pod-4(v2-Ready)]

回滚操作:

bash
# 查看部署历史
kubectl rollout history deployment/cas-server -n cas

# 回滚到上一个版本
kubectl rollout undo deployment/cas-server -n cas

# 回滚到指定版本
kubectl rollout undo deployment/cas-server --to-revision=3 -n cas

# 查看回滚状态
kubectl rollout status deployment/cas-server -n cas

CAS 特有的滚动更新注意事项:

  1. Ticket 兼容性: CAS 的大版本升级(如 6.6 → 7.3)可能引入 Ticket 格式的变化。在滚动更新期间,新旧版本的 Pod 可能同时存在,需要确保 Ticket Registry(Redis)中的数据格式兼容。
  2. Session 兼容性: 如果使用 Spring Session + Redis,需要确保新旧版本的 Session 序列化格式兼容。
  3. 预热策略: CAS 的启动时间较长(1-3 分钟),建议在滚动更新前对新版本进行预热——先启动一个新版本的 Pod 并验证其健康状态,然后再执行滚动更新。

第八章 Docker Compose 开发环境

8.1 完整编排架构设计

在开发和测试环境中,Docker Compose 提供了一种轻量级的方式来编排 CAS 及其依赖服务。以下是一个完整的 Docker Compose 编排方案,包含 CAS、MySQL、Redis 和 Zookeeper。

架构图:

                    +-----------------+
                    |   Nginx/Traefik |
                    |   (反向代理)     |
                    +--------+--------+
                             |
              +--------------+--------------+
              |              |              |
     +--------+--------+   ...   +--------+--------+
     |  CAS Server 1  |        |  CAS Server 2  |
     |  (port 8443)   |        |  (port 8444)   |
     +---+-----+------+        +---+-----+------+
         |     |                     |     |
    +----+     +----+           +----+     +----+
    |              |           |              |
+---+---+    +----+----+  +---+---+    +----+----+
| MySQL |    |  Redis  |  |  Zookeeper  |  Redis  |
| 8.0   |    |  7.x    |  |  3.8.x     |  7.x    |
+-------+    +---------+  +------------+---------+

8.2 CAS + MySQL + Redis + Zookeeper 编排

以下是一个完整的 docker-compose.yml 文件,适用于 CAS 的开发和测试环境:

yaml
version: '3.8'

x-common-env: &common-env
  SPRING_PROFILES_ACTIVE: dev
  CAS_SERVER_NAME: localhost:8443
  CAS_SERVER_PREFIX: https://localhost:8443/cas
  SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/cas_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
  SPRING_DATASOURCE_USERNAME: cas
  SPRING_DATASOURCE_PASSWORD: cas123
  SPRING_DATASOURCE_DRIVER_CLASS_NAME: com.mysql.cj.jdbc.Driver
  CAS_TICKET_REGISTRY_CORE_ENABLE: "true"
  CAS_TICKET_REGISTRY_REDIS_HOST: redis
  CAS_TICKET_REGISTRY_REDIS_PORT: 6379
  CAS_TICKET_REGISTRY_REDIS_PASSWORD: redis123

services:
  # ==================== CAS Server ====================
  cas:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: cas-server
    ports:
      - "8080:8080"    # HTTP
      - "8443:8443"    # HTTPS
      - "5000:5000"    # Debug
    environment:
      <<: *common-env
      JVM_MEM_OPTS: "-Xms512m -Xmx1024M"
      ENTRYPOINT_DEBUG: "true"
    volumes:
      - ./config:/etc/cas/config:ro
      - ./services:/etc/cas/services:ro
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - cas-network
    restart: unless-stopped

  # ==================== MySQL ====================
  mysql:
    image: mysql:8.0
    container_name: cas-mysql
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: cas_db
      MYSQL_USER: cas
      MYSQL_PASSWORD: cas123
      TZ: Asia/Shanghai
    volumes:
      - mysql-data:/var/lib/mysql
      - ./init-db:/docker-entrypoint-initdb.d:ro
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
      - --default-authentication-plugin=caching_sha2_password
      - --max-connections=200
      - --innodb-buffer-pool-size=256M
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot123"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s
    networks:
      - cas-network
    restart: unless-stopped

  # ==================== Redis ====================
  redis:
    image: redis:7-alpine
    container_name: cas-redis
    ports:
      - "6379:6379"
    command:
      - redis-server
      - --requirepass
      - redis123
      - --maxmemory
      - 256mb
      - --maxmemory-policy
      - allkeys-lru
      - --appendonly
      - "yes"
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "redis123", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - cas-network
    restart: unless-stopped

  # ==================== Zookeeper ====================
  zookeeper:
    image: zookeeper:3.8
    container_name: cas-zookeeper
    ports:
      - "2181:2181"
      - "8081:8080"   # Admin Server
    environment:
      ZOO_MY_ID: 1
      ZOO_SERVERS: server.1=0.0.0.0:2888:3888;2181
      TZ: Asia/Shanghai
    volumes:
      - zk-data:/data
      - zk-datalog:/datalog
    healthcheck:
      test: ["CMD", "echo", "ruok", "|", "nc", "localhost", "2181"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - cas-network
    restart: unless-stopped

  # ==================== Redis Commander (管理工具) ====================
  redis-commander:
    image: rediscommander/redis-commander:latest
    container_name: cas-redis-commander
    ports:
      - "8082:8081"
    environment:
      REDIS_HOSTS: local:redis:6379:0:redis123
    depends_on:
      - redis
    networks:
      - cas-network

  # ==================== phpMyAdmin (数据库管理工具) ====================
  phpmyadmin:
    image: phpmyadmin:5.2
    container_name: cas-phpmyadmin
    ports:
      - "8083:80"
    environment:
      PMA_HOST: mysql
      PMA_PORT: 3306
      PMA_USER: root
      PMA_PASSWORD: root123
    depends_on:
      - mysql
    networks:
      - cas-network

volumes:
  mysql-data:
    driver: local
  redis-data:
    driver: local
  zk-data:
    driver: local
  zk-datalog:
    driver: local

networks:
  cas-network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16

8.3 网络配置

Docker 网络模式选择:

Docker Compose 默认创建一个 bridge 网络用于服务间通信。在我们的编排中,使用了自定义子网 172.28.0.0/16

yaml
networks:
  cas-network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16

为什么需要自定义子网?

  1. 避免网络冲突: 默认的 Docker 网络子网(172.17.0.0/16)可能与宿主机的其他网络冲突。自定义子网可以避免这个问题。
  2. 网络策略: 在 Kubernetes 环境中,可以使用 NetworkPolicy 精确控制 Pod 间的网络访问。在 Docker Compose 中,虽然没有原生的 NetworkPolicy 支持,但可以通过自定义网络来隔离不同的服务组。

服务间通信:

在 Docker Compose 网络中,服务之间可以通过服务名进行通信:

CAS → MySQL:    jdbc:mysql://mysql:3306/cas_db
CAS → Redis:    redis://redis:6379
CAS → Zookeeper: zookeeper:2181

外部访问:

通过 ports 映射将容器端口暴露到宿主机:

服务容器端口宿主机端口用途
CAS80808080HTTP 访问
CAS84438443HTTPS 访问
CAS50005000远程调试
MySQL33063306数据库连接
Redis63796379Redis 连接
Zookeeper21812181ZK 连接
Redis Commander80818082Redis 管理界面
phpMyAdmin808083数据库管理界面

8.4 数据持久化

Docker Compose 使用命名卷(Named Volumes)来实现数据持久化:

yaml
volumes:
  mysql-data:
    driver: local
  redis-data:
    driver: local
  zk-data:
    driver: local
  zk-datalog:
    driver: local

各服务的持久化策略:

服务持久化内容卷名称备份策略
MySQL数据文件、Binlogmysql-data每日全量 + Binlog 增量
RedisRDB/AOF 文件redis-dataAOF 持续追加
Zookeeper事务日志、快照zk-data, zk-datalog快照定期备份

MySQL 初始化脚本:

bash
# init-db/init-cas.sql
-- CAS 数据库初始化脚本
CREATE DATABASE IF NOT EXISTS cas_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE cas_db;

-- 用户表
CREATE TABLE IF NOT EXISTS cas_user (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(100) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    email VARCHAR(255),
    phone VARCHAR(20),
    enabled TINYINT(1) DEFAULT 1,
    expired TINYINT(1) DEFAULT 0,
    locked TINYINT(1) DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 插入默认管理员用户
-- 密码为 BCrypt 加密的 "admin123"
INSERT IGNORE INTO cas_user (username, password, email, enabled)
VALUES ('admin', '$2a$10$...', 'admin@example.com', 1);

数据备份与恢复:

bash
# MySQL 备份
docker exec cas-mysql mysqldump -u root -proot123 cas_db > backup/cas_db_$(date +%Y%m%d).sql

# MySQL 恢复
docker exec -i cas-mysql mysql -u root -proot123 cas_db < backup/cas_db_20260409.sql

# Redis 备份
docker exec cas-redis redis-cli -a redis123 BGSAVE
docker cp cas-redis:/data/dump.rdb backup/redis_dump_$(date +%Y%m%d).rdb

# Redis 恢复
docker cp backup/redis_dump_20260409.rdb cas-redis:/data/dump.rdb
docker restart cas-redis

8.5 开发环境 vs 生产环境配置差异

配置维度开发环境生产环境
JVM 内存-Xms512m -Xmx1024M-Xms2g -Xmx4g
副本数13+
日志级别DEBUGWARN
健康检查可选必须
SSL/TLS自签名证书CA 签发证书
密钥库管理打包进镜像Secret/Volume 挂载
数据库容器内 MySQLRDS/云数据库
Redis单节点容器Redis Cluster/Sentinel
网络Bridge 网络Kubernetes CNI
监控可选Prometheus + Grafana
日志控制台输出ELK/Loki 集中收集
备份手动/可选自动化定时备份
安全宽松配置最小权限原则
服务注册允许所有 HTTPS精确匹配 Service ID
调试端口暴露 5000不暴露
时区TZ=Asia/ShanghaiTZ=Asia/Shanghai
资源限制不设置requests + limits

使用 Docker Compose Profiles 管理环境差异:

yaml
# docker-compose.yml
version: '3.8'

services:
  cas:
    build: .
    profiles: ["dev", "staging", "prod"]
    environment:
      SPRING_PROFILES_ACTIVE: ${SPRING_PROFILE:-dev}

  mysql:
    image: mysql:8.0
    profiles: ["dev", "staging"]

  redis:
    image: redis:7-alpine
    profiles: ["dev", "staging"]

  prometheus:
    image: prom/prometheus
    profiles: ["staging", "prod"]

  grafana:
    image: grafana/grafana
    profiles: ["staging", "prod"]
bash
# 启动开发环境(CAS + MySQL + Redis)
docker compose --profile dev up -d

# 启动预发布环境(CAS + MySQL + Redis + Prometheus + Grafana)
docker compose --profile staging up -d

# 启动生产环境(仅 CAS,使用外部数据库和 Redis)
docker compose --profile prod up -d

总结与展望

本文基于我们团队在 CAS Overlay 项目中的实际工程实践,系统地解析了 CAS 从 5.3 到 7.3 三个大版本的容器化方案演进。让我们回顾一下关键的技术变迁:

第一阶段(CAS 5.3):手动 Dockerfile 的基础容器化。 采用两阶段构建(Gradle/Maven 构建 + 最小化 JRE 运行),实现了镜像瘦身和构建分离。这一阶段的容器化方案虽然简单直接,但为后续的演进奠定了基础——两阶段构建的理念、entrypoint.sh 的设计、端口暴露策略等核心设计在后续版本中得到了延续。

第二阶段(CAS 6.6):容器化方案的完善与稳定。 在 CAS 5.3 的基础上,新增了时区设置(TZ=Asia/Shanghai),修复了因时区不一致导致的 Ticket 过期时间异常问题。同时,Jib Plugin 开始作为可选方案引入(版本 3.4.0),但手动 Dockerfile 仍然是主要的容器化方式。

第三阶段(CAS 7.3):云原生容器化的全面拥抱。 Jib Gradle Plugin 升级到 3.5.3,成为推荐的镜像构建方式。CycloneDX Plugin 3.2.0 集成到 bootJar 流程,实现了 SBOM 的自动生成。Foojay Plugin 1.0.0 提供了 JDK 自动下载能力。Gradle Docker Plugin 9.4.0 保留了传统的 Docker 构建路径。配置缓存兼容性处理体现了对 Gradle 新特性的积极跟进。

技术选型的核心原则:

  1. 渐进式演进: 不要一次性替换所有工具,而是在保持稳定性的前提下逐步引入新技术。
  2. 安全优先: 密钥库不打包进镜像、SBOM 自动生成、最小权限原则——安全应该贯穿容器化的每个环节。
  3. 可观测性: 健康检查、日志收集、监控告警——容器化不仅仅是打包方式的变化,更需要配套的运维体系。
  4. 环境一致性: 从开发环境到生产环境,使用相同的容器镜像,通过环境变量区分配置,消除"在我机器上能运行"的问题。

未来展望:

随着云原生技术的持续演进,CAS 的容器化方案还有以下值得关注的趋势:

  1. GraalVM Native Image: CAS 已经开始探索 GraalVM Native Image 支持,这将大幅减少内存占用和启动时间(从分钟级降到毫秒级),非常适合 Serverless 场景。
  2. WebAssembly(Wasm): 随着 Wasm 在服务端的应用逐渐成熟,Java 应用(包括 CAS)可能通过 Wasm 实现更轻量级的部署。
  3. eBPF 可观测性: 基于 eBPF 的网络和安全可观测性工具(如 Cilium Hubble)可以为 CAS 容器提供更细粒度的流量监控和安全策略。
  4. AI 辅助运维: 基于机器学习的异常检测和自动修复能力可以进一步提升 CAS 容器化部署的可靠性。

容器化不是终点,而是起点。从手动 Dockerfile 到 Jib 云原生镜像构建的演进,折射出 Java 生态对云原生理念的深度拥抱。希望本文能够为正在或即将进行 CAS 容器化实践的团队提供有价值的参考。


版权声明: 本文为必码(bima.cc)原创技术文章,仅供学习交流。

本文内容基于实际项目源码解析整理,代码示例均为教学简化版本,仅供学习参考。

文档内容提取自项目源码与配置文件,如需获取完整项目代码,请访问 bima.cc