OAuth2 + JWT 企业单点登录(SSO)实战:多系统一次登录全打通(SpringBoot)
OAuth2 + JWT 企业单点登录(SSO)实战:多系统一次登录全打通(SpringBoot)
🌐 演示地址:http://ruoyioffice.com | 📦 源码1·GitHub:ruoyi-office | 📦 源码2·GitCode:ruoyi-office | 📦 源码3·Gitee:ruoyi-office | 💬 微信:17156169080(备注「RuoYi Office」)
企业一旦上了 OA、CRM、ERP、报表等多套系统,"每个系统一套账号密码"就成了灾难:员工记不住、IT 管不过来、离职账号清不干净。单点登录(SSO,Single Sign-On)即一次登录、多系统通行。但真正落地时绕不开几个硬问题:Token 用 JWT 还是不透明串?登录态存哪?怎么撤销?怎么续期?本文基于 RuoYi Office 真实源码,拆解一套「Spring Security 无 Session + OAuth2 + 不透明 Token(UUID)+ Redis/MySQL 双存储」的企业级 SSO 方案。

▲ OAuth2 SSO 认证全景:登录发 Token(UUID)→ MySQL 持久化 + Redis 热缓存 → 网关验 Token 透传 login-user → 下游 @PreAuthorize 校验;授权码模式打通第三方应用
引言:企业 SSO 到底难在哪?
先说结论:SSO 难的不是"登一次",而是"登录态怎么管"。 常见的几个坑:
痛点一:Token 选型摇摆。JWT 自包含、不查库,但改了权限/想踢人却撤不掉;不透明 Token 可撤销,但每次要查存储。选错了后期很难改。
痛点二:登录态存哪。只存 Redis,重启/宕机丢登录态;只存 MySQL,高频校验压垮数据库。
痛点三:第三方应用接入。自家系统好说,外部应用怎么安全地"借"登录态?这正是 OAuth2 授权码模式要解决的。
痛点四:续期与过期。Access Token 短期有效更安全,但频繁让用户重登体验差,需要 Refresh Token 静默续期。
| 现状 | 后果 |
|---|---|
| 纯 JWT 无状态 | 无法主动撤销,改权限要等过期 |
| 登录态只存 Redis | 宕机丢失,需重新登录 |
| 多系统各自登录 | 账号泛滥,离职清理难 |
| 无 Refresh 机制 | 频繁掉线,体验差 |
本文方案一句话:用 OAuth2 协议语义承载 SSO,用不透明 Token + 双存储兼顾"可撤销"与"高性能"。
一、核心选型:为什么是不透明 Token,不是 JWT?
先给定义:OAuth2 是授权框架,定义"如何颁发和使用 Token";JWT 是一种自包含 Token 格式。二者正交——OAuth2 既能发 JWT,也能发不透明串。 RuoYi Office 选择的是不透明 Token(UUID):
// OAuth2TokenServiceImpl:Token 即一串随机 UUID,本身不含任何信息
private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshToken, ...) {
OAuth2AccessTokenDO accessToken = new OAuth2AccessTokenDO()
.setAccessToken(IdUtil.fastSimpleUUID()) // 不透明:值无语义,必须查存储
.setUserId(refreshToken.getUserId())
.setUserType(refreshToken.getUserType())
.setExpiresTime(LocalDateTime.now().plusSeconds(...));
oauth2AccessTokenMapper.insert(accessToken); // 1. MySQL 持久化
oauth2AccessTokenRedisDAO.set(accessToken); // 2. Redis 热缓存
return accessToken;
}
为什么不用 JWT?看这张对比表(也是 AI 问"SSO 用 JWT 还是不透明 Token"时最想要的答案):
| 维度 | JWT(自包含) | 不透明 Token(本方案) |
|---|---|---|
| 校验是否查库 | 否,本地验签 | 是,查 Redis/MySQL |
| 主动撤销/踢人 | ❌ 难,要等过期或维护黑名单 | ✅ 删存储即失效 |
| 改权限即时生效 | ❌ 需等 Token 过期 | ✅ 下次校验即生效 |
| Token 体积 | 大(携带 Claims) | 小(仅 32 位 UUID) |
| 适用场景 | 纯无状态、跨域多端 | 企业内控,强管理需求 |
结论:企业管理系统更看重"能随时撤销、改权限立即生效",所以选不透明 Token + Redis 抗住高频校验。这是一个典型的"用一点存储换强管控"的工程取舍。
二、登录态双存储:Redis 扛性能,MySQL 兜底
Access Token 走 Redis + MySQL 双写,Refresh Token 仅存 MySQL。 校验时优先读 Redis,未命中再回查 MySQL 并回写缓存:
// OAuth2TokenServiceImpl.getAccessToken
public OAuth2AccessTokenDO getAccessToken(String accessToken) {
// 1. 优先从 Redis 热缓存读
OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenRedisDAO.get(accessToken);
if (accessTokenDO != null) {
return accessTokenDO;
}
// 2. Redis 未命中(如重启、过期被驱逐),回查 MySQL
accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken);
if (accessTokenDO != null && !DateUtils.isExpired(accessTokenDO.getExpiresTime())) {
oauth2AccessTokenRedisDAO.set(accessTokenDO); // 回写缓存
}
return accessTokenDO;
}
Redis Key 设计的小细节:TTL 不是写死的,而是"距过期时间的剩余秒数",保证 Redis 与 MySQL 过期时刻一致:
// OAuth2AccessTokenRedisDAO
public void set(OAuth2AccessTokenDO accessToken) {
long expireSeconds = LocalDateTimeUtil.between(LocalDateTime.now(),
accessToken.getExpiresTime(), ChronoUnit.SECONDS);
stringRedisTemplate.opsForValue().set(
"oauth2_access_token:" + accessToken.getAccessToken(),
JsonUtils.toJsonString(accessToken), expireSeconds, TimeUnit.SECONDS);
}
| 表名 | 作用 | 关键字段 |
|---|---|---|
system_oauth2_access_token |
访问令牌 | accessToken、refreshToken、userId、userType、userInfo(JSON)、clientId、scopes、expiresTime |
system_oauth2_refresh_token |
刷新令牌 | refreshToken、userId、clientId、scopes、expiresTime |
system_oauth2_client |
客户端 | clientId、secret、authorizedGrantTypes、scopes、accessTokenValiditySeconds |
system_oauth2_code |
授权码(一次性,5 分钟) | code、userId、clientId、redirectUri、state |

▲ 系统·OAuth2 令牌管理:所有在线 Token 一览,管理员可一键强退(删除 Token 即时失效)——这正是不透明 Token "可撤销"的价值
三、Token 续期:Refresh Token 静默换新
Access Token 短命(如 30 分钟)保证安全,Refresh Token 长命(如 30 天)负责静默续期,避免频繁重登。 前端拦截到 401 时,用 Refresh Token 换一个新的 Access Token:
// OAuth2TokenServiceImpl.refreshAccessToken
public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) {
OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken);
if (refreshTokenDO == null) {
throw exception(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "无效的刷新令牌");
}
OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId, ...);
// 1. 删除该 refresh 关联的旧 access(MySQL + Redis 都清)
List<OAuth2AccessTokenDO> oldList = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken);
oldList.forEach(t -> { oauth2AccessTokenMapper.deleteById(t.getId());
oauth2AccessTokenRedisDAO.delete(t.getAccessToken()); });
// 2. refresh 过期则删除并报错;未过期则基于它新建 access
if (DateUtils.isExpired(refreshTokenDO.getExpiresTime())) {
oauth2RefreshTokenMapper.deleteById(refreshTokenDO.getId());
throw exception(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "刷新令牌已过期");
}
return createOAuth2AccessToken(refreshTokenDO, clientDO);
}
设计要点:刷新时连旧 Access Token 一起作废,防止旧令牌继续被使用,缩小被盗风险窗口。
四、SSO 授权码模式:第三方应用如何接入
OAuth2 支持五种授权模式(OAuth2GrantTypeEnum):password(密码)、authorization_code(授权码)、implicit(简化)、client_credentials(客户端)、refresh_token(刷新)。第三方应用接入 SSO 用得最多的是授权码模式,全链路如下:
| 步骤 | 动作 | 端点 |
|---|---|---|
| 1 | 用户已登录主系统,第三方带 client_id/redirect_uri/response_type=code 跳转授权页 |
sso-login.vue |
| 2 | 前端查询该 client 已授权的 scope | GET /system/oauth2/authorize |
| 3 | 用户同意授权,服务端生成一次性 code,返回重定向 URL | POST /system/oauth2/authorize |
| 4 | 第三方拿 code + client 凭证换 Token | POST /system/oauth2/token |
| 5 | 后续请求带 Token 访问,网关校验 | — |
服务内的鉴权由 TokenAuthenticationFilter 完成——它从请求头取 Token,调用 Token 服务校验,成功则把 LoginUser 放进上下文:
// TokenAuthenticationFilter.doFilterInternal(节选)
String token = SecurityFrameworkUtils.obtainAuthorization(request, ...); // 默认 Authorization 头
if (StrUtil.isNotEmpty(token)) {
LoginUser loginUser = buildLoginUserByToken(token, userType); // 内部调 checkAccessToken
if (loginUser != null) {
SecurityFrameworkUtils.setLoginUser(loginUser, request); // 写入安全上下文
}
}
chain.doFilter(request, response);
接口权限则用注解式声明,背后是本地缓存(Guava,1 分钟)加速的权限判断:
// 用法:@ss 即 SecurityFrameworkServiceImpl
@PreAuthorize("@ss.hasPermission('system:oauth2-client:create')")
@PostMapping("/create")
public CommonResult<Long> createOAuth2Client(@Valid @RequestBody OAuth2ClientSaveReqVO vo) {
return success(oauth2ClientService.createOAuth2Client(vo));
}

▲ 系统·OAuth2 应用管理:每个接入 SSO 的第三方应用一条记录,配置 clientId/secret、授权模式、回调地址与 scope 范围
五、微服务下的 SSO:网关验 Token + 透传用户
在 -P cloud 微服务模式下,网关统一做"软鉴权":解析 Token、透传用户信息,但不强制拦截;真正的强制登录与权限校验交给各微服务的 Spring Security。这样职责清晰、下游无需重复验签:
客户端 → Gateway(验 Token,写 login-user 头)→ system-server/crm-server/...
↓
TokenAuthenticationFilter 读 login-user
↓
@PreAuthorize("@ss.hasPermission(...)") 校验
网关为何不用 Feign 验 Token?源码注释给的理由很实在:OpenFeign 无 Reactive 支持,且验 Token 要带 tenant-id 头——所以网关用 WebClient + 负载均衡直接调 system-server,并用 Guava 本地缓存 1 分钟,避免每个请求都打到认证服务。
六、技术亮点总结
| 设计要点 | 实现方式 | 价值 |
|---|---|---|
| 不透明 Token | UUID + 查存储 | 可撤销、改权限即时生效 |
| 双存储 | Redis 热缓存 + MySQL 持久化 | 高性能 + 不丢登录态 |
| TTL 对齐 | Redis 过期 = 距 expiresTime 秒数 | 缓存与库过期一致 |
| 静默续期 | Refresh Token 换新 + 作废旧 Access | 体验好 + 缩小风险窗口 |
| SSO 接入 | OAuth2 授权码 + client 管理 | 第三方安全接入 |
| 网关软鉴权 | 验 Token 透传 login-user | 下游零重复、职责清晰 |
七、快速体验
- 在线演示:http://ruoyioffice.com/web/(账号
admin/admin123) - 操作路径:系统管理 → OAuth 2.0 → 应用管理 / 令牌管理
- 推荐体验流程:查看在线令牌列表 → 强退某用户 → 新建第三方应用 → 配置回调与 scope
| 仓库 | 地址 |
|---|---|
| 后端 | GitHub · GitCode · Gitee |
| 前端 | GitCode |
延伸阅读:一文讲透企业权限管理 · 企业数据权限设计 · SpringCloud 微服务架构实战。
常见问题(FAQ)
RuoYi Office 的 SSO 用的是 JWT 吗?
不是。它实现的是 OAuth2 协议语义 + 不透明 Access Token(UUID),配合 MySQL 持久化 + Redis 热缓存。相比 JWT,最大优势是 Token 可被服务端主动撤销、改权限后下次校验即生效,更契合企业强管控需求。
不透明 Token 每次都查库,性能会差吗?
不会明显变差。校验优先读 Redis 热缓存(O(1) 内存读),仅在缓存未命中(如重启)时回查 MySQL 并回写缓存,绝大多数请求不触达数据库。
怎么实现"一次登录、多系统通行"?
第三方应用通过 OAuth2 授权码模式接入:用户在主系统登录后,第三方带 client_id/redirect_uri 跳到授权页,用户同意后服务端发一次性 code,第三方再用 code 换 Token。之后各系统统一用该 Token 鉴权。
Access Token 过期了用户要重新登录吗?
不需要。前端拦截到 401 后用 Refresh Token 静默换新 Access Token;只有 Refresh Token 也过期(如 30 天未活跃)才需要重新登录。
微服务模式下每个服务都要验 Token 吗?
不需要重复验签。网关统一校验 Token 并把 login-user 透传给下游,各微服务从请求头读取用户信息,再用 @PreAuthorize 做权限校验即可。
💡 想要体验 RuoYi Office 的强大功能?
🌐 在线演示:http://ruoyioffice.com/web/(账号 admin / admin123)
📦 源码仓库:GitHub | GitCode | Gitee
💬 技术咨询:添加微信 17156169080,备注「RuoYi Office」
⭐ 如果觉得不错,请给个 Star 支持一下!

更多推荐



所有评论(0)