1. 为什么 CAS 默认不查数据库,以及什么时候必须接
很多人第一次搭 CAS 单点登录,跑通官方 demo 之后都会有一个疑问:用户名密码明明写在配置文件里,怎么换成我们自己的用户表?这个问题看起来只是"改个配置",实际上背后牵扯到 CAS 的认证架构设计,理解清楚之后你才知道该在哪一层动手。
CAS 服务端的认证流程,核心是围绕AuthenticationHandler这条链来走的。默认情况下,它加载的是application.properties里写死的静态账号,比如经典的casuser/Mellon。这套机制存在的意义是让你快速验证服务端能不能起来、票据能不能正常签发,而不是给你上生产用的。一旦你要接入真实业务,用户数据必然躺在 MySQL、PostgreSQL 或者某个已有的用户中心里,这时候就必须把认证来源从"静态配置"切换到"数据库查询"。
那什么时候必须接数据库?我总结下来有三种典型场景。第一种是全新项目,用户体系还没建,CAS 作为统一认证入口,用户表直接由 CAS 托管;第二种是已有业务系统,用户数据在业务库里,CAS 只做认证不做存储,通过查询已有表来校验;第三种是用户中心模式,CAS 通过调用远程接口或者查询中间库来认证。这三种场景对应的实现方式差别很大,选错了后面会非常难受。
这里有个关键判断点:CAS 到底该不该拥有用户数据。我的经验是,如果公司只有一个业务系统,用户表放哪都行;但如果有多个系统要单点登录,用户数据最好独立出来,要么放在 CAS 自己的库,要么放在独立的用户中心,绝对不要让 CAS 去查某个业务系统的用户表。原因很简单,业务系统一旦重构表结构,你的单点登录就跟着崩,耦合太深。
下面这张表可以帮你快速判断自己属于哪种情况:
| 场景 | 用户数据位置 | CAS 角色 | 推荐方案 |
|---|---|---|---|
| 全新项目 | CAS 自己的库 | 认证+存储 | JDBC 直连查询 |
| 已有单系统 | 业务库 | 仅认证 | JDBC 查询业务库(谨慎) |
| 多系统统一认证 | 独立用户中心 | 仅认证 | 远程接口或中间库 |
| 高安全要求 | 独立库+加密 | 认证+风控 | JDBC+密码加密+审计 |
搞清楚定位之后,接下来的问题就是具体怎么接。CAS 官方提供了cas-server-support-jdbc这个模块,专门用来对接关系型数据库,这也是最主流、最稳妥的做法。下面我会从依赖引入、数据源配置、查询语句设计、密码加密、字段映射几个层面,把整个链路拆开讲透。
2. 数据库认证的整体设计与方案选型
2.1 三种主流对接方式的取舍
在动手之前,先要把方案定下来。CAS 对接数据库,业内常见的有三种做法,各有各的适用边界,我一个个说。
第一种是 JDBC 直连查询。CAS 服务端直接配置数据源,通过 SQL 语句去查用户表,拿到密码后和用户输入的做比对。这种方式最简单,配置量最小,适合用户量不大、数据库和 CAS 部署在同一内网的场景。缺点是 CAS 和数据库强耦合,数据库挂了认证就挂了,而且密码比对逻辑在 CAS 内部完成,扩展性一般。
第二种是自定义 AuthenticationHandler。你自己写一个类实现 CAS 的认证接口,在里面调用任意数据源,包括远程接口、缓存、甚至第三方认证服务。这种方式最灵活,能处理各种奇葩需求,比如多表联合查询、动态数据源、加盐逻辑特殊等。代价是要写代码、要打包、要跟着 CAS 版本升级维护,成本明显更高。
第三种是查询+密码加密器组合。这其实是第一种的增强版,在 JDBC 查询的基础上,挂一个PasswordEncoder,让 CAS 帮你处理密码比对。这种方式兼顾了简单和规范,是绝大多数项目的首选。
我的建议很直接:能用 JDBC 直连解决的,就别写自定义 Handler。自定义 Handler 看起来自由,实际上每次 CAS 大版本升级,接口签名一变你就得跟着改,维护成本远超那点灵活性带来的收益。只有当你的认证逻辑确实复杂到 SQL 表达不了,比如需要调用远程接口、需要多因素判断,才考虑自定义。
2.2 数据源该放 CAS 还是复用业务库
这是另一个高频纠结的点。CAS 服务端本身是个独立应用,它有自己的配置文件。数据源配置有两种思路:一是直接在 CAS 的配置文件里配一个指向业务库的数据源;二是让 CAS 通过 JNDI 或者外部配置去拿一个已有的数据源。
我实测下来,直接在 CAS 配置文件里配数据源是最省事的,尤其是用 Spring Boot 方式部署 CAS 的时候,application.properties里加几行就搞定。但这里有个坑:CAS 的数据源和业务系统的数据源是两套连接池,如果都指向同一个库,连接数会翻倍,高并发下可能把数据库连接打满。所以如果你的业务库连接数本来就紧张,要么给 CAS 单独配一个小连接池,要么走读写分离让 CAS 只读从库。
还有一种情况是数据库密码不能明文写在配置文件里。这时候可以用环境变量或者配置中心注入,CAS 支持${DB_PASSWORD}这种占位符写法,配合启动参数传入,安全性会好很多。
2.3 认证链路的关键节点
把方案定下来之后,脑子里要有一条清晰的认证链路。用户提交用户名密码之后,CAS 内部大致经历这么几步:请求进入AuthenticationManager,它遍历所有注册的AuthenticationHandler,找到能处理当前凭证的那个(通常是QueryDatabaseAuthenticationHandler),然后执行 SQL 查询,拿到结果后用PasswordEncoder比对密码,比对通过就返回认证成功,并附带用户属性。
理解这条链路的意义在于,出问题的时候你知道该在哪一环排查。比如"用户明明存在却提示认证失败",可能是 SQL 写错了字段;"密码对但登录不上",多半是加密器配置不匹配;"登录成功但拿不到用户信息",那是属性映射没配。每一环都有对应的排查方向,后面我会详细展开。
3. 核心配置细节与实操要点
3.1 依赖引入与版本对齐
第一步是引入 JDBC 支持模块。CAS 的模块化做得非常彻底,默认的cas-server-webapp是不带数据库支持的,你得手动加。以 Gradle 为例,在build.gradle里加上:
dependencies { implementation "org.apereo.cas:cas-server-support-jdbc:${casVersion}" implementation "org.apereo.cas:cas-server-support-jdbc-drivers:${casVersion}" implementation "mysql:mysql-connector-java:8.0.33" }这里有个必须注意的点:cas-server-support-jdbc的版本一定要和 CAS 服务端版本严格一致。CAS 的模块版本管理很严格,版本错位轻则启动报错,重则认证行为异常。我见过有人图省事用了相近版本,结果启动时各种NoSuchMethodError,排查半天才发现是版本没对齐。
cas-server-support-jdbc-drivers这个模块是自动装配数据库驱动的,如果你用的是 MySQL 8,它会把驱动也带进来。但保险起见,我还是建议显式声明驱动依赖,避免驱动版本和数据库不匹配导致的连接问题。
3.2 数据源配置的完整写法
数据源配置是重头戏。在application.properties里,你需要配置这么几块内容:
cas.authn.jdbc.query[0].url=jdbc:mysql://127.0.0.1:3306/user_center?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai cas.authn.jdbc.query[0].user=readonly_user cas.authn.jdbc.query[0].password=${DB_PASSWORD} cas.authn.jdbc.query[0].driverClass=com.mysql.cj.jdbc.Driver cas.authn.jdbc.query[0].sql=SELECT password, salt, status FROM sys_user WHERE username=? cas.authn.jdbc.query[0].fieldPassword=password cas.authn.jdbc.query[0].fieldExpired=expired cas.authn.jdbc.query[0].fieldDisabled=status cas.authn.jdbc.query[0].passwordEncoder.type=BCRYPT逐行解释一下。query[0]这个下标表示这是第 0 个查询认证器,CAS 支持配置多个,比如同时查两个库,用下标区分。url里我特意加了serverTimezone,MySQL 8 不加这个参数经常报时区错误,这是个高频坑。user建议用只读账号,CAS 只需要查询权限,给写权限纯属给自己埋雷。
sql这条语句是核心,它决定了 CAS 怎么查用户。注意这里用的是?占位符,CAS 会自动把用户输入的用户名填进去,千万不要用字符串拼接,否则就是 SQL 注入的活靶子。查询结果里至少要包含密码字段,其他字段按需。
fieldPassword告诉 CAS 哪一列是密码,fieldExpired和fieldDisabled是可选的,用来支持账号过期和禁用状态。如果你的表里没有这两个字段,可以不配,CAS 会跳过对应检查。
3.3 密码加密器的选择与匹配
密码加密器是认证能否成功的关键。CAS 支持多种加密方式,常见的有NONE(明文)、MD5、SHA-256、BCRYPT、PBKDF2等。选哪个取决于你数据库里密码是怎么存的。
如果你的系统是新做的,强烈建议用 BCRYPT。它自带盐值、计算强度可调、抗彩虹表,是目前密码存储的主流选择。配置就一行passwordEncoder.type=BCRYPT,CAS 会自动处理比对。
如果数据库里存的是 MD5 加盐,配置会复杂一点:
cas.authn.jdbc.query[0].passwordEncoder.type=DEFAULT cas.authn.jdbc.query[0].passwordEncoder.characterEncoding=UTF-8 cas.authn.jdbc.query[0].passwordEncoder.encodingAlgorithm=MD5 cas.authn.jdbc.query[0].passwordEncoder.secret=your_salt_value这里的secret就是盐值。注意,CAS 的加盐逻辑是"密码+盐"还是"盐+密码",不同版本可能有差异,配置完一定要拿一个已知账号实测,别想当然。
我踩过的一个坑是:数据库里存的是MD5(密码+盐),但 CAS 默认按MD5(盐+密码)算,结果怎么都对不上。后来翻了源码才发现顺序问题,改成自定义PasswordEncoder才解决。所以加密器配置完必须实测,这是铁律。
3.4 用户属性映射
认证通过只是第一步,很多时候业务系统还需要拿到用户的其他信息,比如昵称、邮箱、角色。CAS 支持在查询时把这些字段一并查出来,作为属性返回给客户端。
配置方式是在 SQL 里多查几列,然后通过principalAttributeMap映射:
cas.authn.jdbc.query[0].sql=SELECT password, nickname, email, dept_id FROM sys_user WHERE username=? cas.authn.jdbc.query[0].principalAttributeMap.nickname=nickname cas.authn.jdbc.query[0].principalAttributeMap.email=email cas.authn.jdbc.query[0].principalAttributeMap.deptId=dept_id这样客户端拿到票据校验结果时,就能从属性里读到这些值。注意,属性名不要用password这种敏感字段,CAS 默认会过滤掉,但你自己也别作死往里塞。
4. 完整实操流程与关键环节实现
4.1 建表与测试数据准备
动手配置之前,先把表建好。我以一个典型的用户表为例:
CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) NOT NULL UNIQUE, password VARCHAR(128) NOT NULL, salt VARCHAR(32) DEFAULT NULL, nickname VARCHAR(64) DEFAULT NULL, email VARCHAR(128) DEFAULT NULL, status TINYINT DEFAULT 1, expired TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;字段设计有几个讲究。username加唯一索引,避免重复账号;password长度给足,BCRYPT 出来的哈希是 60 位,MD5 是 32 位,留 128 位够用;status和expired用 TINYINT,1 表示正常/未过期,0 表示禁用/已过期,和 CAS 的字段语义对齐。
插入一条测试数据,密码用 BCRYPT 加密。你可以用在线工具或者写个小程序生成,比如密码是Test@123,BCRYPT 之后大概是$2a$10$...这种格式。千万别直接存明文测试,养成好习惯。
4.2 CAS 服务端配置落地
把前面讲的配置整合到application.properties里,一个完整的可用配置长这样:
# 数据源 cas.authn.jdbc.query[0].url=jdbc:mysql://127.0.0.1:3306/user_center?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false cas.authn.jdbc.query[0].user=cas_reader cas.authn.jdbc.query[0].password=${DB_PASSWORD} cas.authn.jdbc.query[0].driverClass=com.mysql.cj.jdbc.Driver # 查询语句 cas.authn.jdbc.query[0].sql=SELECT password, nickname, email, status, expired FROM sys_user WHERE username=? # 字段映射 cas.authn.jdbc.query[0].fieldPassword=password cas.authn.jdbc.query[0].fieldDisabled=status cas.authn.jdbc.query[0].fieldExpired=expired # 密码加密 cas.authn.jdbc.query[0].passwordEncoder.type=BCRYPT # 属性映射 cas.authn.jdbc.query[0].principalAttributeMap.nickname=nickname cas.authn.jdbc.query[0].principalAttributeMap.email=email配置完之后启动 CAS,观察日志。如果看到QueryDatabaseAuthenticationHandler被注册,说明配置生效了。如果没看到,多半是依赖没引对或者配置前缀写错了。
4.3 认证流程实测与日志观察
配置完必须实测。打开 CAS 登录页,输入数据库里的账号密码,观察几个关键点。
第一,看 CAS 日志里有没有执行 SQL。开启 DEBUG 日志后,你能看到类似Executing query [SELECT password...] with args [testuser]的输出,这说明查询确实走了数据库。如果没有这条日志,说明认证器没生效,回去检查依赖和配置。
第二,看认证结果。成功的话会跳转到目标应用并带上 ticket;失败的话登录页会提示错误。失败时不要只看页面提示,一定要翻日志,CAS 会把具体失败原因打出来,比如Authentication failed: password does not match或者User account is disabled,这些信息比页面提示有用得多。
第三,验证属性传递。在客户端应用里打印一下拿到的用户属性,确认 nickname、email 这些字段有没有正确传过来。如果属性为空,检查principalAttributeMap的配置和 SQL 里有没有查对应的列。
4.4 多数据源与多认证器配置
有些场景需要同时支持多种认证方式,比如内部员工查 A 库,外部用户查 B 库。CAS 支持配置多个查询认证器,用下标区分:
cas.authn.jdbc.query[0].sql=SELECT password FROM employee WHERE username=? cas.authn.jdbc.query[0].url=jdbc:mysql://127.0.0.1:3306/hr_db cas.authn.jdbc.query[1].sql=SELECT password FROM customer WHERE username=? cas.authn.jdbc.query[1].url=jdbc:mysql://127.0.0.1:3306/crm_dbCAS 会按顺序尝试,第一个查不到就查第二个。注意,多个认证器之间是"或"的关系,任何一个成功就算认证通过。如果你需要更复杂的逻辑,比如"必须两个都通过",那就得自定义 Handler 了。
这里有个性能提醒:认证器越多,单次登录的查询次数越多。如果配了五六个,登录延迟会明显上升。所以能用一条 SQL 解决的,就别拆成多个认证器。
5. 常见问题与排查技巧实录
5.1 认证失败类问题速查
认证失败是最常见的问题,原因五花八门。我整理了一张速查表,覆盖了八成以上的场景:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 提示用户名密码错误 | SQL 查不到用户 | 检查 SQL 语句、表名、字段名 |
| 用户存在但认证失败 | 密码加密方式不匹配 | 核对数据库存储格式与加密器配置 |
| 提示账号被禁用 | status 字段值不对 | 确认 1/0 语义与 CAS 期望一致 |
| 提示账号过期 | expired 字段误配 | 检查字段映射和值 |
| 登录成功但无属性 | 属性映射未配置 | 检查 principalAttributeMap |
| 启动报驱动找不到 | 驱动依赖缺失 | 补上对应数据库驱动 |
拿"用户存在但认证失败"这个说,最常见的就是加密方式不匹配。数据库存的是 MD5,你配了 BCRYPT,那肯定对不上。排查方法很简单:拿数据库里的密码值,用你配置的加密方式重新算一遍,看能不能对上。对不上就是配置错了。
5.2 连接与性能类问题
数据库连接问题也很常见。典型症状是启动时报Communications link failure或者登录时卡很久然后超时。
第一个排查点是网络。CAS 服务端和数据库之间能不能通,用telnet或者nc测一下端口。别笑,我见过有人配了半天,最后发现是防火墙没开。
第二个排查点是连接池。CAS 默认用的是 HikariCP,连接数默认值可能偏小。如果并发登录量大,会出现"获取连接超时"。可以在配置里调整:
cas.authn.jdbc.query[0].pool.maxSize=20 cas.authn.jdbc.query[0].pool.minSize=5 cas.authn.jdbc.query[0].pool.timeout=5000maxSize根据你的并发量来定,一般 20 到 50 够用。注意,这个连接池是 CAS 自己的,和业务系统的连接池是分开的,别把数据库总连接数算超了。
第三个排查点是 SQL 性能。如果用户表数据量大,username字段又没索引,每次登录都全表扫描,慢是必然的。务必给 username 加索引,这是基本操作。
5.3 密码加密踩坑实录
密码加密这块我踩过的坑最多,挑几个典型的说说。
第一个坑是盐值顺序。前面提过,CAS 的加盐逻辑和你想的可能不一样。解决办法是先用一个已知密码手动算一遍,确认顺序,再配到 CAS 里。
第二个坑是编码问题。中文密码或者特殊字符,如果编码不一致,算出来的哈希完全不同。配置里一定要显式指定characterEncoding=UTF-8,别依赖默认值。
第三个坑是 BCRYPT 的强度。BCRYPT 默认强度是 10,算一次大概几十毫秒。如果强度调到 14 以上,单次认证可能超过一秒,高并发下直接拖垮服务。强度 10 到 12 是甜点区,别盲目调高。
第四个坑是历史数据迁移。老系统用 MD5,新系统想换 BCRYPT,不能直接换,否则老用户全登不上。正确做法是双加密器并存,登录时先按新方式验,失败再按老方式验,验过之后把密码升级成新格式。CAS 支持配置多个加密器,但升级逻辑得自己写,这块要有心理准备。
5.4 属性传递与客户端对接问题
认证通过之后,客户端拿不到属性也是高频问题。排查思路是这样的:先确认 CAS 服务端有没有把属性放进 principal,再确认客户端有没有正确读取。
服务端这边,看日志里 principal 的内容,如果属性为空,说明 SQL 没查出来或者映射没配。客户端这边,不同语言的客户端读取属性的方式不一样,Java 客户端一般通过AttributePrincipal拿,配置不对就读不到。
还有一个容易忽略的点:属性名大小写。CAS 内部对属性名是大小写敏感的,服务端配的是nickname,客户端读的是nickName,那就读不到。统一用全小写或者驼峰,团队内约定好。
6. 生产环境加固与扩展思路
6.1 安全加固的几个必做项
demo 跑通和生产可用之间,还差一堆安全加固。我列几个必做的。
数据库账号必须最小权限。CAS 只需要SELECT,那就只给SELECT,别图省事给ALL。万一 CAS 被攻破,攻击者也只能读不能写,损失可控。
密码字段传输加密。CAS 和数据库之间的连接,如果走的是内网可以接受,但如果是跨机房,建议开 SSL。MySQL 的useSSL=true配上证书,能防住中间人。
敏感配置外置。数据库密码不要写在application.properties里提交到代码库,用环境变量或者配置中心。CAS 支持${DB_PASSWORD}占位符,启动时通过-DDB_PASSWORD=xxx传入,这样代码库里就没有明文密码了。
登录失败次数限制。CAS 本身有cas.authn.throttle相关配置,可以限制同一账号或同一 IP 的失败次数,防暴力破解。这个功能默认可能没开,生产环境务必配上。
6.2 高可用与性能优化
CAS 作为认证入口,是单点,挂了所有系统都登不上。生产环境至少要部署两个节点,前面挂负载均衡。这里有个坑:CAS 的 ticket 默认存在内存里,多节点之间不共享,用户在 A 节点登录,请求打到 B 节点就认不出来。解决办法是把 ticket 存储换成 Redis 或者数据库,配置cas.ticket.registry相关参数即可。
性能方面,除了前面说的连接池和索引,还可以加缓存。用户信息变化不频繁,可以在 CAS 前面加一层缓存,减少数据库查询。不过缓存要考虑失效策略,用户改了密码得能及时生效,这块要权衡。
6.3 后续可扩展的方向
数据库认证跑通之后,还有很多可以扩展的方向。比如接入多因素认证,密码通过之后再要求短信验证码;比如接入用户行为分析,记录登录 IP、时间、设备,异常登录告警;比如对接已有的用户中心,把 JDBC 查询换成远程接口调用。
我个人觉得,认证这件事,简单可靠比功能花哨重要得多。很多团队一上来就想搞生物识别、行为验证,结果基础的用户名密码都没做扎实,登录失败率居高不下。先把 JDBC 这条链路做稳,把日志、监控、告警配齐,再考虑锦上添花的东西。
最后分享一个我自己的小习惯:每次改完 CAS 配置,我都会用一个脚本跑一遍冒烟测试,覆盖正常登录、密码错误、账号禁用、账号过期四种情况,确认都符合预期再上线。这个脚本不复杂,但帮我挡掉过好几次配置失误导致的全员登录故障。认证系统这种东西,出问题的代价太大,多花十分钟测试,比事后救火划算得多。