这篇作为整个SSO专栏的架构基线。后面几篇涉及的服务名、表名、接口路径、Redis Key以及JWT约定都以本文为准。下文认证中心均指专栏服务base-oauth2与第01、02篇中的「认证中心」同义。上一篇PRD把问题拆成了四件事统一身份、统一登录、统一登出、可扩展接入。真正困难的地方不是实现一次登录而是让十年前的Web系统、现在的前后端分离应用以及未来的新系统共用一套身份体系。落到架构层面上我们当时面临的不是「选CAS还是选OAuth2」这么简单而是三类系统形态不一样凭证形态也不一样还要共用同一个认证中心。为什么拆成四个服务先把几个容易混在一起的职责分开CAS解决的是「你是谁」。用户到认证中心登录认证中心发TGT和ST第三方Web应用拿ST向认证中心验票在本地建Session。这条链路不需要业务JWT。OAuth2解决的是「应用如何委托认证中心完成授权登录」。在我们的场景里前后端分离管理后台采用OAuth2授权码流程。管理后台没有服务端Session浏览器只认JSON接口。网关作为OAuth2 Client把用户带到认证中心拿回授权码后再换访问令牌。这条链路拿到的是OAuth2访问令牌而不是系统内部使用的业务JWT。JWT解决的是「管理后台后续API怎么鉴权」。网关换到OAuth2访问令牌之后还要再调service-umd换一张带用户id、工号、邮箱的业务JWT。这张令牌的签发、登出黑名单、issuer约定都归service-umd管不放在CAS里做。因此我们没采用CAS统一签发JWT的方案。CAS只负责身份认证和票据生命周期业务JWT由service-umd维护登录体系和业务权限体系保持解耦。用户主数据也拆成两个服务base-umd负责员工主数据管理。CAS登录时邮箱和钉钉扫码要先到这里解析出工号或邮箱再交给JDBC认证。service-umd负责管理端用户服务查本地用户表、签JWT、写登出Redis。OAuth2 SSO成功后网关调的是service-umd的token接口这个接口设计上就是内部接口只给网关调用不应该直接暴露给外部系统。两个服务名字接近一个偏主数据门面一个偏管理端令牌和登出职责不能混。service-gateway只做入口OAuth2登录、Bearer校验、路由转发。它不接用户库也不签发JWT。很多团队习惯让网关直连用户表做权限判断我们当时刻意没这么做后面「关键取舍」一节会展开原因。整体架构部署4个微服务逻辑上划分为三层认证层、网关层、用户与身份服务层。管理后台走OAuth2不安装CAS Client协作平台和知识库直连认证中心校验CAS服务票据。base-oauth2仅在单点登出时回调service-umd业务JWT由service-gateway向service-umd换取。Redis分工base-oauth2存TGT/STservice-umd写登出标记service-gateway读黑名单与SSO登出时间。技术选型如下组件选型说明IdPApereo CAS 6.3 OverlayCAS协议、OAuth2授权、SLO网关Spring Cloud Gateway WebFluxOAuth2 Client、JWT校验用户服务Spring Boot 2.x MyBatis Plus用户主数据与JWT签发缓存RedisTGT/ST票据、登出标记、JWT黑名单包名com.column.sso.*业务扩展类统一前缀base-oauth2基于Apereo CAS Overlay构建作为整个系统的统一身份提供方IdP。对传统Web应用它提供CAS协议通过ST完成认证对前后端分离应用则通过OAuth2授权码流程为网关提供登录能力。TGTTicket Granting Ticket票据授予票据与STService Ticket服务票据是CAS协议的两类核心票据。服务名里的oauth2取的是广义身份认证含义并不是另起一套独立的OAuth2 Server。四个服务各解决什么问题base-oauth2是认证中心登录页、多方式认证、TGT/ST、服务注册、全局登出编排都在这里。它不做业务JWT签发避免认证中心和业务权限绑死。service-gateway是管理后台的统一入口OAuth2登录、授权码换访问令牌、再换业务JWT、后续Bearer校验和路由。它不直连用户库登出校验只读Redis。base-umd是用户主数据平台员工表、组织信息、邮箱/钉钉扫码解析。CAS认证前需要它帮忙把各种登录入口归一到同一个人。service-umd是管理端用户服务签JWT、处理SSO登出回调、维护JWT黑名单。SLO时base-oauth2调它写Redisservice-gateway读Redis拦截旧令牌。服务核心能力不做什么base-oauth2多方式登录、JDBC认证、服务注册JSON、全局登出编排不签发业务JWTservice-gatewayOAuth2登录入口、授权码换取访问令牌后再换取业务JWT、Bearer校验不直连用户库base-umd用户主数据CRUD、邮箱/扫码查用户不签发JWTservice-umd管理端JWT、登出回调、JWT黑名单管理不承担CAS登录页三条SSO链路后面的实现篇会逐条展开这里先把架构边界和流程定下来。时序图里的服务名、接口路径与后文API表一致。链路A前后端分离管理后台OAuth2→JWT这里有一个容易误解的地方网关OAuth2登录成功后不会直接用CAS OAuth2模块签发的访问令牌当业务JWT而是拿OAuth2身份里的loginName再调service-umd换一张issuerumd-sso的管理端JWT。原因见下文「业务JWT为什么独立于CAS管理」。前端只认service-gateway的OAuth2入口/api/service-gateway/admin/openapi/**不安装CAS Client。JWT通过授权回调时的URL query parameterx-access-token下发后续请求走HeaderAuthorization: Bearer {token}。链路B协作平台A/知识库CAS ST第三方产品是传统Web应用本地Session是现成的硬塞JWT反而要改更多。CAS ST验票后建Session是这类系统改造成本最低的路径。base-oauth2侧用RegexRegisteredServiceJSON登记serviceId正则配置logoutType: BACK_CHANNEL。应用侧过滤器与认证器改造见第07篇。链路C单点登出SLO→Redis→网关拦截全局登出要同时处理两类凭证第三方应用的本地Session和管理后台手里还没过期的JWT。Session靠CAS Back-Channel SLO通知应用销毁JWT靠service-umd写Redis、service-gateway比对token签发时间与登出时间戳。service-umd写入service-gateway:user:sso:logout:{userId}service-gateway的JwtAuthGatewayFilterFactory对issuerumd-sso的token校验SSO登出时间。普通登出时service-umd另写入service-gateway:user:logout:{token}JWT黑名单由service-gateway读取校验。数据设计MySQL存员工主数据、RBAC和登录审计Redis存TGT/ST、登出标记和JWT黑名单接入应用在base-oauth2启动时从JSON加载RegisteredService。存储内容读写服务MySQL员工主数据、RBAC、登录审计base-oauth2、base-umd、service-umdRedisTGT/STApereo CAS Redis Ticket Registry、SSO登出标记、JWT黑名单base-oauth2、service-gateway、service-umdJSON文件接入应用RegisteredServicebase-oauth2启动加载CAS产生的TGT和ST是临时票据只在认证会话期间有效没有长期业务价值因此只保存在Redis不进入MySQL。service-gateway不直连数据库登出校验只读Redis。用户主数据表base-umd / service-umd 共用专栏表名umd_user。base-oauth2JDBC认证、JWT签发、邮箱/工号/钉钉扫码归一均依赖此表。字段说明SSO用途id主键JWT的subRedis登出key里的userIdemail企业邮箱邮箱登录CAS JDBC匹配字段job_number工号工号登录优先作为CAS Principalmobile手机号找回密码、Legacy登录password密码密文JDBC密码校验bcrypt历史兼容md5ding_userid钉钉用户ID钉钉扫码关联主用户name姓名展示status启用状态CAS SQLstatus1delete_flag软删除标记CAS SQLdelete_flag0resetting_password密码重置中标记密码过期策略password_updated_at密码更新时间180天改密策略WHERE (email :username OR job_number :username) AND delete_flag 0 AND status 1无login_name列。API参数loginName语义为email、job_number或mobileCAS Principal优先用job_number无工号时回退email与第02篇PRD一致。钉钉扫码辅助表base-umd专栏表名umd_ding_user。扫码授权码经unionid/ding_userid定位员工再关联umd_user。字段说明ding_userid钉钉用户IDunionid钉钉unionidjob_number工号email邮箱mobile手机号delete_flag软删除管理后台授权表base-umd / service-umdumd_user_role、umd_role、umd_role_priv、umd_priv、umd_module五张表管RBAC不参与CAS登录认证。第04篇展开用户表RBAC后续按需引用。base-oauth2辅助表COM_AUDIT_TRAIL记登录审计也是JDBC登录节流的数据源。RegisteredService通过services/*.json按环境管理第05篇给样例不走数据库注册。Redis Key约定Key用途service-gateway:user:sso:logout:{userId}SSO全局登出Unix时间戳service-gateway:user:logout:{token}单JWT黑名单service-gateway:user:logout:at:{userId}用户级登出时间JWT约定service-umd签发issuer为umd-ssosub取userIdclaims包含id、job_number、email。授权回调时通过URL query parameterx-access-token下发后续请求通过HeaderAuthorization: Bearer {token}携带。API边界专栏命名base-oauth2→base-umd方法路径用途GET/center/base-umd/user/detail/email/{email}邮箱登录解析用户GET/center/base-umd/user/detail/ding/scan/{code}企业扫码解析用户service-gateway→service-umd方法路径用途GET/api/service-umd/admin/user/token?loginNameOAuth2成功后换取业务JWTbase-oauth2→service-umdSLO回调方法路径用途POST/api/service-umd/admin/user/logout/email/{emailOrJobNumber}全局登出写入Redisservice-gateway对外前缀用途/api/service-gateway/admin/openapi/**OAuth2 callback与白名单其他受保护路由JwtAuth过滤器服务注册策略base-oauth2同一套CAS中心通过不同的RegisteredService类型同时服务OAuth2网关链路和CAS ST第三方链路应用类型RegisteredService类型关键字段管理后台OAuth2OAuthRegisteredServiceclientId、redirectUri环境regex协作平台A/知识库RegexRegisteredServiceserviceId正则、logoutTypeBACK_CHANNELJSON按环境拆分dev/test/pre/prodevaluationOrder控制匹配优先级。代码层面的几个扩展点后面几篇会对着源码改这里先把关键类串起来方便对照时序图看。base-oauth2主要扩展两个点CustomJdbcAuthenticationHelper按customFields[loginType]分流邮箱、工号、钉钉扫码邮箱和扫码登录时调base-umd解析身份ExternalLogoutNotifier对应源码里DefaultLogoutManager的扩展在SLO时POSTservice-umd写登出Redis回调失败只打warn不阻断Back-Channel SLO。service-gateway侧GatewaySecurityConfig配OAuth2 Login链OAuth2AuthenticationSuccessHandler在OAuth2成功后调service-umd换JWT并302回前端JwtAuthGatewayFilterFactory做Bearer校验对issuerumd-sso的token额外查Redis登出时间和JWT黑名单。service-umd侧AdminUserController暴露token和logout接口JwtTokenProvider.createAdminToken写入用户claims和issuer。base-umd侧UserQueryController提供邮箱和钉钉扫码查询。设计中的几个关键取舍为什么CAS和OAuth2并存而不是二选一第02篇PRD里两类系统的凭证形态就不一样管理后台要JWT第三方Web应用要Session。OAuth2授权码适合SPA走网关CAS ST适合已有CAS Client的Jira/Wiki。共用同一个base-oauth2只是RegisteredService类型不同管理后台登记OAuthRegisteredService协作平台和知识库登记RegexRegisteredService。运维上多维护一种协议配置但应用侧改造成本低很多。业务JWT为什么独立于CAS管理CAS的OAuth2模块确实可以直接为Client签发JWT Access TokenRegisteredService里配jwtAccessToken: true即可。但我们管理后台需要的JWT带有业务claimsuserId、工号、邮箱还要跟Legacy登录、主动登出、SSO全局登出共用同一套黑名单和issuer约定。这些逻辑已经在service-umd里跑通了再塞进CAS Overlay认证中心和业务权限会强耦合后续改密钥、改claims、改登出策略都要动CAS发布节奏。拆出来以后CAS只管「登录成功」JWT只管「后续API访问」边界清楚。为什么网关不直连用户库service-gateway的pom里没有JDBC/MyBatis依赖。鉴权靠JWT签名校验加Redis登出标记角色和API权限从Redis缓存读不每次查库。认证入口应该保持轻量只负责身份验证和路由不应该逐渐变成「大杂烩」既做OAuth2 Client又管用户表又管权限SQL。用户数据的写操作集中在base-umd和service-umd网关只通过HTTP调token接口不碰数据库。为什么SLO回调失败不阻断base-oauth2调service-umd登出接口超时或失败只记warnBack-Channel SLO继续通知第三方应用销毁Session。如果因为service-umd短暂不可用就把SLO整段掐掉Jira/Wiki的本地Session可能清不掉用户以为已经退出第三方系统却还能访问。JWT侧的登出标记可以稍后补上Session侧必须优先保证CAS标准SLO走完。为什么Redis故障采用fail-openservice-gateway查JWT黑名单和SSO登出时间时Redis异常会记warn并当作「未登出」处理请求继续放行。限流模块也是类似取向Redis不可用时优先保证流量可用而不是因为缓存故障把整个认证链路打死。这是有意的取舍生产环境里Redis短暂抖动比「全员无法访问后台」代价小。当然fail-open意味着Redis故障期间登出拦截会失效必须配监控告警不能裸奔。非功能性设计高可用与水平扩展base-oauth2的TGT/ST存在RedisApereo CAS Redis Ticket Registry多实例共享同一票据存储前面挂负载均衡就行不需要Sticky Session。service-gateway鉴权在Filter链完成会话状态由JWT和Redis承载本身无状态下游路由通过服务发现如lb://service-umd分担流量。TGT默认7天无操作过期time-to-kill-in-seconds: 604800OAuth access token按接入方JSON配置TTLJWT另有主动登出与SSO全局登出失效机制。网关侧配Hystrix隔离和fallbackFeign调base-umd失败走FallbackFactory。base-oauth2调service-umd登出超时仅告警不阻断Back-Channel SLO。安全防护登录口有多层防护base-oauth2侧JDBC登录节流查COM_AUDIT_TRAIL按IP用户名统计失败频率和图形验证码service-umd侧Redis错误计数密码连错5次锁5分钟验证码连错3次锁1分钟service-gateway侧API令牌桶限流MethodPath规则Redis Lua计数。单层都不够几层叠在一起才扛得住撞库。验证码白名单captcha-ignores仅限非生产测试账号。新用户统一用bcrypt存密码历史账号因为迁移成本暂时通过MultiAlgorithmPasswordEncoder兼容md5用户下一次修改密码时再升级。密码策略要求8到16位复杂度180天未改密强制改密。JWT失效靠JWT黑名单、用户级登出时间和SSO全局登出时间三层。传输层Nginx TLS终结用X-Forwarded-*还原客户端IP。第三方应用的Session Cookie设HttpOnly降低XSS窃取Session风险。网关还有ValidateAccess按角色/API权限做二次拦截。不同OAuth2客户端可以通过JSON配置独立的JWT签名密钥。异常降级认证失败时账号/密码错误统一文案验证码、禁用、节流blocked各有独立提示。base-umd解析失败则中断认证链不拿空用户名去走JDBC认证。全局登出时service-umd回调失败仅记warnBack-Channel SLO继续。管理端JWT缺失、过期或已登出返回401issuerumd-sso的token额外校验Redis登出时间戳与JWT黑名单。限流与登出查询在Redis异常时默认放行须配监控告警。MQ消费与事件监听失败记日志/trace不影响主登录链路。扩展性新系统接入新增services/*.json不改Java代码。新登录方式CAS登录表单通过customFields[loginType]区分邮箱/工号/钉钉扫码在CustomJdbcAuthenticationHelper加分支即可。节流阈值、钉钉appId、验证码白名单等经配置中心RefreshScope在线调整。权限二级缓存网关Redis共享本地EhCachebase-umd权限变更经Fanout MQ刷新各节点。可观测性base-oauth2写COM_AUDIT_TRAIL审计表和独立cas_audit.logservice-gateway审查日志异步投递MQ响应头回写X-trace-idservice-umd用umd_sys_log记操作日志对齐traceId。风险矩阵风险对策OAuth2与CAS双协议配置漂移JSON服务注册纳入配置评审登录口撞库攻击JDBC节流验证码Redis计数API限流多层协同单层不足以覆盖全局登出漏拦截issuerumd-sso强制SSO Redis校验Redis故障fail-open监控告警优先可用性非fail-close第三方改造遗漏PRD清单第07篇Checklist密码双轨迁移bcrypt/md5兼容180天改密小结四个服务、三条链路、一套Redis Key约定就是这篇要定下来的东西。后面第04篇从base-oauth2的认证Handler入手把多方式登录和JDBC认证先跑通第06篇接service-gateway的OAuth2第07篇接CAS Client第08篇把单点登出闭环。