AI 助手 06:Spring Boot 多租户系统如何设计登录态和权限上下文

基于 ai-study V1.5 已实现基线,拆解 JWT access token、refresh token 轮换、请求上下文与 RBAC 如何共同替代浏览器伪造的租户身份。

字数 954 阅读时长 ≈ 3 分钟 2026-7-28 2026-7-28
所属项目:AI-assistant
AI 助手 06:Spring Boot 多租户系统如何设计登录态和权限上下文

AI 应用做到会话、知识库或 Agent 时,最先不能相信的往往是浏览器传来的租户和角色。早期开发为了调试方便,可以给请求默认的 demo 身份;一旦进入多用户、多租户场景,这种方式必须退出主链路。否则用户只要改一个 Header,就可能读到不属于自己的会话或文档。

ai-study 的 V1.5 做的不是完整 IAM 平台,而是先建立可信登录态:Java 通过 JWT 解析用户、租户和角色,前端不再直接决定这些身份字段;Python 只能接收 Java 已认证后透传的上下文。

登录、令牌刷新、Java 请求上下文和 Python 调用的信任边界

把身份信息从浏览器手里收回来

当前匿名入口只保留健康检查、登录和刷新接口。用户登录成功后,Java 返回短期 access token;前端把它保存在 Pinia 的内存状态里,并在受保护请求中放入:

Authorization: Bearer <access token>

令牌内包含用户 ID、tenantId、角色和过期时间。RequestContextFilter 负责验签并构建 RequestContext,业务代码只从这里取得租户、用户和角色。会话、文档等查询因此可以在服务层固定带上 tenant_id + user_id 条件,而不是接受前端传来的范围。

这是一条很重要的约束:前端能表达“我想访问什么”,但不能表达“我是谁”。当前 Java 会忽略浏览器传入的 x-tenant-idx-user-id 和角色 Header;这些值只有通过签名令牌才会进入业务上下文。

为什么 access token 和 refresh token 要拆开

access token 默认有效 15 分钟,目的是让被盗取后的有效窗口可控;它不落库,页面刷新后会丢失。为了避免用户频繁登录,服务端再发一个 7 天有效的 refresh token,放入 HttpOnlySameSite=Lax、路径为 /api/auth 的 Cookie。

refresh token 不保存明文。代码生成 32 字节随机值,只把 SHA-256 哈希写入 PostgreSQL。每次 /api/auth/refresh 都撤销旧记录、签发新 token,因此旧 Cookie 即使被重放,也无法持续刷新会话。登录、刷新和退出同时写入安全审计记录。

这套设计比“把长效 JWT 放 localStorage”多了服务端撤销能力,也减少了脚本直接读取 refresh token 的风险。它仍不是万能方案:本地 HTTP 环境暂未启用 Secure Cookie,生产 HTTPS 部署时必须打开;access token 也尚未有 jti denylist,只能依赖其 15 分钟自然过期。

RBAC 不应散落在 Controller 里

认证解决“是谁”,授权解决“能做什么”。当前 Chat 和文档接口必须登录,/api/admin/** 和运行日志接口额外要求 ADMIN。授权判断由统一的上下文和 AdminAccessService 完成,而不是在每个页面或 SQL 中各写一段字符串判断。

这样做的收益在 V2 很明显:文档上传、列表和删除均使用当前 tenantId + userId;下一步做知识库检索时,只需沿用同一份身份上下文,并把它变成检索过滤条件。认证不是 RAG 的附属功能,而是权限检索能成立的前提。

当前验收与未完成项

可通过登录、刷新、退出与 /api/auth/me 验证基本闭环;未登录访问 Chat、租户后台或运行日志应得到 401,非管理员访问管理接口应得到 403。页面刷新时,前端用 refresh Cookie 恢复内存里的 access token,路由守卫再决定是否跳转登录页。

这版还没有登录限流、账户锁定、验证码或 MFA,也没有企业 SSO/LDAP/OAuth。它适合当前项目把“可信主体”接进 AI 主流程,不等同于已经具备完整企业身份平台。后续安全加固会继续补齐登录防护、HTTPS、Cookie 安全属性和更细的运维角色范围。