随着企业内部系统不断增加,每个系统分别保存用户名和密码,会逐渐暴露出以下问题:
用户需要记住多套账号和密码;
入职、转岗、离职需要在多个系统重复维护;
密码策略、账户锁定和权限回收无法统一;
每个业务系统都要重复开发登录和用户查询逻辑;
登录审计、异常检测和多因素认证难以集中建设;
用户访问不同系统时需要反复登录。
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 认证处理器:
CAS 使用只读服务账号连接 OpenLDAP;
根据用户名搜索用户 DN;
使用用户 DN 和用户提交的密码执行 LDAP Bind;
Bind 成功后,CAS 创建登录会话并签发 TGT;
用户访问业务系统时,CAS 为该业务系统签发一次性 Service Ticket;
业务系统通过后端请求向 CAS 校验 Service Ticket;
校验通过后,业务系统建立自己的本地会话。
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-releaseuname -runame -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:
生产环境不建议直接关闭 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 强装。
5.2 检查目录与服务 1 2 3 4 ls -lah /etc/openldapls -lah /etc/openldap/slapd.dls -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 slapdrm -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.ldifchown -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_PASSWORDecho LDAP_ADMIN_PASSWORD_HASH="$(slappasswd -s "${LDAP_ADMIN_PASSWORD} " ) " unset LDAP_ADMIN_PASSWORDread -rsp "CAS Reader Password: " CAS_READER_PASSWORDecho CAS_READER_PASSWORD_HASH="$(slappasswd -s "${CAS_READER_PASSWORD} " ) " read -rsp "Alice Password: " ALICE_PASSWORDecho 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.ldifunset \ 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-pkicd /root/ldap-pki openssl genrsa -out company-root-ca.key 4096chmod 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 3072chmod 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.ldifshred -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.dcat > /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 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.ldifrm -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 -versionreadlink -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 \ casmkdir -p /opt/casmkdir -p /etc/cas/configmkdir -p /etc/cas/servicesmkdir -p /etc/cas/ldapmkdir -p /var/log/casmkdir -p /var/lib/cas/heapdumpschown -R cas:cas /opt/cas /var/log/cas /var/lib/caschown -R root:cas /etc/caschmod 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-overlaychmod +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.p12chmod 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.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 cas.authn.accept.enabled =false cas.service-registry.json.location =file:/etc/cas/services cas.service-registry.core.init-from-json =true 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 server.tomcat.threads.min-spare =20 server.tomcat.threads.max =200 server.tomcat.accept-count =100 server.tomcat.max-connections =8192 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.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.propertieschmod 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}))
同时必须保证 uid 和 mail 唯一并建立 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.envchmod 0640 /etc/cas/cas.envunset 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.jsonchmod 0640 \ /etc/cas/services/example-application-10000001.json
不要使用:
每个业务系统应使用独立且明确的 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 ServerDocumentation =https://apereo.github.io/cas/After =network-on line.targetWants =network-on line.target[Service] Type =simpleUser =casGroup =casEnvironmentFile =/etc/cas/cas.envEnvironment ="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 -failureRestartSec =10 sTimeoutStartSec =600 TimeoutStopSec =60 SuccessExitStatus =143 LimitNOFILE =65536 TasksMax =65536 NoNewPrivileges =true PrivateTmp =true ProtectHome =true ProtectSystem =strictReadOnlyPaths =/opt/casReadOnlyPaths =/etc/casReadWritePaths =/var/log/casReadWritePaths =/var/lib/cas[Install] WantedBy =multi-user.target
如果服务器只有 4 GB 内存,将 JVM 调整为:
启动:
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-pagertail -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 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-...
验证单点登录:
登录业务系统 A;
保持浏览器中的 CAS TGT Cookie;
访问业务系统 B;
系统 B 重定向到 CAS;
CAS 发现现有 TGT;
不再要求密码,直接签发新的 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/ldapls -lh /var/lib/ldap/data.mdbdf -h /var/lib/ldapdf -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 -ftail -f /var/log/cas/cas.logtail -f /var/log/cas/gc.log journalctl -u slapd -ftail -f /var/log/nginx/cas-access.logtail -f /var/log/nginx/cas-error.log
Actuator 不应全部暴露公网。本文只开放 health 和 info。
二十八、备份与恢复 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 slapdmv /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-时间.ldifchown -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/openldapmkdir -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 pipefailumask 077read_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_PASSWORDmkdir -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.confchown ldap:ldap /run/openldap/slapd.confchmod 0600 /run/openldap/slapd.conf slaptest -f /run/openldap/slapd.conf -uif [[ ! -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.ldiffi unset \ LDAP_ADMIN_PASSWORD_HASH \ LDAP_BIND_PASSWORD_HASH \ LDAP_TEST_USER_PASSWORD_HASHexec /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.extchmod 0644 pki/company-root-ca.crt pki/ldap.crtchmod 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 builderWORKDIR /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-jammyRUN 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 casEXPOSE 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-passwordchmod 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 写入流量:
导出 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
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
必须通过目录治理保证 uid 和 mail 唯一。
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
读取时使用:
四十二、生产检查清单 Rocky Linux
OpenLDAP
CAS
Docker
四十三、总结 CAS + LDAP 的核心链路是:
1 2 3 4 5 OpenLDAP 保存身份 -> CAS 验证身份 -> CAS 签发票据 -> 业务系统验证票据 -> 业务系统不再接触密码
生产部署最重要的原则:
使用官方推荐的 CAS WAR Overlay;
固定 CAS Tag、JDK 和依赖;
LDAP 使用 TLS;
CAS 只使用最小权限 Reader;
为实际搜索过滤器建立精确索引;
用户搜索必须返回唯一 DN;
不允许万能 Service 正则;
多节点必须使用共享 Ticket Registry;
JVM、Tomcat 和 LDAP 连接池不能盲目调大;
LDAP 数据、配置、证书和 CAS 配置都要备份;
Docker 镜像必须可复现、可扫描、可回滚;
SSO 是关键基础设施,必须进行监控、压测和灾备演练。
CAS 登录页看起来只是一张普通表单,但它背后站着整个企业的身份入口。一旦配置错误,不是一个页面打不开,而是一群系统同时决定今天不让大家上班。
参考资料