多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

SpringBoot3+OAuth2 企业 SSO:不透明 Token、Redis 热缓存与授权码一次性消费

SpringBoot3+OAuth2 企业 SSO:不透明 Token、Redis 热缓存与授权码一次性消费 SpringBoot3OAuth2 企业 SSO不透明 Token、Redis 热缓存与授权码一次性消费文档地址https://ruoyioffice.com源码1·GitHubhttps://github.com/yuqing2026/ruoyi-office源码2·GitCodehttps://gitcode.com/zhouzhongyan/ruoyi-office源码3·Giteehttps://gitee.com/yqzy1688/ruoyi-office微信17156169080备注「RuoYi Office」OA、CRM、报表、大屏一旦拆成多套入口员工就会同时记四套密码离职账号也清不干净。单点登录要的不是“少输一次密码”而是登录态可颁发、可续期、可审计、可立刻作废。本文按 RuoYi Office 的 OAuth2 实现讲清为什么 Access Token 用 UUID 而不是 JWT以及授权码、Redis 热缓存、令牌管理页如何咬合。▲ 客户端契约 → 一次性 code → UUID 双令牌双存储 → 续期作废旧票 / 管理端踢人引言SSO 难的是“登录态怎么管”纯 JWT 很香网关不用查库水平扩展简单。企业场景却会立刻碰到现状后果Token 自包含、改了角色还有效权限变更要等过期安全窗口不可控只把登录态放 RedisRedis 抖动或重启全员掉线只把登录态放 MySQL每个接口验票都打库第三方 302 重定向Axios 前端接不住授权页卡死报表 / WebSocket 只能带一个 token不会 refresh 就频繁 401RuoYi Office 的选择是OAuth2 协议语义 不透明 TokenUUID MySQL 持久化 Redis 热路径。JWT 可以以后作为某种 client 的可选项但默认管理端登录态必须能踢人。一、先登记客户端再谈发 Token每个接入系统是一行 OAuth2 Client编号、密钥、回调 URI、授权模式、Access / Refresh 有效期、scope。这些是契约不是注释。▲ 效果创建客户端时同时登记授权类型、Access/Refresh 秒数、可重定向 URI 和授权范围避免把密钥写死在业务代码里常见模式在字典里就有authorization_code、password、refresh_token、client_credentials、implicit。管理后台自己登录多用密码模式跨系统 SSO 必须走授权码。validOAuthClientFromCache会校验client 存在且启用本次 grantType 在允许列表中redirectUri 命中白名单申请的 scope 是子集。任何一项失败都不应发码更不应发 Token。二、授权码五分钟、一次性、三角校验2.1 同意授权页前后端分离后浏览器不会像传统 Servlet 那样吃 302。前端先拉客户端信息再POST /system/oauth2/authorize后端返回最终跳转 URL 字符串由location.href完成。▲ 效果已登录用户进入 SSO 页勾选「访问你的个人信息」后点同意若该 client 配置了自动授权范围可跳过这屏自动授权和手动授权是两条路径auto_approvetrue检查历史同意记录或 client 自动 scope通不过就返回空 URL前端停在同意页auto_approvefalse用户点同意/拒绝写入批准记录后再发码。2.2 发码与消费授权码默认5 分钟生成同样用 UUIDpublicOAuth2CodeDOconsumeAuthorizationCode(Stringcode){OAuth2CodeDOcodeDOoauth2CodeMapper.selectByCode(code);if(codeDOnull){throwexception(OAUTH2_CODE_NOT_EXISTS);}if(DateUtils.isExpired(codeDO.getExpiresTime())){throwexception(OAUTH2_CODE_EXPIRE);}oauth2CodeMapper.deleteById(codeDO.getId());// 一次性returncodeDO;}换票时还要核对clientId、redirectUri、state。这三项有一项对不上就说明回调被劫持或客户端配错了必须拒绝。code 用过即删重放无效。三、不透明 Token可撤销比自包含更重要3.1 颁发Access 和 Refresh 都是IdUtil.fastSimpleUUID()有效期来自 client 配置演示环境常见 Access 1800 秒。Access 写入 MySQL 后立刻写入 RedisRefresh 以 MySQL 为准。privateOAuth2AccessTokenDOcreateOAuth2AccessToken(OAuth2RefreshTokenDOrefreshTokenDO,OAuth2ClientDOclientDO){OAuth2AccessTokenDOtokennewOAuth2AccessTokenDO().setAccessToken(generateAccessToken()).setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()).setUserInfo(buildUserInfo(refreshTokenDO.getUserId(),refreshTokenDO.getUserType())).setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes()).setRefreshToken(refreshTokenDO.getRefreshToken()).setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds()));oauth2AccessTokenMapper.insert(token);oauth2AccessTokenRedisDAO.set(token);returntoken;}userInfo在颁发时写入昵称和部门避免每个请求再打用户表。租户号优先取 Refresh 上的tenantId防止 ThreadLocal 被污染写成 null。3.2 验票Redis 未命中再回源publicOAuth2AccessTokenDOgetAccessToken(StringaccessToken){OAuth2AccessTokenDOtokenoauth2AccessTokenRedisDAO.get(accessToken);if(token!null){returntoken;}tokenoauth2AccessTokenMapper.selectByAccessToken(accessToken);if(tokennull){// 报表 / WebSocket 只能带一个串时允许用未过期的 Refresh 顶一下OAuth2RefreshTokenDOrefreshoauth2RefreshTokenMapper.selectByRefreshToken(accessToken);if(refresh!null!DateUtils.isExpired(refresh.getExpiresTime())){tokenconvertToAccessToken(refresh);}}if(token!null!DateUtils.isExpired(token.getExpiresTime())){oauth2AccessTokenRedisDAO.set(token);}returntoken;}这是刻意的兼容不是协议纯洁主义。积木报表、WebSocket URL 往往塞不下 refresh_token若死守“只能用 Access”业务会逼着把 Access 有效期拉到几天安全更差。Redis 写入时 TTL 剩余秒数并清掉 creator/updateTime 等审计字段避免缓存体膨胀。过期 key 自动消失与 MySQL 过期判断双保险。3.3 续期会作废旧 AccessrefreshAccessToken先按 refresh 查出所有 AccessMySQL 和 Redis 一起删再发新 Access。Refresh 过期则连 Refresh 行也删返回未授权。这样“续期”不会在库里堆出一串仍有效的旧票。踢人更直接令牌管理页删除一行调用removeAccessTokenAccess Refresh Redis 同步清理。按用户维度的强制下线则遍历该用户全部 Access 做同样的事。▲ 效果访问令牌是 32 位 UUID 而不是三段式 JWT删除即踢当前会话不必等 30 分钟自然过期过期清理是分批deleteLimit循环避免大表一次性锁死。Access 和 Refresh 各有清理入口留给定时任务而不是靠用户撞 401。四、授权模式怎么分工模式谁用风险点authorization_code第三方系统 SSOredirectUri 必须白名单code 必须一次性password管理端自身登录只给第一方第三方禁止用refresh_token所有需要静默续期的端必须校验 clientId 与原票一致client_credentials服务间调用用户 ID 为 0必须限制 scopeimplicit历史兼容前端直接拿 Token默认不建议新系统启用密码模式走authenticate(username, password)再createAccessToken。它不是 SSO 的主路径只是第一方登录的 OAuth2 外壳方便和第三方共用同一套 Token 存储。五、和 JWT 的边界不要做成宗教讨论维度不透明 UUID自包含 JWT踢人删库 删缓存立刻失效除非黑名单否则等过期改角色下次验票读库/缓存里的新信息旧 Token 仍带旧角色声明网关压力要查 Redis只验签Token 体积短声明一多就变长离线校验不行可以企业后台、审批、财务对“立刻踢人”的需求强于“网关零查询”。报表大屏若必须离线验签可以给那个 client 单独发 JWT不必把管理端默认登录态也改掉。六、前端对应关系页面作用/system/oauth2/oauth2/application登记 client 契约/system/oauth2/token审计当前票、踢人/auth/sso-login第三方授权同意页SSO 页会先尝试自动授权URL 上带了scope且历史已同意就直接跳转用户甚至看不到同意屏。这是体验优化前提是自动 scope 必须在 client 上显式配置不能默认全开。七、数据结构精简客户端表client_id、secret、redirect_uris、authorized_grant_types、scopes、access_token_validity_seconds、refresh_token_validity_seconds。Access 表access_token、refresh_token、user_id、user_type、client_id、scopes、expires_time、user_info、tenant_id。Refresh 表字段与 Access 类似有效期更长。授权码表code、redirect_uri、state、五分钟过期消费后物理删除。Redis key 按 Access 字符串格式化TTL 对齐剩余寿命。八、技术亮点设计要点实现方式价值不透明 TokenUUID不自包含可立刻撤销双存储MySQL 权威 Redis 热路径可审计、扛 QPSRedis TTL剩余秒数过期自然淘汰授权码一次性消费即删防重放三角校验client / redirect / state防回调劫持续期作废旧票按 refresh 删全部 Access不堆积有效会话特殊通道兼容未过期 Refresh 可当 Access 验报表 / WS 能用前端拿 URL不依赖 Axios 302分离架构可落地九、快速体验在线演示https://ruoyioffice.com/web/账号 admin / admin123推荐路径系统管理 → OAuth 2.0 → 应用管理打开新增表单看授权类型和回调 URI进入令牌管理确认 Access 是 UUID删除一条观察是否立即需要重新登录用带client_id、redirect_uri、response_typecode的地址打开 SSO 授权页同意后看回调 URL 上的code重复使用应失败。源码仓库GitHubhttps://github.com/yuqing2026/ruoyi-officeGitCodehttps://gitcode.com/zhouzhongyan/ruoyi-officeGiteehttps://gitee.com/yqzy1688/ruoyi-office常见问题FAQ为什么默认不用 JWT管理端要支持改权限立即生效、离职立即踢人。JWT 做不到这一点除非再维护黑名单那已经失去“无状态”的优点。Access 只有 30 分钟会不会总掉线前端用 Refresh 静默续期。续期会作废旧 Access所以同时只应保留当前这一张热票。密码模式给第三方用可以吗不可以。密码模式等于把账号密码交给对方。第三方只走授权码 回调白名单。Redis 挂了会不会全员登出不会立刻。验票会回源 MySQL 并回填 Redis。Redis 只是加速层权威在数据库。为什么授权接口返回 URL 而不是 302浏览器 Axios 默认不把跨域 302 当授权完成处理。返回字符串后由前端跳转行为可预期也方便带上已登录态 Cookie/Token。结语企业 SSO 的目标不是“少输一次密码”而是把登录态变成可治理的资源谁发的、发给哪个 client、何时过期、能否续、能否立刻作废。不透明 Token 加双存储看起来比 JWT 多一次查询却换来权限变更和人员离职时真正可用的控制面。想要体验 RuoYi Office 的强大功能在线演示https://ruoyioffice.com/web/账号 admin / admin123源码仓库GitHubhttps://github.com/yuqing2026/ruoyi-office GitCodehttps://gitcode.com/zhouzhongyan/ruoyi-office Giteehttps://gitee.com/yqzy1688/ruoyi-office技术咨询添加微信17156169080备注「RuoYi Office」⭐如果觉得不错请给个 Star 支持一下
返回列表