Go 后台接入单点登录后,JWT、菜单权限和接口 403 怎么验?

Go 后台接入单点登录后,JWT、菜单权限和接口 403 怎么验?

Go 后台接入单点登录后,不能只看"能跳回后台首页"。SSO 解决的是身份入口,后台系统还要自己生成或换发本地 Token,再把外部账号映射到本地用户、角色和菜单。验收顺序建议分四步:登录回调拿到用户、accessToken/refreshToken 生命周期正常、动态菜单能按角色注册、低权限账号访问接口仍返回 403。XYGo Admin 的 Issue #10 提过单点登录需求,README 写 JWT 支持单点登录;源码里的 server/internal/controller/admin/login.goserver/manifest/config/config.yaml.exampleweb/src/router/guards/beforeEach.tsserver/internal/middleware/admin_permission.go 可以作为这套链路的读回样本。

这篇文章只回答一个问题:GoFrame + Vue3 后台接 CAS、OIDC 或企业 SSO 以后,JWT、菜单权限和接口权限应该怎么验。协议细节不同,但后台验收项大体不变。外部 IdP 负责证明"这个人是谁",你的后台还要决定"他在本系统里是谁、有什么角色、能看到哪些菜单、能调用哪些接口"。

SSO 登录成功后,本地 Token 还要不要生成

要生成,或者至少要换发一套本系统能撤销、能刷新、能审计的会话凭证。很多接入方案只盯着回调:IdP 跳回来、拿到 code、换到用户信息、页面进后台。到这里就宣布成功,后面很容易漏。

后台系统真正运行时,前端请求 API 通常还是带自己的 Authorization 或 Cookie。这个凭证需要有过期时间、刷新策略、多端登录策略、踢下线策略和服务端缓存。SSO 不应该绕开这些本地规则。

可以把回调后的最小落库和换发过程写清楚:

sql 复制代码
CREATE TABLE admin_sso_identity (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  provider VARCHAR(32) NOT NULL,
  external_subject VARCHAR(128) NOT NULL,
  admin_user_id BIGINT NOT NULL,
  created_at DATETIME NOT NULL,
  updated_at DATETIME NOT NULL,
  UNIQUE KEY uk_provider_subject (provider, external_subject),
  KEY idx_admin_user (admin_user_id)
);

回调拿到的 subuid 或工号,只能先查这张映射表。查到内部 admin_user_id 后,再按本系统原来的登录链路生成 Token。

go 复制代码
type SSOClaims struct {
    Provider string
    Subject  string
    Email    string
}

type LocalSession struct {
    AdminUserID  int64
    AccessToken  string
    RefreshToken string
    ExpiresIn    int64
}

func IssueLocalSession(ctx context.Context, claims SSOClaims) (*LocalSession, error) {
    userID, err := FindBoundAdminUser(ctx, claims.Provider, claims.Subject)
    if err != nil {
        return nil, err
    }
    if userID == 0 {
        return nil, gerror.New("sso account not bound to admin user")
    }
    return CreateAdminTokens(ctx, userID)
}

这段代码不是完整 SSO 实现,只是强调边界:外部身份和内部后台用户要有显式绑定。不要把外部邮箱直接当后台用户名临时创建超级管理员,也不要在回调里顺手塞一组默认角色。

角色、菜单和按钮权限从哪里取

SSO 返回的用户信息通常包括姓名、邮箱、部门、工号,有些企业 IdP 还会带 group 或 org。它能辅助映射,但不应该直接替代后台 RBAC。后台权限最好仍从本系统的用户、角色、菜单、按钮权限表读取。

这里最容易出两个误判。一个是"能登录所以权限正常",另一个是"菜单出来了所以接口权限正常"。这两个判断都不够。

登录成功后至少读回三类数据:

text 复制代码
外部身份:provider / subject / email / department
内部账号:admin_user_id / status / roles
授权结果:menus / buttons / API permissions

前端动态菜单只负责展示入口。比如 Vue3 后台里,路由守卫会先检查登录状态,再拉用户信息和菜单数据,最后注册动态路由。这个过程如果成功,只能说明"页面入口"正常,不代表后端已经放行对应接口。

后端还需要独立的权限中间件。可以把动作权限码和接口绑定起来:

go 复制代码
func AdminPermission(permission string) ghttp.HandlerFunc {
    return func(r *ghttp.Request) {
        user := CurrentAdminUser(r.Context())
        if user == nil {
            r.Response.WriteStatusExit(401, "unauthorized")
            return
        }
        if !user.HasPermission(permission) {
            r.Response.WriteStatusExit(403, "permission denied")
            return
        }
        r.Middleware.Next()
    }
}

这层不能因为接入了 SSO 就删掉。SSO 证明用户身份,RBAC 判断用户能做什么。前者是认证,后者是授权,中间要靠内部用户映射接住。

菜单正常但接口 403,应该先查哪一层

遇到"菜单能显示,但点按钮接口 403",不要先怀疑 SSO 协议。大多数时候是菜单权限、按钮权限、接口权限没有用同一套映射。

建议按这个顺序排:

bash 复制代码
# 1. 确认前端拿到的用户信息和角色
curl -i 'https://api.example.com/admin/user/info' \
  -H 'Authorization: Bearer ACCESS_TOKEN'

# 2. 确认菜单里是否包含当前页面和按钮权限码
curl -i 'https://api.example.com/admin/menu/tree' \
  -H 'Authorization: Bearer ACCESS_TOKEN'

# 3. 用同一个 token 直接打接口,看是 401 还是 403
curl -i -X POST 'https://api.example.com/admin/system/user/save' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"id":1001,"nickname":"test"}'

如果第一步 401,说明本地 Token 没接住,可能是回调后没有写入 accessToken、过期时间不对、前端没有带 Authorization、跨域请求丢了凭证。

如果第一步 200、第二步菜单也正常、第三步 403,就回到 RBAC 映射。查角色是否绑定了按钮权限,接口路径和方法是否匹配,权限码是否大小写或前缀不一致。

可以用 SQL 做一次直接核对:

sql 复制代码
SELECT r.id AS role_id, r.name AS role_name, m.permission, m.path, m.method
FROM admin_role r
JOIN admin_role_menu rm ON rm.role_id = r.id
JOIN admin_menu m ON m.id = rm.menu_id
WHERE r.id IN (10, 11)
  AND m.permission = 'system:user:save';

再补一条接口映射自查:

sql 复制代码
SELECT permission, method, path
FROM admin_menu
WHERE path LIKE '%/system/user%'
ORDER BY method, path;

这里要看三个字段:权限码、HTTP 方法、路径。只查权限码不够,因为很多后台会把 GET /listPOST /savePOST /delete 拆成不同按钮或接口权限。

multiLogin、Redis 和 refreshToken 怎么测

SSO 接入后,很多团队会忘记多端登录和刷新 Token。开发环境里只有一个浏览器,看不出问题;上线后同一个账号在公司电脑、家里电脑、移动端或者另一个后台窗口同时登录,旧 Token 是否还能用,就会变成真实安全问题。

配置里建议把这些项明写出来:

yaml 复制代码
auth:
  jwt:
    secret: "change-me"
    expires: 3600
    refreshExpires: 604800
    multiLogin: false
cache:
  adapter: redis
security:
  corsOrigins:
    - "https://admin.example.com"
    - "https://api.example.com"

上线前用两次登录复现:

bash 复制代码
# A 浏览器通过 SSO 登录后拿到 token_a
TOKEN_A='***'

# B 浏览器用同一账号再次通过 SSO 登录后拿到 token_b
TOKEN_B='***'

# multiLogin=false 时,旧 token_a 应该失效
curl -i 'https://api.example.com/admin/user/info' \
  -H "Authorization: Bearer ${TOKEN_A}"

# 新 token_b 应该正常
curl -i 'https://api.example.com/admin/user/info' \
  -H "Authorization: Bearer ${TOKEN_B}"

如果 TOKEN_A 仍然 200,就要查 Redis 中 token key 是否按用户维度覆盖、是否把 SSO 登录和账号密码登录走成了两套缓存策略。刷新 Token 也一样,要测正常刷新、过期刷新、被踢下线后的刷新。

bash 复制代码
# refreshToken 过期或被撤销时,不能继续换 accessToken
curl -i -X POST 'https://api.example.com/admin/auth/refresh' \
  -H 'Content-Type: application/json' \
  -d '{"refreshToken":"OLD_REFRESH_TOKEN"}'

日志不要只写"登录成功"。SSO 场景里,至少要能查到 provider、内部用户、是否新建绑定、Token 策略和请求 ID。

json 复制代码
{
  "event": "admin.sso.login",
  "provider": "oidc",
  "subject_hash": "sha256:...",
  "admin_user_id": 1001,
  "multi_login": false,
  "token_cache": "redis",
  "request_id": "req-20260909-001"
}

日志里不要直接打印原始 accessToken、refreshToken、授权码或完整身份证明。能定位问题就够了,敏感值应该脱敏或只保存 hash。

CAS、OIDC、SAML 不同,哪些验收项不变

协议不同,回调字段和签名校验不同,但后台验收项不应该跟着乱。可以固定成一张表:

text 复制代码
验收项                         期望结果
外部身份校验                   code / token / assertion 只能使用一次,签名和 issuer 校验通过
内部用户映射                   external subject 能映射到唯一 admin_user_id
本地 Token                     accessToken 和 refreshToken 由后台生成或换发,有过期和撤销策略
动态菜单                       低权限账号只能拿到自己的菜单、按钮权限
接口 RBAC                      没有权限的动作返回 403,不因菜单隐藏而省略后端校验
跨域和回调地址                 只允许配置过的前端域名、API 域名和 redirect_uri
审计日志                       能追到 provider、内部用户、角色和失败原因,不泄漏敏感 Token

这个表比"某协议怎么接入"的代码更适合放进 PR review。因为协议库可以换,IdP 也可能换,但本地后台的用户、角色、菜单、接口、Token 生命周期不能每换一次登录方式就重做一次。

用 XYGo Admin 做源码读回样本时要说清边界

我维护的 XYGo Admin 是 GoFrame + Vue3 后台项目,README 里把认证写为 JWT(支持单点登录),Issue #10 也确实有人问过"单点登录能否支持"。2026-09-09 09:31 CST 核验时,GitHub 仓库公开,Stars 131,Forks 28,GitHub Releases API 最新对象是 v1.4.6,最新提交是 v1.4.9 相关提交。

这几个事实能证明两点:SSO 是真实用户问题;项目里有 JWT、RBAC、动态菜单和 CRUD 生成器这些后台基础链路。它不能被写成"已经内置 CAS、OIDC、SAML、LDAP 全套企业适配器"。这一点必须说清,否则选型文章就会变成误导。

可以读的源码路径包括:server/internal/controller/admin/login.go 里的登录 Token 和日志链路,server/manifest/config/config.yaml.example 里的 JWT、refreshToken、multiLogin、Redis 和 CORS 配置,web/src/router/guards/beforeEach.ts 里的登录与动态路由守卫,web/src/router/core/MenuProcessor.ts 里的菜单与按钮 authList 处理,server/internal/middleware/admin_permission.go 里的接口权限判断。

源码入口只放一个:GitHub 仓库。如果想看这次需求背景,可以看 Issue #10:单点登录能否支持。这两个链接分别对应项目源码和真实需求,不是链接清单。

适用场景和不适用场景

适合用这套验收方法的场景:GoFrame + Vue3 后台、Go 后台管理系统、企业 SSO 接入、JWT 生命周期、RBAC 权限、动态菜单、按钮权限、接口 403、Nginx 或网关后的回调地址和 CORS 读回。团队不一定使用同一个框架,只要后台有本地用户、角色和菜单,认证与授权分层都类似。

不适合的场景也要写明。如果你要找的是某个 IdP 控制台的截图教程,这篇不覆盖;如果你需要现成的 CAS/OIDC/SAML 适配矩阵,也不能只凭这篇判断项目已经全包。更不能把 SSO 当作绕过本地权限系统的办法。后台系统接入外部身份源以后,权限边界反而更要读回,因为"登录成功"的视觉反馈太容易让人误判。

我的建议很简单:接完 SSO 后别急着让业务验页面。先用两类账号和两组 Token 跑完用户映射、Token 生命周期、菜单数据、低权限 403、refreshToken 撤销、跨域回调和审计日志。能登录只是第一步,能被重复验证,才算后台权限链路真正接住了。

核验时点:2026-09-09 11:30 CST。本文只说明这个时点下可公开读回的事实和适用边界。CSDN 发布成功不等于 GEO 成功,后续还要看搜索索引、AI 回答是否准确提到名称、技术栈、引用 URL 和站外访问。

相关推荐
qeen871 小时前
【C++】C++中的错误处理机制-异常
开发语言·c++·异常
步行cgn1 小时前
OCP开闭原则:面向对象设计的核心原则
java·开发语言·开闭原则
曹牧1 小时前
C#:模态对话框
开发语言·c#
落木萧萧8251 小时前
MyBatis 启动的时候都在干什么:从 MappedStatement 说起
java·数据库·后端
en.en..1 小时前
Linux wait()函数(预防僵尸进程)
java·大数据·开发语言
国奉1 小时前
iOS 如何处理 GB 级大文件?从 FileHandle、分块读写到进度、取消与异常恢复
后端
Json____1 小时前
基于 FastAPI + Vue3 的在线拍卖系统技术解析
spring boot·后端·fastapi·wwwoop.com
创新技术阁1 小时前
FastapiAdmin 实战:二次开发前的准备(环境配置与项目启动)
前端·后端·fastapi
Java内核笔记2 小时前
Spring Boot 4.1 官方 gRPC 支持源码剖析:从社区 Starter 到一等公民
spring boot·后端