1. 从内存用户到数据库:为什么必须跨过这道坎
很多团队在搭建统一认证中心时,第一版几乎都是照着官方示例跑的:配置文件里写死几个用户名和密码,登录页一测就通,单点跳转也顺滑。但只要项目一进入真实交付阶段,问题立刻暴露——用户从哪来?账号怎么管?密码怎么改?权限怎么分配?这时候,把 CAS 服务端的认证源从静态配置切换到数据库,就成了绕不开的一步。
我见过不少项目卡在这个环节:有人以为改几行配置就能搞定,结果启动报错;有人把密码字段直接明文塞进数据库,上线后被安全扫描一票否决;还有人对接完之后发现,明明数据库里有这个用户,登录却一直提示认证失败,排查半天才发现是密码加密方式对不上。这些坑其实都不复杂,但如果没有提前理清 CAS 的认证链路,就会反复试错。
这篇内容面向的是已经跑通过 CAS 基础流程、准备把用户体系落到数据库的开发者。我会围绕Spring Boot 集成 CAS 服务端这个场景,把认证源切换的完整思路、密码加密器的选择、数据库表结构设计、以及几个高频踩坑点讲透。读完之后,你应该能独立完成一套可用的数据库认证方案,并且知道每一步为什么要这么做,而不是照抄配置。
需要先明确一个概念:CAS 服务端本身是一个独立的 Web 应用,它负责“认证”这件事,而 Spring Boot 更多是作为客户端(CAS Client)或者承载 CAS Server 的宿主。本文讨论的重点,是CAS Server 侧的认证源如何对接数据库,同时会顺带说明 Spring Boot 客户端在拿到用户信息后怎么用。这个边界分清楚,后面排查问题会省很多力气。
2. CAS 认证链路拆解:数据库到底该插在哪一环
2.1 一次登录请求在服务端经历了什么
要对接数据库,先得知道 CAS 服务端处理登录时内部发生了什么。用户提交账号密码后,请求会进入 CAS 的认证管理器(AuthenticationManager),它并不直接查库,而是委托给一个或多个认证处理器(AuthenticationHandler)。每个处理器负责一种认证方式,比如静态配置、LDAP、数据库、JSON 文件等。
数据库认证对应的处理器,核心逻辑是:接收用户提交的凭证,根据配置的 SQL 语句去数据库查询对应用户记录,拿到存储的密码(或密码哈希),再用密码编码器比对用户输入的密码是否匹配。匹配成功则返回认证通过,并附带用户属性;失败则抛出认证异常。
这里有个容易被忽略的点:CAS 的认证处理器返回的不只是“通过/不通过”,它还会携带一组属性(attributes),这些属性后续会随票据传递给各个客户端。所以数据库查询的字段设计,不能只查账号密码,最好把昵称、邮箱、角色等常用信息一并查出来,避免客户端再单独查一次库。
2.2 认证源切换涉及的三类配置
把认证源从内存切到数据库,实际要动的地方分三类。第一类是数据源配置,让 CAS 服务端能连上你的数据库,这通常通过 Spring Boot 的 DataSource 或者 CAS 自身的配置完成。第二类是认证处理器配置,声明使用数据库认证,并指定查询 SQL、密码字段、密码编码器等。第三类是密码编码器配置,决定用户密码是以明文、MD5、BCrypt 还是其他方式存储和比对。
这三类配置缺一不可,而且顺序上有依赖:数据源不通,处理器就查不了库;编码器不匹配,查到了也认证失败。我在实际项目里习惯先把数据源单独验证一遍,确认能连上、能查到数据,再去配认证处理器,这样出问题时排查范围小很多。
2.3 客户端与服务端的职责边界
再强调一次边界问题。CAS 服务端只负责“你是谁”,它不负责“你能干什么”。数据库里存的用户角色、权限,服务端可以查出来作为属性传递,但真正的鉴权逻辑应该在各个客户端应用里完成。有些团队把权限判断也塞进 CAS 服务端,导致服务端越来越臃肿,后期维护很痛苦。
所以数据库表设计时,用户基础信息表放在认证库,权限相关的表可以放在同一个库,也可以分库,但认证处理器只查它需要的那部分。客户端拿到用户标识后,自己去查权限,这样职责清晰,扩展也方便。
3. 数据库表结构与密码存储的取舍
3.1 用户表字段怎么设计才够用
一个能支撑 CAS 数据库认证的用户表,至少需要这些字段:唯一主键、登录账号、密码、账号状态(启用/禁用)、以及若干业务属性。下面是一个我常用的基础结构,字段命名尽量直白,方便后续维护。
CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', username VARCHAR(64) NOT NULL COMMENT '登录账号', password VARCHAR(128) NOT NULL COMMENT '密码哈希', nickname VARCHAR(64) DEFAULT NULL COMMENT '昵称', email VARCHAR(128) DEFAULT NULL COMMENT '邮箱', status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 1启用 0禁用', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) COMMENT '用户表';几个设计要点值得说明。username上加唯一索引,既保证账号不重复,也能让认证查询走索引,速度快。password长度给到 128,是因为 BCrypt 哈希结果通常是 60 个字符,留足余量。status字段很重要,禁用用户时不需要删记录,认证处理器在 SQL 里加一个status = 1条件即可,这样被禁用的账号无法登录,但历史数据还在。
3.2 密码到底该用什么方式存
这是安全扫描的重灾区。明文存储绝对不行,这一点没有讨论余地。常见的几种方式里,MD5 和 SHA 系列属于快速哈希,计算快,但也意味着暴力破解快,而且不加盐的话,相同密码哈希值一样,容易被彩虹表命中。BCrypt 是专门为密码存储设计的慢哈希算法,自带盐值,每次加密结果都不同,安全性明显更高。
CAS 支持多种密码编码器,配置时通过passwordEncoder指定。如果历史数据已经是 MD5 存的,短期内没法全部迁移,可以先用 MD5 编码器兼容,同时在新用户注册或用户改密时逐步切换到 BCrypt。但要注意,同一个认证处理器只能配一种编码器,混用需要自定义处理器,这一点后面会讲。
提示:无论用哪种编码方式,数据库连接本身也要走加密通道,避免密码在传输环节被截获。生产环境的数据库连接串建议开启 SSL。
3.3 属性查询字段的取舍
前面提到认证处理器会返回属性。如果你的客户端需要昵称、邮箱这些信息,就在查询 SQL 里一并 select 出来。但不要贪多,把整张表所有字段都查出来,一来没必要,二来属性会随票据在网络上传递,字段越多,票据越大,还可能泄露不必要的信息。
我的做法是只查客户端真正用到的字段,比如username、nickname、email,角色信息如果客户端自己能查,就不放进 CAS 属性里。这样票据轻量,职责也清晰。
4. 认证处理器与密码编码器的落地配置
4.1 数据源接入的两种常见方式
CAS 服务端如果是基于 Spring Boot 构建的(比如用 cas-overlay 模板改造),数据源可以直接复用 Spring Boot 的DataSource配置,在application.yml里写好连接信息,CAS 的自动配置会把它注入到需要的地方。这种方式最省事,也最符合 Spring Boot 的习惯。
另一种方式是 CAS 自身的配置文件里单独声明数据源,适合 CAS 服务端和业务系统完全隔离部署的场景。两种方式没有绝对优劣,关键是保证认证处理器用的数据源和你的用户表在同一个库,或者至少能访问到。
spring: datasource: url: jdbc:mysql://db-host:3306/auth_db?useSSL=true&serverTimezone=Asia/Shanghai username: auth_user password: your_db_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 2连接池参数别设太大,认证请求本身不密集,10 个连接足够支撑中小规模系统。serverTimezone一定要配,否则时间字段可能出错,这个坑我踩过不止一次。
4.2 查询 SQL 的写法与参数绑定
数据库认证处理器的核心是那条查询 SQL。CAS 通常约定用?作为账号参数的占位符,处理器会把用户提交的账号传进去。SQL 要尽量简单,只查必要字段。
SELECT username, password, nickname, email FROM sys_user WHERE username = ? AND status = 1这里把status = 1直接写进 SQL,禁用用户自然查不到,认证就会失败,逻辑很干净。注意不要用SELECT *,字段明确列出,既提升可读性,也避免表结构变动带来的意外。
如果用的是自定义认证处理器,参数绑定方式可能不同,有的用命名参数,有的用位置参数,配置时要看清楚文档或源码,别想当然。
4.3 密码编码器的选择与配置
密码编码器决定了 CAS 怎么比对密码。如果数据库存的是 BCrypt 哈希,就配BCryptPasswordEncoder;如果存的是 MD5,就配对应的编码器。配置位置通常在 CAS 的认证配置类里,通过@Bean声明。
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }这里有个细节:BCrypt 每次加密结果不同,但比对时不需要知道盐值,编码器内部会从哈希串里解析出来,所以直接用它比对即可。而 MD5 如果不加盐,比对就是简单的字符串相等,安全性差很多。如果历史数据是 MD5 且加了固定盐,那编码器也要对应实现加盐逻辑,否则比对不上。
4.4 自定义认证处理器的适用场景
标准配置能覆盖大部分场景,但有两种情况需要自定义处理器。一是密码编码方式不统一,比如老用户 MD5、新用户 BCrypt,需要在处理器里根据用户记录判断用哪种方式比对。二是查询逻辑复杂,比如用户表分库分表,或者需要联表查询角色信息。
自定义处理器的思路是继承 CAS 提供的抽象处理器,重写认证方法,在里面完成查询和比对。写的时候注意异常处理,认证失败要抛出 CAS 认识的异常类型,否则前端拿到的错误信息会很奇怪。
5. 联调阶段最容易翻车的几个点
5.1 认证一直失败但数据库明明有数据
这是最高频的问题。排查顺序我一般是这样:先确认数据源连的是不是同一个库,有时候开发环境配了 A 库,测试环境连了 B 库,数据对不上。再确认查询 SQL 在数据库客户端里手动执行能不能查到记录,如果查不到,说明条件有问题,比如status字段值不是 1,或者账号有空格。
如果 SQL 能查到,那就是密码比对的问题。把用户输入的密码和数据库存的哈希拿出来,用同样的编码器手动比对一次,看结果是否匹配。这一步能快速定位是编码器配错了,还是密码本身存错了。我遇到过有人把密码存成了明文,但编码器配的是 BCrypt,自然永远比对失败。
5.2 登录成功但客户端拿不到用户属性
认证通过后,属性会随票据传递。如果客户端拿不到,先检查认证处理器的查询 SQL 有没有把属性字段查出来,没查自然没有。再检查客户端的属性接收配置,有些客户端默认只接收username,其他属性需要显式声明。
还有一种情况是属性名大小写不一致,CAS 内部可能做了转换,客户端按转换后的名字取。这个要看具体版本的行为,联调时把票据解析出来的属性打印一遍,一目了然。
5.3 密码升级导致的兼容性断裂
从 MD5 迁移到 BCrypt 时,如果直接全量替换,老用户会全部登录失败。稳妥的做法是双编码器兼容:认证时先按新方式比对,失败再按老方式比对,比对成功后顺手把密码重新用新方式加密存回数据库,实现平滑迁移。这个逻辑需要自定义处理器来实现,标准配置做不到。
迁移期间要监控登录失败率,如果某天失败率突然升高,可能是迁移逻辑有 bug,及时回滚。数据库操作记得加事务,避免密码更新到一半失败导致用户状态异常。
5.4 并发登录与票据管理的细节
CAS 默认支持单点登录,也支持单点登出。数据库认证本身不影响票据管理,但用户量上来后,票据存储如果还是默认的内存方式,重启服务会导致所有登录态丢失。生产环境建议把票据存储也切换到数据库或缓存,保证服务重启后用户不用重新登录。
票据的过期时间也要合理设置,太短用户频繁登录,太长又有安全风险。一般根据业务敏感度来定,普通系统几小时到一天都算合理。
6. 从能用到好用:几个提升稳定性的经验
6.1 给认证查询加缓存要谨慎
有人为了提升性能,给用户查询加缓存。这在用户量大的时候确实有效,但要注意缓存一致性:用户改密码或禁用账号后,缓存没及时失效,旧凭证还能登录,这是安全隐患。如果一定要加缓存,失效策略要设计好,改密、禁用、删除用户时都要主动清缓存。
我的建议是认证查询先不加缓存,数据库加好索引,单次查询毫秒级,对多数系统足够。真到了性能瓶颈,再考虑缓存,并且优先用短过期时间兜底。
6.2 日志要能定位到具体失败原因
认证失败时,日志里如果只有一句“认证失败”,排查起来很痛苦。建议在自定义处理器里把失败原因分类记录:用户不存在、密码错误、账号禁用,分别打不同日志。注意日志里不要打印密码明文,账号可以打,密码绝对不行。
日志级别也要控制,认证失败的日志用 warn 级别,避免刷屏;系统异常用 error,方便告警。生产环境日志量大的话,做好滚动和清理策略。
6.3 数据库连接异常时的降级表现
数据库挂了,CAS 认证会全部失败,用户无法登录。这种情况要有预案:数据库主从切换要快,连接池要有合理的超时和重试配置。超时时间别设太长,否则请求堆积,整个认证服务被拖垮。一般连接超时几秒,查询超时几秒,超过就快速失败,返回友好提示。
如果系统对可用性要求极高,可以考虑多认证源并存,数据库不可用时降级到备用认证方式,但这会增加复杂度,多数系统用不上,了解即可。
6.4 定期审计用户数据
数据库认证把用户管理权交给了你自己的系统,那就要承担起审计责任。定期检查有没有长期未登录的僵尸账号、有没有弱密码账号、有没有该禁用却还启用的账号。这些工作可以写成定时任务,也可以人工定期跑 SQL。安全这件事,工具能帮一部分,但意识更重要。
我在实际项目里会建一张登录日志表,记录每次认证的时间、账号、结果、来源 IP。出问题时能追溯,平时也能分析异常登录行为。这张表的数据量会增长很快,记得定期归档或清理。
7. 客户端侧拿到用户之后怎么用
7.1 票据校验与用户信息获取
客户端应用集成 CAS 后,用户访问受保护资源时会被重定向到 CAS 登录,登录成功后带着票据回来。客户端需要拿票据去 CAS 服务端校验,校验通过后拿到用户标识和属性。这一步通常由 CAS 客户端过滤器自动完成,开发者只需要从会话或请求上下文里取用户信息。
取到的用户信息建议封装成一个统一的上下文对象,业务代码里通过它获取当前用户,而不是到处从请求里抠。这样以后换认证方案,业务代码改动最小。
7.2 本地用户与认证用户的映射
CAS 返回的是认证库里的用户标识,但业务库里的用户表可能是另一套 ID。这时候需要一张映射关系,或者在业务用户表里存认证账号。常见做法是用认证账号作为业务用户表的唯一标识,两边保持一致,省去映射的麻烦。
如果两边确实无法统一,就在业务用户表里加一个auth_username字段,登录后根据它查业务用户。这个映射关系要维护好,认证库删用户时,业务库的关联数据也要处理,避免出现孤儿数据。
7.3 权限判断放在客户端做
前面提过,CAS 只管认证。用户登录后能访问哪些接口、哪些页面,由客户端自己判断。可以在客户端维护角色权限表,登录后根据用户标识查出角色,再做接口级或页面级的鉴权。这样 CAS 服务端保持轻量,权限逻辑贴近业务,调整起来也灵活。
如果多个客户端共用一套权限体系,可以把权限服务独立出来,各客户端调用同一个权限接口。但这是架构层面的选择,和 CAS 数据库对接本身没有强绑定,按团队实际情况决定即可。
8. 写在最后的一点个人体会
把 CAS 认证源切到数据库,技术上并不算特别复杂,难的是把每个环节的边界和依赖理清楚。我自己的习惯是,动手改配置之前,先把认证链路在纸上画一遍,标出数据从哪来、经过谁、到哪去,这样配的时候心里有数,出问题也知道去哪找。
密码存储这件事,千万别图省事。我见过太多项目因为早期用了弱哈希,后期迁移痛苦不堪。新项目直接上 BCrypt,老项目逐步迁移,这是最稳妥的路径。数据库连接、票据存储、日志审计这些配套,也建议在对接认证源的同时一并规划好,别等上线了再补,那时候改动成本高得多。
最后分享一个小技巧:联调阶段把 CAS 服务端的日志级别调到 debug,能看到认证处理器每一步的决策过程,比猜快得多。等稳定运行后再调回正常级别,避免日志量过大。这个习惯帮我省下了大量排查时间,你也可以试试。