Rocky Linux 10 搭建 CAS + OpenLDAP 单点登录系统:原生部署、Docker 部署与生产调优

随着企业内部系统不断增加,每个系统分别保存用户名和密码,会逐渐暴露出以下问题:

  • 用户需要记住多套账号和密码;
  • 入职、转岗、离职需要在多个系统重复维护;
  • 密码策略、账户锁定和权限回收无法统一;
  • 每个业务系统都要重复开发登录和用户查询逻辑;
  • 登录审计、异常检测和多因素认证难以集中建设;
  • 用户访问不同系统时需要反复登录。

CAS(Central Authentication Service)负责登录流程、单点登录会话和票据签发;OpenLDAP 用于集中保存用户、组织和用户组等目录数据。二者结合后的职责如下:

1
2
3
OpenLDAP:保存用户、密码、组织和用户组
CAS:调用 LDAP 验证身份,维护 SSO 会话并签发票据
业务系统:信任 CAS,只消费认证结果,不直接处理用户密码

本文基于 Rocky Linux 10,使用 Apereo CAS 7.3.8、JDK 21 和 OpenLDAP 2.6.x 搭建一套企业级单点登录系统,并分别给出原生部署和 Docker Compose 部署方案。

本文的版本基线为 2026 年 7 月 31 日。CAS 7.3.x 要求构建和运行阶段使用 JDK 21;Rocky Linux 10 的 OpenLDAP 服务端由 EPEL 提供。正式部署前应再次查看 Release Notes,并在测试环境验证。

一、CAS + LDAP 的认证流程

CAS 并不是用户数据库。用户提交用户名和密码后,CAS 会调用 LDAP 认证处理器:

  1. CAS 使用只读服务账号连接 OpenLDAP;
  2. 根据用户名搜索用户 DN;
  3. 使用用户 DN 和用户提交的密码执行 LDAP Bind;
  4. Bind 成功后,CAS 创建登录会话并签发 TGT;
  5. 用户访问业务系统时,CAS 为该业务系统签发一次性 Service Ticket;
  6. 业务系统通过后端请求向 CAS 校验 Service Ticket;
  7. 校验通过后,业务系统建立自己的本地会话。
sequenceDiagram
    participant U as 用户浏览器
    participant A as 业务系统
    participant C as CAS Server
    participant L as OpenLDAP

    U->>A: 访问受保护页面
    A-->>U: 重定向到 CAS 登录页
    U->>C: 提交用户名和密码
    C->>L: CAS Reader 搜索用户 DN
    L-->>C: 返回 uid=alice,ou=people,...
    C->>L: 使用用户 DN 和密码执行 Bind
    L-->>C: Bind 成功
    C-->>U: 创建 TGT Cookie,并携带 ST 返回业务系统
    U->>A: GET /login/cas?ticket=ST-...
    A->>C: 后端调用 serviceValidate
    C-->>A: 返回用户身份和允许释放的属性
    A-->>U: 建立业务会话

CAS 中最常见的两类票据:

票据 说明
TGT Ticket Granting Ticket,代表用户已经在 CAS 登录,是 SSO 会话基础
ST Service Ticket,签发给某个具体业务系统,通常校验一次后失效

业务系统不应直接读取 LDAP 密码,也不应保存 LDAP 管理员账号。正确链路是:

1
2
用户密码 -> CAS -> LDAP Bind 验证
业务系统 <- CAS 认证结果和用户属性

二、部署架构

本文原生部署使用两台服务器:

节点 主机名 IP 作用
CAS 节点 sso.example.com 192.168.9.20 CAS、Nginx
LDAP 节点 ldap.example.com 192.168.9.21 OpenLDAP

实验环境可以部署在一台机器上,但生产环境建议将 CAS 与 LDAP 分离。

flowchart LR
    Browser[用户浏览器] -->|HTTPS 443| Nginx[Nginx]
    Nginx -->|HTTP 127.0.0.1:8080| CAS[Apereo CAS 7.3.8]
    CAS -->|LDAPS 636| LDAP[OpenLDAP 2.6.x]
    CAS --> Registry[Service Registry]
    App[业务系统] -->|后端校验 ST| Nginx

域名规划:

1
2
3
sso.example.com
ldap.example.com
app.example.com

测试环境可临时配置:

1
2
3
4
5
cat >> /etc/hosts <<'EOF'
192.168.9.20 sso.example.com
192.168.9.21 ldap.example.com
192.168.9.30 app.example.com
EOF

生产环境应使用企业 DNS。

三、资源规划

3.1 CAS 节点

规模 CPU 内存 JVM Heap 起点
测试 2 核 4 GB 1 GB
小型生产 4 核 8 GB 2 GB
中型生产 8 核 16 GB 4 GB

CAS 实际占用包括:

1
2
3
4
5
6
7
Java Heap
+ Metaspace
+ Thread Stack
+ Direct/Native Memory
+ TLS
+ 日志
+ Linux Page Cache

不要把宿主机全部内存都交给 -Xmx

3.2 LDAP 节点

规模 CPU 内存 数据盘
测试 2 核 2 GB 20 GB SSD
小型生产 4 核 4~8 GB 50 GB SSD
中型生产 8 核 8~16 GB 100 GB 以上 SSD

OpenLDAP MDB 依赖内存映射和 Page Cache。重点关注:

  • 搜索条件是否命中索引;
  • MDB Map Size 是否足够;
  • 磁盘延迟;
  • LDAP 连接池;
  • 热数据能否进入 Page Cache。

四、系统初始化

在 CAS 和 LDAP 节点分别执行:

1
2
3
4
cat /etc/rocky-release
uname -r
uname -m
hostnamectl

设置时区和时间同步:

1
2
3
4
5
6
timedatectl set-timezone Asia/Shanghai
systemctl enable --now chronyd

timedatectl
chronyc tracking
chronyc sources -v

CAS 票据和 TLS 证书都依赖准确时间。

更新系统:

1
2
3
dnf clean all
dnf makecache
dnf upgrade -y

安装工具:

1
2
3
dnf install -y \
vim curl wget tar unzip jq git openssl \
bind-utils lsof policycoreutils-python-utils firewalld

保持 SELinux:

1
2
getenforce
sestatus

生产环境不建议直接关闭 SELinux。

五、安装 OpenLDAP

以下步骤在 ldap.example.com 执行。

5.1 启用 CRB 和 EPEL

1
2
3
4
5
dnf config-manager --set-enabled crb
dnf install -y epel-release

dnf clean all
dnf makecache

查看版本:

1
2
3
dnf info openldap
dnf info openldap-servers
dnf info openldap-clients

安装:

1
dnf install -y openldap-servers openldap-clients

确认:

1
2
3
slapd -VV
ldapsearch -VV
rpm -q openldap openldap-servers openldap-clients

EPEL 的 openldap-servers 对 EL10 对应小版本的 openldap 库有严格依赖。如果遇到 nothing provides openldap = ...,先确认 Rocky、BaseOS 和 EPEL 是否处于相同小版本,再执行 dnf distro-sync。不要用 --nodeps 强装。

1
dnf distro-sync -y

5.2 检查目录与服务

1
2
3
4
ls -lah /etc/openldap
ls -lah /etc/openldap/slapd.d
ls -lah /var/lib/ldap
systemctl cat slapd

主要目录:

目录 用途
/etc/openldap/slapd.d 动态配置数据库 cn=config
/etc/openldap/schema LDAP Schema
/etc/openldap/certs TLS 证书
/var/lib/ldap MDB 数据
/usr/share/openldap-servers/slapd.ldif 初始配置模板

启动:

1
2
systemctl enable --now slapd
systemctl status slapd --no-pager -l

检查:

1
2
3
4
5
6
7
8
9
10
ss -lntp | grep slapd
ss -lx | grep ldapi

ldapsearch \
-Q -LLL \
-Y EXTERNAL \
-H ldapi:/// \
-b cn=config \
'(objectClass=olcDatabaseConfig)' \
dn olcDatabase olcSuffix

如果 /etc/openldap/slapd.d 是空目录,才执行初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
systemctl stop slapd

rm -rf /etc/openldap/slapd.d/*
mkdir -p /etc/openldap/slapd.d

slapadd \
-n 0 \
-F /etc/openldap/slapd.d \
-l /usr/share/openldap-servers/slapd.ldif

chown -R ldap:ldap /etc/openldap/slapd.d /var/lib/ldap
restorecon -Rv /etc/openldap/slapd.d /var/lib/ldap

systemctl start slapd

不要在已有数据的服务器上清空 slapd.d

六、规划 LDAP 目录树

本文使用:

1
2
3
4
5
6
7
dc=example,dc=com
├── ou=people
│ └── uid=alice
├── ou=groups
│ └── cn=developers
└── ou=system
└── cn=cas-reader
对象 DN
根节点 dc=example,dc=com
目录管理员 cn=directory-admin,dc=example,dc=com
CAS 查询账号 cn=cas-reader,ou=system,dc=example,dc=com
用户目录 ou=people,dc=example,dc=com
用户组目录 ou=groups,dc=example,dc=com
示例用户 uid=alice,ou=people,dc=example,dc=com

RootDN 可以绕过普通 ACL,只用于初始化和紧急管理。CAS 必须使用最小权限的只读账号,不能使用 RootDN。

七、配置 OpenLDAP MDB 数据库

7.1 生成密码哈希

1
2
3
4
5
6
7
8
9
10
11
12
13
read -rsp "Directory Admin Password: " LDAP_ADMIN_PASSWORD
echo
LDAP_ADMIN_PASSWORD_HASH="$(slappasswd -s "${LDAP_ADMIN_PASSWORD}")"
unset LDAP_ADMIN_PASSWORD

read -rsp "CAS Reader Password: " CAS_READER_PASSWORD
echo
CAS_READER_PASSWORD_HASH="$(slappasswd -s "${CAS_READER_PASSWORD}")"

read -rsp "Alice Password: " ALICE_PASSWORD
echo
ALICE_PASSWORD_HASH="$(slappasswd -s "${ALICE_PASSWORD}")"
unset ALICE_PASSWORD

CAS Reader 的明文密码后续需要安全配置到 CAS。请保存到密码管理系统,不要提交到 Git。

7.2 动态查找 MDB 配置 DN

不要硬编码 olcDatabase={2}mdb,cn=config,编号可能因环境不同而变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
MDB_DN="$(
ldapsearch \
-Q -LLL \
-Y EXTERNAL \
-H ldapi:/// \
-b cn=config \
'(&(objectClass=olcDatabaseConfig)(olcDatabase=mdb))' \
dn \
| awk -F': ' '/^dn: / {print $2; exit}'
)"

printf 'MDB_DN=%s\n' "${MDB_DN}"
test -n "${MDB_DN}"

7.3 配置 Suffix、RootDN、索引和 ACL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
cat > /root/configure-mdb.ldif <<EOF
dn: ${MDB_DN}
changetype: modify
replace: olcSuffix
olcSuffix: dc=example,dc=com
-
replace: olcRootDN
olcRootDN: cn=directory-admin,dc=example,dc=com
-
replace: olcRootPW
olcRootPW: ${LDAP_ADMIN_PASSWORD_HASH}
-
replace: olcDbDirectory
olcDbDirectory: /var/lib/ldap
-
replace: olcDbMaxSize
olcDbMaxSize: 10737418240
-
replace: olcDbIndex
olcDbIndex: objectClass eq
olcDbIndex: entryUUID,entryCSN eq
olcDbIndex: uid eq
olcDbIndex: mail eq
olcDbIndex: cn,sn,displayName eq,sub
olcDbIndex: member eq
-
replace: olcAccess
olcAccess: {0}to attrs=userPassword by self write by anonymous auth by * none
olcAccess: {1}to dn.subtree="ou=people,dc=example,dc=com" by dn.exact="cn=cas-reader,ou=system,dc=example,dc=com" read by self read by users read by * none
olcAccess: {2}to dn.subtree="ou=groups,dc=example,dc=com" by dn.exact="cn=cas-reader,ou=system,dc=example,dc=com" read by users read by * none
olcAccess: {3}to dn.subtree="ou=system,dc=example,dc=com" by self read by * none
olcAccess: {4}to * by users read by * none
EOF

应用:

1
2
3
4
5
ldapmodify \
-Q \
-Y EXTERNAL \
-H ldapi:/// \
-f /root/configure-mdb.ldif

检查:

1
2
3
4
5
6
7
ldapsearch \
-Q -LLL \
-Y EXTERNAL \
-H ldapi:/// \
-b "${MDB_DN}" \
-s base \
olcSuffix olcRootDN olcDbDirectory olcDbMaxSize olcDbIndex olcAccess

olcDbMaxSize 是 MDB 可增长的上限,并不代表立即占用 10 GiB 内存或磁盘。应将其设置为未来数据规模的数倍,并持续监控文件系统。

八、创建目录树和用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
cat > /root/base-data.ldif <<EOF
dn: dc=example,dc=com
objectClass: top
objectClass: dcObject
objectClass: organization
o: Example Corporation
dc: example

dn: ou=people,dc=example,dc=com
objectClass: top
objectClass: organizationalUnit
ou: people
description: Application users

dn: ou=groups,dc=example,dc=com
objectClass: top
objectClass: organizationalUnit
ou: groups
description: Application groups

dn: ou=system,dc=example,dc=com
objectClass: top
objectClass: organizationalUnit
ou: system
description: Service accounts

dn: cn=cas-reader,ou=system,dc=example,dc=com
objectClass: top
objectClass: organizationalRole
objectClass: simpleSecurityObject
cn: cas-reader
description: Read-only account used by Apereo CAS
userPassword: ${CAS_READER_PASSWORD_HASH}

dn: uid=alice,ou=people,dc=example,dc=com
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
uid: alice
cn: Alice Zhang
sn: Zhang
givenName: Alice
displayName: Alice Zhang
mail: alice@example.com
userPassword: ${ALICE_PASSWORD_HASH}

dn: cn=developers,ou=groups,dc=example,dc=com
objectClass: top
objectClass: groupOfNames
cn: developers
description: Development team
member: uid=alice,ou=people,dc=example,dc=com
EOF

导入:

1
2
3
4
5
6
ldapadd \
-x \
-H ldap://127.0.0.1 \
-D 'cn=directory-admin,dc=example,dc=com' \
-W \
-f /root/base-data.ldif

测试 CAS Reader:

1
2
3
4
5
6
7
8
ldapsearch \
-x -LLL \
-H ldap://127.0.0.1 \
-D 'cn=cas-reader,ou=system,dc=example,dc=com' \
-W \
-b 'ou=people,dc=example,dc=com' \
'(uid=alice)' \
uid cn sn mail displayName

测试用户 Bind:

1
2
3
4
5
ldapwhoami \
-x \
-H ldap://127.0.0.1 \
-D 'uid=alice,ou=people,dc=example,dc=com' \
-W

正确输出:

1
dn:uid=alice,ou=people,dc=example,dc=com

清理临时文件和变量:

1
2
3
4
5
6
7
shred -u /root/configure-mdb.ldif /root/base-data.ldif

unset \
LDAP_ADMIN_PASSWORD_HASH \
CAS_READER_PASSWORD_HASH \
ALICE_PASSWORD_HASH \
MDB_DN

保留 CAS Reader 明文密码到安全的密码管理系统后:

1
unset CAS_READER_PASSWORD

九、为 OpenLDAP 配置 TLS

LDAP Simple Bind 如果运行在明文网络上,用户密码会以可恢复形式经过网络。生产环境必须使用企业 CA 签发的 LDAPS 或 StartTLS。

本文使用:

1
CAS -> LDAPS 636 -> OpenLDAP

9.1 实验环境创建内部 CA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mkdir -p /root/ldap-pki
cd /root/ldap-pki

openssl genrsa -out company-root-ca.key 4096
chmod 0600 company-root-ca.key

openssl req \
-x509 -new -sha256 -days 3650 \
-key company-root-ca.key \
-out company-root-ca.crt \
-subj '/C=CN/O=Example Corporation/OU=Infrastructure/CN=Example Internal Root CA'

openssl genrsa -out ldap.example.com.key 3072
chmod 0600 ldap.example.com.key

openssl req \
-new -sha256 \
-key ldap.example.com.key \
-out ldap.example.com.csr \
-subj '/C=CN/O=Example Corporation/OU=Infrastructure/CN=ldap.example.com'

扩展文件:

1
2
3
4
5
6
7
8
cat > ldap.example.com.ext <<'EOF'
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=DNS:ldap.example.com,IP:192.168.9.21
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
EOF

签发:

1
2
3
4
5
6
7
8
openssl x509 \
-req -sha256 -days 825 \
-in ldap.example.com.csr \
-CA company-root-ca.crt \
-CAkey company-root-ca.key \
-CAcreateserial \
-out ldap.example.com.crt \
-extfile ldap.example.com.ext

检查:

1
2
3
openssl x509 \
-in ldap.example.com.crt \
-noout -subject -issuer -dates -ext subjectAltName

9.2 安装证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mkdir -p /etc/openldap/certs

install -o ldap -g ldap -m 0644 \
company-root-ca.crt \
/etc/openldap/certs/company-root-ca.crt

install -o ldap -g ldap -m 0644 \
ldap.example.com.crt \
/etc/openldap/certs/ldap.example.com.crt

install -o ldap -g ldap -m 0600 \
ldap.example.com.key \
/etc/openldap/certs/ldap.example.com.key

restorecon -Rv /etc/openldap/certs

9.3 修改 cn=config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cat > /root/configure-tls.ldif <<'EOF'
dn: cn=config
changetype: modify
replace: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/openldap/certs/company-root-ca.crt
-
replace: olcTLSCertificateFile
olcTLSCertificateFile: /etc/openldap/certs/ldap.example.com.crt
-
replace: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/openldap/certs/ldap.example.com.key
EOF

ldapmodify \
-Q \
-Y EXTERNAL \
-H ldapi:/// \
-f /root/configure-tls.ldif

shred -u /root/configure-tls.ldif

编辑:

1
vim /etc/sysconfig/slapd

设置:

1
2
SLAPD_URLS="ldapi:/// ldaps:///"
SLAPD_OPTIONS=""

重启:

1
2
3
systemctl restart slapd
systemctl status slapd --no-pager -l
ss -lntp | grep 636

客户端配置 /etc/openldap/ldap.conf

1
2
3
4
URI ldaps://ldap.example.com
BASE dc=example,dc=com
TLS_CACERT /etc/openldap/certs/company-root-ca.crt
TLS_REQCERT demand

测试:

1
2
3
4
5
6
7
8
ldapsearch \
-x -LLL \
-H ldaps://ldap.example.com:636 \
-D 'cn=cas-reader,ou=system,dc=example,dc=com' \
-W \
-b 'ou=people,dc=example,dc=com' \
'(uid=alice)' \
uid cn mail

验证证书:

1
2
3
4
5
openssl s_client \
-connect ldap.example.com:636 \
-servername ldap.example.com \
-CAfile /etc/openldap/certs/company-root-ca.crt \
-verify_return_error

十、OpenLDAP 防火墙与系统调优

只允许 CAS 节点访问 636:

1
2
3
4
5
6
7
8
systemctl enable --now firewalld

firewall-cmd \
--permanent \
--add-rich-rule='rule family="ipv4" source address="192.168.9.20/32" port port="636" protocol="tcp" accept'

firewall-cmd --reload
firewall-cmd --list-rich-rules

提高文件句柄:

1
2
3
4
5
6
7
8
9
10
11
12
mkdir -p /etc/systemd/system/slapd.service.d

cat > /etc/systemd/system/slapd.service.d/limits.conf <<'EOF'
[Service]
LimitNOFILE=65536
TasksMax=65536
EOF

systemctl daemon-reload
systemctl restart slapd

systemctl show slapd -p LimitNOFILE -p TasksMax

系统参数:

1
2
3
4
5
6
7
cat > /etc/sysctl.d/99-openldap.conf <<'EOF'
fs.file-max = 1048576
net.core.somaxconn = 4096
vm.swappiness = 1
EOF

sysctl --system

不要复制大量来源不明的 TCP 参数。LDAP 性能问题更常见于索引、过滤器、磁盘和连接池。

十一、OpenLDAP 索引与 ACL 调优

CAS 使用:

1
(uid={user})

因此核心索引是:

1
uid eq

索引不是越多越好。索引会增加写入成本、空间和导入时间,只为真实查询条件创建索引。

新增索引后,已有数据需要在维护窗口重建:

1
2
3
4
5
6
7
8
9
10
systemctl stop slapd

slapindex \
-F /etc/openldap/slapd.d \
-b 'dc=example,dc=com'

chown -R ldap:ldap /var/lib/ldap
restorecon -Rv /var/lib/ldap

systemctl start slapd

验证 CAS Reader 不能读取密码:

1
2
3
4
5
6
7
8
ldapsearch \
-x -LLL \
-H ldaps://ldap.example.com:636 \
-D 'cn=cas-reader,ou=system,dc=example,dc=com' \
-W \
-b 'uid=alice,ou=people,dc=example,dc=com' \
-s base \
userPassword

不应返回密码哈希。

验证 CAS Reader 不能修改用户:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cat > /tmp/forbidden-update.ldif <<'EOF'
dn: uid=alice,ou=people,dc=example,dc=com
changetype: modify
replace: displayName
displayName: Unauthorized Change
EOF

ldapmodify \
-x \
-H ldaps://ldap.example.com:636 \
-D 'cn=cas-reader,ou=system,dc=example,dc=com' \
-W \
-f /tmp/forbidden-update.ldif

rm -f /tmp/forbidden-update.ldif

应返回权限不足。

十二、安装 JDK 21

以下步骤在 sso.example.com 执行:

1
2
3
4
5
dnf install -y java-21-openjdk-devel

java -version
javac -version
readlink -f "$(command -v java)"

CAS 7.3 在构建和运行阶段均需要 JDK 21。

十三、创建 CAS 账户与目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
useradd \
--system \
--create-home \
--home-dir /opt/cas \
--shell /usr/sbin/nologin \
cas

mkdir -p /opt/cas
mkdir -p /etc/cas/config
mkdir -p /etc/cas/services
mkdir -p /etc/cas/ldap
mkdir -p /var/log/cas
mkdir -p /var/lib/cas/heapdumps

chown -R cas:cas /opt/cas /var/log/cas /var/lib/cas
chown -R root:cas /etc/cas

chmod 0750 /etc/cas /etc/cas/config /etc/cas/services /etc/cas/ldap

十四、获取 CAS Overlay

CAS 官方推荐使用 WAR Overlay 管理部署,不要直接克隆完整 CAS 源码。

本文固定使用对应 CAS 7.3.8 的 Overlay Tag:

1
2
3
4
5
6
7
8
9
10
11
12
13
export CAS_OVERLAY_TAG='20260729172137'

cd /usr/local/src

curl -fL \
"https://github.com/apereo/cas-overlay-template/archive/refs/tags/${CAS_OVERLAY_TAG}.tar.gz" \
-o "cas-overlay-${CAS_OVERLAY_TAG}.tar.gz"

tar -xzf "cas-overlay-${CAS_OVERLAY_TAG}.tar.gz"

mv \
"cas-overlay-template-${CAS_OVERLAY_TAG}" \
/opt/cas-overlay

检查版本:

1
2
3
4
5
cd /opt/cas-overlay

grep -R "cas.version" \
gradle.properties build.gradle settings.gradle \
2>/dev/null

也可以通过 CAS Initializr 生成:

1
https://getcas.apereo.org/ui

生产环境不要直接跟踪 Overlay master,应使用固定 Tag、Release 或 Commit SHA。

十五、添加 LDAP 和 JSON Service Registry 模块

编辑:

1
2
cd /opt/cas-overlay
vim build.gradle

在现有 dependencies 中加入:

1
2
3
4
dependencies {
implementation "org.apereo.cas:cas-server-support-ldap"
implementation "org.apereo.cas:cas-server-support-json-service-registry"
}

不要创建重复的 dependencies 块。

检查:

1
2
3
grep -n \
'cas-server-support-ldap\|cas-server-support-json-service-registry' \
build.gradle

十六、构建 CAS

1
2
3
4
5
6
7
8
9
cd /opt/cas-overlay

chmod +x gradlew

./gradlew \
clean \
build \
-x test \
--no-daemon

查找 WAR:

1
2
3
4
5
find build/libs \
-maxdepth 1 \
-type f \
-name '*.war' \
-ls

安装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CAS_WAR="$(
find build/libs \
-maxdepth 1 \
-type f \
-name '*.war' \
| head -n 1
)"

test -n "${CAS_WAR}"

install \
-o cas -g cas -m 0640 \
"${CAS_WAR}" \
/opt/cas/cas.war

十七、创建 LDAP TrustStore

将 LDAP CA 复制到 CAS 节点:

1
2
3
scp \
root@ldap.example.com:/etc/openldap/certs/company-root-ca.crt \
/etc/cas/ldap/company-root-ca.crt

检查指纹:

1
2
3
openssl x509 \
-in /etc/cas/ldap/company-root-ca.crt \
-noout -subject -issuer -fingerprint -sha256

生成 TrustStore 密码:

1
2
3
LDAP_TRUSTSTORE_PASSWORD="$(
openssl rand -base64 32 | tr -d '\n'
)"

导入:

1
2
3
4
5
6
7
8
keytool \
-importcert \
-noprompt \
-alias company-ldap-root-ca \
-file /etc/cas/ldap/company-root-ca.crt \
-keystore /etc/cas/ldap/ldap-truststore.p12 \
-storetype PKCS12 \
-storepass "${LDAP_TRUSTSTORE_PASSWORD}"

权限:

1
2
3
4
5
6
7
chown root:cas \
/etc/cas/ldap/company-root-ca.crt \
/etc/cas/ldap/ldap-truststore.p12

chmod 0640 \
/etc/cas/ldap/company-root-ca.crt \
/etc/cas/ldap/ldap-truststore.p12

十八、配置 CAS LDAP 认证

创建 /etc/cas/config/cas.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# Server
server.address=127.0.0.1
server.port=8080
server.servlet.context-path=/cas
server.forward-headers-strategy=framework

cas.server.name=https://sso.example.com
cas.server.prefix=${cas.server.name}/cas

# Disable demo static authentication
cas.authn.accept.enabled=false

# JSON service registry
cas.service-registry.json.location=file:/etc/cas/services
cas.service-registry.core.init-from-json=true

# LDAP authentication
cas.authn.ldap[0].type=AUTHENTICATED
cas.authn.ldap[0].name=OpenLDAP
cas.authn.ldap[0].order=0

cas.authn.ldap[0].ldap-url=ldaps://ldap.example.com:636
cas.authn.ldap[0].base-dn=ou=people,dc=example,dc=com
cas.authn.ldap[0].search-filter=uid={user}
cas.authn.ldap[0].subtree-search=true

cas.authn.ldap[0].bind-dn=cn=cas-reader,ou=system,dc=example,dc=com
cas.authn.ldap[0].bind-credential=${LDAP_BIND_PASSWORD}

cas.authn.ldap[0].principal-attribute-id=uid
cas.authn.ldap[0].principal-attribute-list=uid,cn,sn,givenName,displayName,mail

cas.authn.ldap[0].trust-store=/etc/cas/ldap/ldap-truststore.p12
cas.authn.ldap[0].trust-store-type=PKCS12
cas.authn.ldap[0].trust-store-password=${LDAP_TRUSTSTORE_PASSWORD}

cas.authn.ldap[0].min-pool-size=3
cas.authn.ldap[0].max-pool-size=20
cas.authn.ldap[0].block-wait-time=PT3S
cas.authn.ldap[0].connect-timeout=PT5S
cas.authn.ldap[0].response-timeout=PT5S
cas.authn.ldap[0].fail-fast=true

cas.authn.ldap[0].validate-on-checkout=true
cas.authn.ldap[0].validate-periodically=true
cas.authn.ldap[0].validate-period=PT5M
cas.authn.ldap[0].validate-timeout=PT5S

# Embedded Tomcat
server.tomcat.threads.min-spare=20
server.tomcat.threads.max=200
server.tomcat.accept-count=100
server.tomcat.max-connections=8192

# Actuator
management.endpoints.enabled-by-default=false
management.endpoint.health.enabled=true
management.endpoint.info.enabled=true
management.endpoints.web.exposure.include=health,info
management.endpoint.health.show-details=when-authorized

# Logging
logging.file.name=/var/log/cas/cas.log
logging.level.root=INFO
logging.level.org.apereo.cas=INFO
logging.level.org.ldaptive=WARN

权限:

1
2
chown root:cas /etc/cas/config/cas.properties
chmod 0640 /etc/cas/config/cas.properties

AUTHENTICATED 模式会先使用 CAS Reader 搜索用户 DN,再用用户 DN 和用户密码执行 Bind。

推荐使用精确等值搜索:

1
cas.authn.ldap[0].search-filter=uid={user}

如果需要用户名或邮箱登录:

1
cas.authn.ldap[0].search-filter=(|(uid={user})(mail={user}))

同时必须保证 uidmail 唯一并建立 eq 索引。不要使用大量前后模糊匹配。

十九、配置密码与 Secret

创建 /etc/cas/cas.env

1
2
3
4
5
6
7
8
9
cat > /etc/cas/cas.env <<EOF
LDAP_BIND_PASSWORD=请替换为CAS-Reader明文密码
LDAP_TRUSTSTORE_PASSWORD=${LDAP_TRUSTSTORE_PASSWORD}
EOF

chown root:cas /etc/cas/cas.env
chmod 0640 /etc/cas/cas.env

unset LDAP_TRUSTSTORE_PASSWORD

不要将该文件提交到 Git,也不要将密码作为 JVM 命令行参数。正式环境建议使用 Vault、systemd Credential、Docker Secret 或 Kubernetes Secret。

二十、注册业务系统

创建 /etc/cas/services/example-application-10000001.json

1
2
3
4
5
6
7
8
{
"@class": "org.apereo.cas.services.CasRegisteredService",
"serviceId": "^https://app\\.example\\.com(/.*)?$",
"name": "Example Application",
"description": "Example CAS client application",
"id": 10000001,
"evaluationOrder": 10
}

权限:

1
2
3
4
5
chown root:cas \
/etc/cas/services/example-application-10000001.json

chmod 0640 \
/etc/cas/services/example-application-10000001.json

不要使用:

1
"serviceId": ".*"

每个业务系统应使用独立且明确的 HTTPS 域名、路径和属性释放策略。

二十一、配置 CAS systemd

创建 /etc/systemd/system/cas.service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[Unit]
Description=Apereo CAS Server
Documentation=https://apereo.github.io/cas/
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=cas
Group=cas

EnvironmentFile=/etc/cas/cas.env

Environment="JAVA_TOOL_OPTIONS=-Xms2g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/lib/cas/heapdumps -Xlog:gc*,safepoint:file=/var/log/cas/gc.log:time,uptime,level,tags:filecount=10,filesize=20M"

ExecStart=/usr/bin/java \
-jar /opt/cas/cas.war \
--spring.config.additional-location=file:/etc/cas/config/

Restart=on-failure
RestartSec=10s

TimeoutStartSec=600
TimeoutStopSec=60
SuccessExitStatus=143

LimitNOFILE=65536
TasksMax=65536

NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict

ReadOnlyPaths=/opt/cas
ReadOnlyPaths=/etc/cas
ReadWritePaths=/var/log/cas
ReadWritePaths=/var/lib/cas

[Install]
WantedBy=multi-user.target

如果服务器只有 4 GB 内存,将 JVM 调整为:

1
-Xms1g -Xmx1g

启动:

1
2
3
4
5
6
systemctl daemon-reload
systemctl enable --now cas

systemctl status cas --no-pager -l
journalctl -u cas -n 300 --no-pager
tail -f /var/log/cas/cas.log

检查:

1
2
3
ss -lntp | grep 8080
curl -I http://127.0.0.1:8080/cas/login
curl -fsS http://127.0.0.1:8080/cas/actuator/health | jq

二十二、配置 Nginx HTTPS

安装:

1
dnf install -y nginx

将证书放入:

1
2
/etc/nginx/ssl/sso.example.com.crt
/etc/nginx/ssl/sso.example.com.key

创建 /etc/nginx/conf.d/cas.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
upstream cas_backend {
server 127.0.0.1:8080;
keepalive 32;
}

server {
listen 80;
server_name sso.example.com;

return 301 https://$host$request_uri;
}

server {
listen 443 ssl;
http2 on;
server_name sso.example.com;

ssl_certificate /etc/nginx/ssl/sso.example.com.crt;
ssl_certificate_key /etc/nginx/ssl/sso.example.com.key;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:CAS_SSL:20m;
ssl_session_timeout 1d;

client_max_body_size 10m;

access_log /var/log/nginx/cas-access.log;
error_log /var/log/nginx/cas-error.log warn;

add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

location /cas/ {
proxy_pass http://cas_backend;

proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-Proto https;

proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering off;
}

location = /healthz {
access_log off;
proxy_pass http://cas_backend/cas/actuator/health;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
}

检查并启动:

1
2
3
4
5
6
nginx -t

setsebool -P httpd_can_network_connect 1

systemctl enable --now nginx
systemctl reload nginx

防火墙:

1
2
3
4
5
6
systemctl enable --now firewalld

firewall-cmd --permanent --add-service=ssh
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload

验证:

1
2
curl -I https://sso.example.com/cas/login
curl -fsS https://sso.example.com/healthz | jq

不要将 CAS 的 8080 直接开放到公网。

二十三、验证登录与 SSO

浏览器访问:

1
https://sso.example.com/cas/login

使用 alice 和创建 LDAP 用户时设置的密码登录。

注册 Service 后访问:

1
https://sso.example.com/cas/login?service=https%3A%2F%2Fapp.example.com%2Flogin%2Fcas

认证成功后浏览器会跳转:

1
https://app.example.com/login/cas?ticket=ST-...

业务系统调用:

1
https://sso.example.com/cas/p3/serviceValidate

并传递:

1
2
service=https://app.example.com/login/cas
ticket=ST-...

验证单点登录:

  1. 登录业务系统 A;
  2. 保持浏览器中的 CAS TGT Cookie;
  3. 访问业务系统 B;
  4. 系统 B 重定向到 CAS;
  5. CAS 发现现有 TGT;
  6. 不再要求密码,直接签发新的 ST。

退出地址:

1
https://sso.example.com/cas/logout

CAS Logout 销毁 CAS SSO 会话。业务系统本地 Session 是否同时退出,取决于是否正确接入 Single Logout。

二十四、CAS 与 LDAP 调优

24.1 JVM Heap

推荐起点:

主机内存 Xms Xmx
4 GB 1 GB 1 GB
8 GB 2 GB 2 GB
16 GB 4 GB 4 GB

保持 Xms = Xmx,但不要将 80% 以上物理内存全部分给 Heap。

查看 JVM:

1
2
3
4
5
CAS_PID="$(pgrep -f '/opt/cas/cas.war' | head -n 1)"

jcmd "${CAS_PID}" VM.flags
jcmd "${CAS_PID}" GC.heap_info
jcmd "${CAS_PID}" Thread.print

GC 日志:

1
tail -f /var/log/cas/gc.log

Heap Dump 可能包含用户、票据和配置,应按敏感数据管理。

24.2 LDAP 连接池

起点:

1
2
cas.authn.ldap[0].min-pool-size=3
cas.authn.ldap[0].max-pool-size=20

连接池不是越大越好。例如:

1
4 个 CAS 节点 × 每节点 100 个连接 = 400 个 LDAP 连接

应从 10~20 开始,监控连接池等待、LDAP QPS 和 P95,再逐步调整。

24.3 Tomcat 线程池

本文配置:

1
2
server.tomcat.threads.min-spare=20
server.tomcat.threads.max=200

线程过多会增加 Thread Stack、上下文切换,并在故障时同时冲击 LDAP。应通过压测调整,而不是直接配置 2000。

24.4 MDB 和磁盘

监控:

1
2
3
4
5
6
7
8
du -sh /var/lib/ldap
ls -lh /var/lib/ldap/data.mdb
df -h /var/lib/ldap
df -ih /var/lib/ldap

free -h
vmstat 1
iostat -xz 1

推荐 SSD/NVMe 和独立逻辑卷,不建议把 LDAP 数据目录放到高延迟网络文件系统。

24.5 只读取必要属性

1
cas.authn.ldap[0].principal-attribute-list=uid,cn,sn,givenName,displayName,mail

不要无差别读取全部属性,以减少网络、内存和属性泄漏面。

二十五、多节点高可用设计

单节点可以使用默认内存 Ticket Registry。多节点不能只在 Nginx 后面增加两台 CAS:

flowchart TD
    U[用户] --> LB[Load Balancer]
    LB --> C1[CAS Node 1]
    LB --> C2[CAS Node 2]

    C1 --> Ticket[(共享 Ticket Registry)]
    C2 --> Ticket

    C1 --> Service[(共享 Service Registry)]
    C2 --> Service

    C1 --> L1[LDAP Provider]
    C2 --> L2[LDAP Consumer]

如果 TGT 只存在 Node 1 内存,请求落到 Node 2 时会要求重新登录。

多节点应使用 CAS 当前版本支持的共享 Ticket Registry,例如 Redis、Hazelcast、JDBC 或其他实现。Sticky Session 不能替代共享票据存储。

OpenLDAP 也需要单独建设:

1
2
3
4
5
6
Provider/Consumer
Syncrepl
MirrorMode
复制延迟监控
故障切换
恢复演练

CAS 可配置多个 LDAP URL,但这不代表 LDAP 数据已经自动复制。

二十六、安全加固

26.1 禁用默认静态认证

1
cas.authn.accept.enabled=false

26.2 限制登录暴力破解

可在 Nginx 增加 IP 级限流:

1
2
3
4
5
6
7
8
9
10
limit_req_zone $binary_remote_addr zone=cas_login:10m rate=5r/s;

location = /cas/login {
limit_req zone=cas_login burst=20 nodelay;

proxy_pass http://cas_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

同时应结合:

  • CAS Authentication Throttling;
  • LDAP Password Policy;
  • WAF;
  • 异常登录检测;
  • MFA。

26.3 不记录敏感数据

日志中不能出现:

1
2
3
4
5
用户密码
LDAP Bind Credential
TGT Cookie
完整 Service Ticket
TrustStore 密码

初步扫描:

1
2
3
4
grep -RniE \
'password|credential|ticket=' \
/var/log/cas /var/log/nginx \
| head -100

26.4 Service 白名单

不要注册 .*。每个 Service 单独配置精确 HTTPS 正则、属性释放和 MFA 策略。

26.5 MFA

LDAP 只提供密码认证。高风险系统建议由 CAS 统一增加 TOTP、WebAuthn/Passkey、Duo 或风险认证,而不是让每个业务系统各自实现。

二十七、监控

CAS 至少监控:

1
2
3
4
5
6
7
8
9
10
11
JVM Heap
GC Pause
线程数量
HTTP QPS
P95/P99
登录成功率
登录失败率
LDAP 响应时间
票据签发数量
Nginx 4xx/5xx
证书有效期

OpenLDAP 至少监控:

1
2
3
4
5
6
7
8
9
10
slapd 状态
636 端口
LDAP Bind 成功率
查询 P95/P99
活跃连接
文件句柄
MDB 文件大小
磁盘容量和延迟
复制延迟
证书有效期

日志:

1
2
3
4
5
6
7
8
journalctl -u cas -f
tail -f /var/log/cas/cas.log
tail -f /var/log/cas/gc.log

journalctl -u slapd -f

tail -f /var/log/nginx/cas-access.log
tail -f /var/log/nginx/cas-error.log

Actuator 不应全部暴露公网。本文只开放 healthinfo

二十八、备份与恢复

28.1 OpenLDAP 备份

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mkdir -p /backup/openldap

systemctl stop slapd

slapcat \
-n 0 \
-F /etc/openldap/slapd.d \
-l "/backup/openldap/config-$(date +%F-%H%M%S).ldif"

slapcat \
-F /etc/openldap/slapd.d \
-b 'dc=example,dc=com' \
-l "/backup/openldap/data-$(date +%F-%H%M%S).ldif"

tar \
--xattrs --acls \
-czf "/backup/openldap/certs-$(date +%F-%H%M%S).tar.gz" \
/etc/openldap/certs

systemctl start slapd

恢复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
systemctl stop slapd

mv /etc/openldap/slapd.d \
"/etc/openldap/slapd.d.before-restore-$(date +%F-%H%M%S)"

mv /var/lib/ldap \
"/var/lib/ldap.before-restore-$(date +%F-%H%M%S)"

mkdir -p /etc/openldap/slapd.d /var/lib/ldap

slapadd \
-n 0 \
-F /etc/openldap/slapd.d \
-l /backup/openldap/config-时间.ldif

slapadd \
-F /etc/openldap/slapd.d \
-b 'dc=example,dc=com' \
-l /backup/openldap/data-时间.ldif

chown -R ldap:ldap /etc/openldap/slapd.d /var/lib/ldap
restorecon -Rv /etc/openldap/slapd.d /var/lib/ldap

slaptest -F /etc/openldap/slapd.d -u

systemctl start slapd

28.2 CAS 备份

必须备份:

1
2
3
4
5
6
7
8
9
CAS Overlay Git 仓库
build.gradle 和 gradle.properties
/etc/cas/config
/etc/cas/services
LDAP TrustStore
Nginx 配置与证书
systemd Unit
外部 Ticket Registry
外部 Service Registry
1
2
3
4
5
6
7
8
mkdir -p /backup/cas

tar \
--xattrs --acls \
-czf "/backup/cas/cas-config-$(date +%F-%H%M%S).tar.gz" \
/etc/cas \
/etc/nginx/conf.d/cas.conf \
/etc/systemd/system/cas.service

cas.env 包含密码,备份必须加密。

二十九、Docker 部署方案

Docker 运行在 Rocky Linux 10 宿主机。

生产 CAS 镜像应从自己的 Overlay 构建,不应直接把公共 CAS 快速体验镜像当作生产镜像。OpenLDAP 也采用自建 Rocky Linux 10 镜像,避免依赖已下架、弃用或版本不透明的第三方镜像。

flowchart LR
    U[用户] -->|443| N[Nginx Container]
    N -->|8080| C[CAS Container]
    C -->|636| L[Rocky 10 OpenLDAP Container]

    C --> Config[CAS Config]
    C --> Services[Service JSON]
    L --> Data[LDAP Data Volume]
    L --> TLS[LDAP TLS]

三十、安装 Docker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
dnf remove -y \
docker docker-client docker-client-latest docker-common \
docker-latest docker-latest-logrotate docker-logrotate \
docker-engine podman-docker

dnf install -y dnf-plugins-core

dnf config-manager \
--add-repo \
https://download.docker.com/linux/rhel/docker-ce.repo

dnf install -y \
docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin

systemctl enable --now docker

docker version
docker compose version

不要开放未认证的 tcp://0.0.0.0:2375

三十一、Docker 项目目录

1
2
3
4
5
mkdir -p /opt/cas-ldap-docker/{openldap,cas-overlay,cas-config,cas-services,nginx,secrets,pki}
mkdir -p /data/docker/openldap
mkdir -p /data/docker/cas/{logs,heapdumps}

cd /opt/cas-ldap-docker

三十二、构建 Rocky Linux 10 OpenLDAP 镜像

32.1 Dockerfile

openldap/Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
FROM rockylinux/rockylinux:10

RUN set -eux; \
dnf -y install dnf-plugins-core epel-release; \
dnf config-manager --set-enabled crb; \
dnf -y distro-sync; \
dnf -y install \
openldap-servers \
openldap-clients \
gettext \
openssl \
procps-ng \
findutils; \
dnf clean all; \
rm -rf /var/cache/dnf

COPY slapd.conf.template /opt/openldap/slapd.conf.template
COPY base.ldif.template /opt/openldap/base.ldif.template
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh

RUN chmod 0755 /usr/local/bin/docker-entrypoint.sh \
&& mkdir -p \
/var/lib/ldap \
/run/openldap/certs \
/input-certs \
&& chown -R ldap:ldap /var/lib/ldap /run/openldap

VOLUME ["/var/lib/ldap"]

EXPOSE 389 636

HEALTHCHECK --interval=30s --timeout=5s --retries=5 \
CMD ldapsearch -x -H ldap://127.0.0.1:389 \
-b "" -s base "(objectClass=*)" namingContexts >/dev/null || exit 1

ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

32.2 slapd.conf.template

openldap/slapd.conf.template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
include         /etc/openldap/schema/core.schema
include /etc/openldap/schema/cosine.schema
include /etc/openldap/schema/nis.schema
include /etc/openldap/schema/inetorgperson.schema

pidfile /run/openldap/slapd.pid
argsfile /run/openldap/slapd.args

modulepath /usr/lib64/openldap
moduleload back_mdb

TLSCACertificateFile /run/openldap/certs/company-root-ca.crt
TLSCertificateFile /run/openldap/certs/ldap.crt
TLSCertificateKeyFile /run/openldap/certs/ldap.key

loglevel stats

database mdb
maxsize 10737418240

suffix "dc=example,dc=com"
rootdn "cn=directory-admin,dc=example,dc=com"
rootpw ${LDAP_ADMIN_PASSWORD_HASH}

directory /var/lib/ldap

index objectClass eq
index entryUUID,entryCSN eq
index uid eq
index mail eq
index cn,sn,displayName eq,sub
index member eq

access to attrs=userPassword
by self write
by anonymous auth
by * none

access to dn.subtree="ou=people,dc=example,dc=com"
by dn.exact="cn=cas-reader,ou=system,dc=example,dc=com" read
by self read
by users read
by * none

access to dn.subtree="ou=groups,dc=example,dc=com"
by dn.exact="cn=cas-reader,ou=system,dc=example,dc=com" read
by users read
by * none

access to dn.subtree="ou=system,dc=example,dc=com"
by self read
by * none

access to *
by users read
by * none

如果镜像中的 MDB 已静态编译,moduleload back_mdb 可能需要删除。构建后必须执行 slaptest 验证。

32.3 base.ldif.template

openldap/base.ldif.template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
dn: dc=example,dc=com
objectClass: top
objectClass: dcObject
objectClass: organization
o: Example Corporation
dc: example

dn: ou=people,dc=example,dc=com
objectClass: top
objectClass: organizationalUnit
ou: people

dn: ou=groups,dc=example,dc=com
objectClass: top
objectClass: organizationalUnit
ou: groups

dn: ou=system,dc=example,dc=com
objectClass: top
objectClass: organizationalUnit
ou: system

dn: cn=cas-reader,ou=system,dc=example,dc=com
objectClass: top
objectClass: organizationalRole
objectClass: simpleSecurityObject
cn: cas-reader
description: Read-only account used by Apereo CAS
userPassword: ${LDAP_BIND_PASSWORD_HASH}

dn: uid=alice,ou=people,dc=example,dc=com
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
uid: alice
cn: Alice Zhang
sn: Zhang
givenName: Alice
displayName: Alice Zhang
mail: alice@example.com
userPassword: ${LDAP_TEST_USER_PASSWORD_HASH}

dn: cn=developers,ou=groups,dc=example,dc=com
objectClass: top
objectClass: groupOfNames
cn: developers
member: uid=alice,ou=people,dc=example,dc=com

32.4 Entrypoint

openldap/docker-entrypoint.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#!/usr/bin/env bash

set -Eeuo pipefail
umask 077

read_secret() {
local file_path="$1"

if [[ ! -r "${file_path}" ]]; then
echo "无法读取 Secret:${file_path}" >&2
exit 1
fi

tr -d '\r\n' < "${file_path}"
}

LDAP_ADMIN_PASSWORD="$(read_secret /run/secrets/ldap-admin-password)"
LDAP_BIND_PASSWORD="$(read_secret /run/secrets/ldap-bind-password)"
LDAP_TEST_USER_PASSWORD="$(read_secret /run/secrets/ldap-test-user-password)"

export LDAP_ADMIN_PASSWORD_HASH="$(slappasswd -s "${LDAP_ADMIN_PASSWORD}")"
export LDAP_BIND_PASSWORD_HASH="$(slappasswd -s "${LDAP_BIND_PASSWORD}")"
export LDAP_TEST_USER_PASSWORD_HASH="$(slappasswd -s "${LDAP_TEST_USER_PASSWORD}")"

unset LDAP_ADMIN_PASSWORD LDAP_BIND_PASSWORD LDAP_TEST_USER_PASSWORD

mkdir -p /run/openldap/certs /var/lib/ldap

install -o ldap -g ldap -m 0644 \
/input-certs/company-root-ca.crt \
/run/openldap/certs/company-root-ca.crt

install -o ldap -g ldap -m 0644 \
/input-certs/ldap.crt \
/run/openldap/certs/ldap.crt

install -o ldap -g ldap -m 0600 \
/input-certs/ldap.key \
/run/openldap/certs/ldap.key

envsubst \
'${LDAP_ADMIN_PASSWORD_HASH}' \
< /opt/openldap/slapd.conf.template \
> /run/openldap/slapd.conf

chown ldap:ldap /run/openldap/slapd.conf
chmod 0600 /run/openldap/slapd.conf

slaptest -f /run/openldap/slapd.conf -u

if [[ ! -f /var/lib/ldap/data.mdb ]]; then
echo "初始化 OpenLDAP 数据库"

envsubst \
'${LDAP_BIND_PASSWORD_HASH} ${LDAP_TEST_USER_PASSWORD_HASH}' \
< /opt/openldap/base.ldif.template \
> /run/openldap/base.ldif

slapadd \
-f /run/openldap/slapd.conf \
-l /run/openldap/base.ldif

chown -R ldap:ldap /var/lib/ldap
chmod 0700 /var/lib/ldap

shred -u /run/openldap/base.ldif
fi

unset \
LDAP_ADMIN_PASSWORD_HASH \
LDAP_BIND_PASSWORD_HASH \
LDAP_TEST_USER_PASSWORD_HASH

exec /usr/sbin/slapd \
-d 0 \
-u ldap \
-g ldap \
-f /run/openldap/slapd.conf \
-h "ldap:/// ldaps:///"

权限:

1
chmod 0755 openldap/docker-entrypoint.sh

三十三、准备 Docker LDAP 证书

证书 SAN 必须包含 Compose 服务名 openldap

1
2
3
4
5
6
7
8
cat > pki/ldap-docker.ext <<'EOF'
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=DNS:openldap,DNS:ldap.example.com
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
EOF

使用企业 CA 签发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
openssl genrsa -out pki/ldap.key 3072

openssl req \
-new -sha256 \
-key pki/ldap.key \
-out pki/ldap.csr \
-subj '/C=CN/O=Example Corporation/OU=Infrastructure/CN=ldap.example.com'

openssl x509 \
-req -sha256 -days 825 \
-in pki/ldap.csr \
-CA pki/company-root-ca.crt \
-CAkey /安全路径/company-root-ca.key \
-CAcreateserial \
-out pki/ldap.crt \
-extfile pki/ldap-docker.ext

chmod 0644 pki/company-root-ca.crt pki/ldap.crt
chmod 0600 pki/ldap.key

生产环境不要把 CA 私钥放在 Docker 主机。

三十四、构建 CAS Docker 镜像

将 Overlay 复制到项目:

1
2
3
rsync -aH --delete \
/opt/cas-overlay/ \
/opt/cas-ldap-docker/cas-overlay/

cas-overlay/Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
FROM eclipse-temurin:21-jdk-jammy AS builder

WORKDIR /workspace

COPY . .

RUN chmod +x gradlew \
&& ./gradlew clean build -x test --no-daemon \
&& CAS_WAR="$(find build/libs -maxdepth 1 -type f -name '*.war' | head -n 1)" \
&& test -n "${CAS_WAR}" \
&& cp "${CAS_WAR}" /tmp/cas.war

FROM eclipse-temurin:21-jre-jammy

RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --system --gid 10001 cas \
&& useradd \
--system \
--uid 10001 \
--gid 10001 \
--home-dir /opt/cas \
--shell /usr/sbin/nologin \
cas \
&& mkdir -p /opt/cas /var/log/cas /var/lib/cas/heapdumps \
&& chown -R cas:cas /opt/cas /var/log/cas /var/lib/cas

COPY --from=builder \
--chown=cas:cas \
/tmp/cas.war \
/opt/cas/cas.war

USER cas

EXPOSE 8080

ENTRYPOINT [
"java",
"-jar",
"/opt/cas/cas.war",
"--spring.config.additional-location=file:/etc/cas/config/"
]

生产环境应固定基础镜像版本或 Digest。

三十五、Docker CAS 配置与 TrustStore

复制原生配置:

1
2
cp /etc/cas/config/cas.properties cas-config/
cp /etc/cas/services/example-application-10000001.json cas-services/

修改 cas-config/cas.properties

1
2
3
server.address=0.0.0.0
cas.authn.ldap[0].ldap-url=ldaps://openldap:636
cas.authn.ldap[0].trust-store=/etc/cas/ldap/ldap-truststore.p12

创建 Secret:

1
2
3
4
5
6
openssl rand -base64 32 | tr -d '\n' > secrets/ldap-admin-password
openssl rand -base64 32 | tr -d '\n' > secrets/ldap-bind-password
openssl rand -base64 24 | tr -d '\n' > secrets/ldap-test-user-password
openssl rand -base64 32 | tr -d '\n' > secrets/ldap-truststore-password

chmod 0600 secrets/*

创建 TrustStore:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mkdir -p cas-config/ldap

LDAP_TRUSTSTORE_PASSWORD="$(cat secrets/ldap-truststore-password)"

keytool \
-importcert \
-noprompt \
-alias company-ldap-root-ca \
-file pki/company-root-ca.crt \
-keystore cas-config/ldap/ldap-truststore.p12 \
-storetype PKCS12 \
-storepass "${LDAP_TRUSTSTORE_PASSWORD}"

unset LDAP_TRUSTSTORE_PASSWORD

三十六、Docker Nginx 配置

nginx/default.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
upstream cas_backend {
server cas:8080;
keepalive 32;
}

server {
listen 80;
server_name sso.example.com;
return 301 https://$host$request_uri;
}

server {
listen 443 ssl;
http2 on;
server_name sso.example.com;

ssl_certificate /etc/nginx/ssl/sso.crt;
ssl_certificate_key /etc/nginx/ssl/sso.key;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:CAS_SSL:20m;
ssl_session_timeout 1d;

add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

location /cas/ {
proxy_pass http://cas_backend;

proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-Proto https;

proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering off;
}

location = /healthz {
access_log off;
proxy_pass http://cas_backend/cas/actuator/health;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
}

三十七、Docker Compose

compose.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
services:
openldap:
build:
context: ./openldap
image: company/rocky10-openldap:2.6
container_name: cas-openldap
restart: unless-stopped

secrets:
- ldap-admin-password
- ldap-bind-password
- ldap-test-user-password

volumes:
- /data/docker/openldap:/var/lib/ldap:Z
- ./pki:/input-certs:ro,Z

networks:
- identity-backend

ulimits:
nofile:
soft: 65536
hard: 65536

mem_limit: 2g
cpus: 2.0

cas:
build:
context: ./cas-overlay
image: company/apereo-cas:7.3.8
container_name: apereo-cas
restart: unless-stopped

depends_on:
openldap:
condition: service_healthy

secrets:
- ldap-bind-password
- ldap-truststore-password

environment:
JAVA_TOOL_OPTIONS: >-
-Xms2g
-Xmx2g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/lib/cas/heapdumps
-Xlog:gc*,safepoint:file=/var/log/cas/gc.log:time,uptime,level,tags:filecount=10,filesize=20M

entrypoint:
- /bin/sh
- -ec
- |
export LDAP_BIND_PASSWORD="$$(cat /run/secrets/ldap-bind-password)"
export LDAP_TRUSTSTORE_PASSWORD="$$(cat /run/secrets/ldap-truststore-password)"
exec java \
-jar /opt/cas/cas.war \
--spring.config.additional-location=file:/etc/cas/config/

volumes:
- ./cas-config:/etc/cas/config:ro,Z
- ./cas-config/ldap:/etc/cas/ldap:ro,Z
- ./cas-services:/etc/cas/services:ro,Z
- /data/docker/cas/logs:/var/log/cas:Z
- /data/docker/cas/heapdumps:/var/lib/cas/heapdumps:Z

networks:
- identity-backend

healthcheck:
test:
- CMD-SHELL
- curl -fsS http://127.0.0.1:8080/cas/actuator/health >/dev/null || exit 1
interval: 30s
timeout: 5s
retries: 10
start_period: 120s

ulimits:
nofile:
soft: 65536
hard: 65536

mem_limit: 4g
cpus: 4.0

nginx:
image: nginx:stable-alpine
container_name: cas-nginx
restart: unless-stopped

depends_on:
cas:
condition: service_healthy

ports:
- "80:80"
- "443:443"

volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro,Z
- ./pki/sso.crt:/etc/nginx/ssl/sso.crt:ro,Z
- ./pki/sso.key:/etc/nginx/ssl/sso.key:ro,Z

networks:
- identity-frontend
- identity-backend

healthcheck:
test:
- CMD-SHELL
- wget -qO- http://127.0.0.1/healthz >/dev/null || exit 1
interval: 30s
timeout: 5s
retries: 5

ulimits:
nofile:
soft: 65536
hard: 65536

mem_limit: 256m
cpus: 1.0

networks:
identity-frontend:
name: identity-frontend

identity-backend:
name: identity-backend
internal: true

secrets:
ldap-admin-password:
file: ./secrets/ldap-admin-password

ldap-bind-password:
file: ./secrets/ldap-bind-password

ldap-test-user-password:
file: ./secrets/ldap-test-user-password

ldap-truststore-password:
file: ./secrets/ldap-truststore-password

OpenLDAP 和 CAS 不映射宿主机端口,只有 Nginx 暴露 80/443。

三十八、构建和启动容器

1
2
3
4
5
cd /opt/cas-ldap-docker

docker compose config
docker compose build openldap cas
docker compose up -d

查看:

1
2
3
4
docker compose ps
docker compose logs -f openldap
docker compose logs -f cas
docker compose logs -f nginx

健康状态:

1
2
3
4
5
6
7
docker inspect \
--format '{{json .State.Health}}' \
cas-openldap | jq

docker inspect \
--format '{{json .State.Health}}' \
apereo-cas | jq

验证:

1
2
curl -I https://sso.example.com/cas/login
curl -fsS https://sso.example.com/healthz | jq

查看测试用户密码:

1
cat secrets/ldap-test-user-password

正式环境应删除测试用户。

三十九、Docker 权限、SELinux 与调优

Compose 中的 :Z 会为独占挂载设置 SELinux Label。不要因为出现 Permission denied 就关闭 SELinux。

排查:

1
2
ls -ldZ /data/docker/openldap /data/docker/cas
ausearch -m AVC -ts recent

确定容器中的 LDAP UID:

1
docker compose run --rm openldap id ldap

必要时调整:

1
2
3
4
LDAP_UID="$(docker compose run --rm openldap id -u ldap)"
LDAP_GID="$(docker compose run --rm openldap id -g ldap)"

chown -R "${LDAP_UID}:${LDAP_GID}" /data/docker/openldap

生产镜像应固定明确版本或 Digest,并执行漏洞扫描:

1
2
docker scout cves company/apereo-cas:7.3.8
docker scout cves company/rocky10-openldap:2.6

配置 Docker 日志轮转:

1
2
3
4
5
6
7
8
9
10
11
cat > /etc/docker/daemon.json <<'EOF'
{
"log-driver": "local",
"log-opts": {
"max-size": "50m",
"max-file": "5"
}
}
EOF

systemctl restart docker

CAS 自身日志目录也需要配置 Logrotate 或接入日志系统。

四十、Docker 备份

停止 CAS 写入流量:

1
docker compose stop cas

导出 LDAP:

1
2
3
4
5
6
7
mkdir -p /backup/openldap

docker compose exec -T openldap \
slapcat \
-f /run/openldap/slapd.conf \
-b 'dc=example,dc=com' \
> "/backup/openldap/docker-data-$(date +%F-%H%M%S).ldif"

冷备数据目录:

1
2
3
4
5
6
7
8
9
10
docker compose stop openldap

tar \
--xattrs --acls \
-czf "/backup/openldap/docker-volume-$(date +%F-%H%M%S).tar.gz" \
-C /data/docker \
openldap

docker compose start openldap
docker compose start cas

同时备份:

1
2
3
4
5
6
7
OpenLDAP Dockerfile 和模板
Compose
CAS Overlay
CAS 配置和 Service JSON
TLS 证书
Secret 的加密副本
Nginx 配置

四十一、常见故障排查

41.1 CAS 需要 JDK 21

1
java -version

systemd 使用的 Java 可能与当前 Shell 不同:

1
2
readlink -f /usr/bin/java
systemctl cat cas

41.2 LDAP 证书错误

错误:

1
2
PKIX path building failed
unable to find valid certification path

检查:

1
2
3
4
openssl s_client \
-connect ldap.example.com:636 \
-servername ldap.example.com \
-showcerts

检查 TrustStore:

1
2
3
4
keytool \
-list \
-keystore /etc/cas/ldap/ldap-truststore.p12 \
-storetype PKCS12

常见原因:

  • CA 未导入;
  • SAN 不包含 LDAP 域名;
  • 容器中连接 openldap,证书没有 DNS:openldap
  • TrustStore 密码错误;
  • 系统时间错误。

不要关闭主机名校验来掩盖证书问题。

41.3 LDAP Invalid Credentials

分别测试 CAS Reader 和用户:

1
2
3
4
5
6
7
8
9
10
11
ldapwhoami \
-x \
-H ldaps://ldap.example.com:636 \
-D 'cn=cas-reader,ou=system,dc=example,dc=com' \
-W

ldapwhoami \
-x \
-H ldaps://ldap.example.com:636 \
-D 'uid=alice,ou=people,dc=example,dc=com' \
-W

41.4 CAS 搜索不到用户

执行相同过滤器:

1
2
3
4
5
6
7
8
ldapsearch \
-x -LLL \
-H ldaps://ldap.example.com:636 \
-D 'cn=cas-reader,ou=system,dc=example,dc=com' \
-W \
-b 'ou=people,dc=example,dc=com' \
'(uid=alice)' \
dn uid

检查 Base DN、Search Filter、ACL 和索引。

41.5 用户搜索返回多个 DN

1
2
3
4
5
6
7
8
ldapsearch \
-x -LLL \
-H ldaps://ldap.example.com:636 \
-D 'cn=cas-reader,ou=system,dc=example,dc=com' \
-W \
-b 'dc=example,dc=com' \
'(uid=alice)' \
dn

必须通过目录治理保证 uidmail 唯一。

41.6 CAS 重定向到 HTTP 或错误端口

检查:

1
2
3
cas.server.name=https://sso.example.com
cas.server.prefix=${cas.server.name}/cas
server.forward-headers-strategy=framework

以及 Nginx:

1
2
3
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-Proto https;

41.7 Service 未注册

错误通常为:

1
Application Not Authorized to Use CAS

检查 JSON:

1
jq . /etc/cas/services/example-application-10000001.json

确认实际 Service URL 与正则完全匹配。

41.8 多节点反复要求重新登录

检查是否多节点仍使用默认内存 Ticket Registry。负载均衡 Sticky Session 只能降低概率,不能替代共享 Ticket Registry。

41.9 Docker OpenLDAP MDB 模块失败

1
2
3
4
5
docker compose run --rm openldap \
find /usr/lib64/openldap \
-maxdepth 1 \
-name '*mdb*' \
-ls

如果 MDB 静态编译,删除 moduleload back_mdb。如果模块文件名不同,按实际文件调整。

41.10 Docker Secret 多出换行

创建时使用:

1
openssl rand -base64 32 | tr -d '\n' > secret-file

读取时使用:

1
tr -d '\r\n'

四十二、生产检查清单

Rocky Linux

  • 安装安全更新;
  • Chrony 同步正常;
  • SELinux 保持 Enforcing;
  • firewalld 只开放必要端口;
  • 文件句柄已验证;
  • 证书有效期已监控;
  • 数据盘、日志盘和 inode 已监控。

OpenLDAP

  • 使用 LDAPS 或 StartTLS;
  • CAS 使用只读 Reader;
  • Reader 无法读取 userPassword
  • Reader 无法修改用户;
  • uid 搜索命中等值索引;
  • MDB Map Size 有容量余量;
  • LDAP 不暴露公网;
  • 已验证备份和恢复;
  • 生产环境有复制或灾备。

CAS

  • 使用固定 Overlay Tag;
  • JDK 为 21;
  • 禁用 Accept Static Users;
  • Service Registry 未使用 .*
  • HTTPS 和 Forwarded Header 正确;
  • LDAP CA 已进入 TrustStore;
  • LDAP Pool 根据压测调优;
  • Actuator 未全部暴露;
  • 多节点使用共享 Ticket Registry;
  • 多节点使用一致 Service Registry;
  • 日志没有密码和票据。

Docker

  • CAS 从自己的 Overlay 构建;
  • OpenLDAP 镜像可追溯;
  • 镜像固定版本或 Digest;
  • 镜像已扫描;
  • LDAP 数据持久化;
  • Secret 未提交 Git;
  • CAS 和 LDAP 未映射公网端口;
  • SELinux Label 正确;
  • 容器日志已轮转;
  • 验证过容器重建和数据恢复。

四十三、总结

CAS + LDAP 的核心链路是:

1
2
3
4
5
OpenLDAP 保存身份
-> CAS 验证身份
-> CAS 签发票据
-> 业务系统验证票据
-> 业务系统不再接触密码

生产部署最重要的原则:

  1. 使用官方推荐的 CAS WAR Overlay;
  2. 固定 CAS Tag、JDK 和依赖;
  3. LDAP 使用 TLS;
  4. CAS 只使用最小权限 Reader;
  5. 为实际搜索过滤器建立精确索引;
  6. 用户搜索必须返回唯一 DN;
  7. 不允许万能 Service 正则;
  8. 多节点必须使用共享 Ticket Registry;
  9. JVM、Tomcat 和 LDAP 连接池不能盲目调大;
  10. LDAP 数据、配置、证书和 CAS 配置都要备份;
  11. Docker 镜像必须可复现、可扫描、可回滚;
  12. SSO 是关键基础设施,必须进行监控、压测和灾备演练。

CAS 登录页看起来只是一张普通表单,但它背后站着整个企业的身份入口。一旦配置错误,不是一个页面打不开,而是一群系统同时决定今天不让大家上班。

参考资料


Rocky Linux 10 搭建 CAS + OpenLDAP 单点登录系统:原生部署、Docker 部署与生产调优
https://allendericdalexander.github.io/2026/07/31/devops/linux/rocky/rocky-linux-10-cas-openldap-sso/
作者
AtLuoFu
发布于
2026年7月31日
许可协议