Strapi 认证体系深度解析:SessionManager、JWT 双令牌与会话轮换机制
2026/9/6 18:20:27 网站建设 项目流程

Strapi 认证体系深度解析:SessionManager、JWT 双令牌与会话轮换机制

【免费下载链接】strapi🚀 Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi

本文基于 Strapi 仓库的认证文档与核心源码,系统讲解 Strapi 中 Admin(后台)与 Content API(users-permissions 插件)两套认证体系的会话管理实现:从SessionManager的 origin 多租户模型、access token / refresh token 双令牌设计,到刷新令牌轮换(rotation)、空闲/绝对生命周期、设备维度吊销,以及密码变更时自动失效所有会话的安全策略。读完本文,你可以完整理解 Strapi 中一条 refresh token 从签发、轮换到吊销的完整生命周期,并能正确配置admin.auth.sessions.*plugin::users-permissions相关的认证参数。

一、整体架构:两条认证链路共用一个 SessionManager

Strapi 仓库内的认证分为两个来源(origin):

  • Admin(后台):管理员登录、注册、重置密码等,走admin.auth.*配置;
  • Content API:经由 users-permissions 插件(下文简称 UP),面向内容 API 的终端用户认证,走plugin::users-permissions.*配置。

两条链路在 v5 架构下共享同一套核心会话服务。Core 提供的SessionManager统一负责签发:

  • 短时效 access token(JWT,客户端以Authorization: Bearer <token>携带);
  • 长时效 refresh/session token(JWT,按来源(origin)以不同方式存储——Admin 端存 httpOnly cookie,Content API 端直接下发给客户端)。

其实现入口位于 session-manager.ts:createSessionManager默认绑定数据库 provider 并写入隐藏内容类型admin::session,返回一个"既可调用又可挂方法"的流式 API——strapi.sessionManager('admin')返回绑定adminorigin 的OriginSessionManager,同时暴露generateSessionIddefineOriginhasOrigin等全局方法。

// packages/core/core/src/services/session-manager.ts const createSessionManager = ({ db }: { db: Database }) => { const provider = createDatabaseProvider(db, 'admin::session'); const sessionManager = new SessionManager(provider); // Add callable functionality const fluentApi = (origin: string): OriginSessionManager => { if (!origin || typeof origin !== 'string') { throw new Error( 'SessionManager: Origin parameter is required and must be a non-empty string' ); } return new OriginSessionManager(sessionManager, origin); }; // ... 挂载 defineOrigin / hasOrigin / generateSessionId };

Origin 的注册时机

每个 origin 必须在 bootstrap 阶段完成配置注册。以 Admin 为例,admin/server/src/bootstrap.ts 中执行:

strapi.sessionManager.defineOrigin('admin', { jwtSecret: strapi.config.get('admin.auth.secret'), accessTokenLifespan: strapi.config.get('admin.auth.sessions.accessTokenLifespan', 30 * 60), maxRefreshTokenLifespan: strapi.config.get( 'admin.auth.sessions.maxRefreshTokenLifespan', legacyMaxRefreshFallback ), idleRefreshTokenLifespan: strapi.config.get( 'admin.auth.sessions.idleRefreshTokenLifespan', DEFAULT_IDLE_REFRESH_TOKEN_LIFESPAN ), maxSessionLifespan: strapi.config.get( 'admin.auth.sessions.maxSessionLifespan', legacyMaxSessionFallback ), idleSessionLifespan: strapi.config.get( 'admin.auth.sessions.idleSessionLifespan', DEFAULT_IDLE_SESSION_LIFESPAN ), algorithm: options?.algorithm, // Pass through all JWT options (includes privateKey, publicKey, and any other options) jwtOptions: options, });

UP 插件同理,在 users-permissions/server/src/bootstrap/index.js 中以sessionManager.defineOrigin('users-permissions', {...})注册自己的 origin。若对未注册的 origin 发起操作,getConfigForOrigin会直接抛出SessionManager: Origin '<origin>' is not defined错误(见 session-manager.ts),这保证了配置在启动期就暴露问题。

每个 origin 需要提供的配置项如下(bootstrap 时定义):

配置项含义
jwtSecret对称算法(HS256/HS384/HS512)的签名密钥
accessTokenLifespan(秒)access token 有效期
maxRefreshTokenLifespanidleRefreshTokenLifespan(秒)refresh 家族的绝对上限 / 空闲超时
maxSessionLifespanidleSessionLifespan(秒)session 家族(rememberMe=false)的绝对上限 / 空闲超时
algorithmJWT 算法,默认HS256(constants.ts 中的DEFAULT_ALGORITHM
jwtOptions透传给jsonwebtoken的其他选项(issuer、audience、subject、privateKey 等)

会话数据模型

各 origin 的会话记录统一落在隐藏内容类型admin::session中(底层表strapi_sessions),核心字段包括userIdsessionIddeviceIdoriginexpiresAtabsoluteExpiresAtstatustype,另有一个自由格式的metadata字段供各 origin 存放自定义数据(如设备名、登录时间),SessionManager 只负责原样存取、不做解释(SessionData 定义)。

关键字段的语义:

  • type: 'refresh' | 'session'——refresh是长期令牌家族(rememberMe),session是绑定浏览器会话的短家族;
  • status: 'active' | 'rotated' | 'revoked'——只有active状态的记录才有资格换取 access token(validateRefreshToken 校验);
  • childId——轮换链的父子指针,父记录被标记rotated并指向子记录,这是实现"一次有效"轮换的关键结构;
  • expiresAt是空闲到期时间,absoluteExpiresAt是整个令牌家族的绝对到期时间。

对外公开 API(按 origin)

OriginSessionManager暴露的方法(类型定义见 types/src/modules/session-manager.ts):

generateRefreshToken(userId, deviceId?, { type?: 'refresh' | 'session' }) rotateRefreshToken(refreshToken) generateAccessToken(refreshToken) validateAccessToken(token) validateRefreshToken(token) invalidateRefreshToken(userId, deviceId?) listSessions(userId) revokeSessionById(userId, sessionId) isSessionActive(sessionId)

主要实现文件:

  • Core 服务:session-manager.ts
  • 类型:types/src/modules/session-manager.ts

二、令牌生命周期与轮换机制(源码级解析)

1. 签发:generateRefreshToken

generateRefreshToken 的流程是:

  1. 按 token 类型选择空闲生命周期与绝对生命周期(refreshidleRefreshTokenLifespan/maxRefreshTokenLifespansessionidleSessionLifespan/maxSessionLifespan);
  2. 在数据库创建根记录childId: nullstatus: 'active'),sessionId由 16 字节随机数的 hex 生成(generateSessionId);
  3. 用记录的createdAt计算iat/exp,以noTimestamp: true方式签名 JWT,payload 为{ userId, sessionId, type: 'refresh', iat, exp }
  4. 返回{ token, sessionId, absoluteExpiresAt }

值得注意的细节:对称算法下签名使用config.jwtSecret;非对称算法(RS*/ES*/PS* 前缀)则要求jwtOptions.privateKey(签名)或jwtOptions.publicKey(验签)存在,否则直接抛错(getJwtKey)。另外,签名前会剔除expiresIn/privateKey/publicKey等与 payload、密钥选择冲突的选项,避免jsonwebtoken行为歧义。

2. 校验:validateRefreshToken 的四重防线

validateRefreshToken 依次检查:

  1. JWT 验签通过且payload.type === 'refresh'(拒绝拿 access token 冒充 refresh token);
  2. 数据库中存在对应sessionId的记录;
  3. expiresAt(空闲到期)与absoluteExpiresAt(家族绝对到期)均未过;
  4. 记录status仍为active,且userId与 payload 一致。

validateAccessToken则是纯签名校验(L399-L427),无数据库查询,因此适合放进每个请求的热路径。

3. 轮换:rotateRefreshToken 的"子令牌"模型

rotateRefreshToken 实现的是典型的 refresh token rotation,几个关键行为:

  • 重放检测:若当前记录的父记录已经有childId,说明旧 token 已被轮换过一次——此时不再创建新记录,而是重新返回同一个子 token(L611-L650)。这是为了避免客户端并发双发时误伤合法请求;
  • 空闲窗口:从当前 token 记录的createdAt起计算,超过 idle 生命周期则返回idle_window_elapsed
  • 家族窗口absoluteExpiresAt一旦过去则返回max_window_elapsed,即无论多活跃,令牌家族达到最大寿命后必须重新登录;
  • 正常路径下创建一个active的子记录(继承deviceIdmetadata与家族的absoluteExpiresAt),并把父记录更新为status: 'rotated'childId: <新 sessionId>
  • 每次轮换都会顺带触发惰性清理:maybeCleanupExpired每 50 次调用执行一次deleteExpired(删除absoluteExpiresAt已过期的记录,L307-L314)。

generateAccessToken(refreshToken)则先走validateRefreshToken,通过后再签发一个以accessTokenLifespanexpiresIn的短令牌,payload 为{ userId, sessionId, type: 'access' }(L521-L562)。

4. 吊销:invalidateRefreshToken / revokeSessionById

  • invalidateRefreshToken(userId, deviceId?)直接deleteBy({ userId, origin, deviceId })——不传deviceId即吊销该用户在此 origin 下的全部会话,传了则只吊销该设备家族(L489-L491);
  • revokeSessionById有归属校验:只有会话的userIdorigin都与请求方匹配时才删除并返回true,防止跨用户越权吊销(L503-L519);
  • listSessions仅返回status: 'active'的记录,按createdAt倒序,因此每个"活跃登录家族"只产生一条条目。

三、Admin 端认证:登录、Cookie 与会话管理端点

Admin 定义了adminorigin,配置位于admin.auth.sessions.*

端点清单

端点说明
POST /admin/login登录,签发 refresh/session token(写入 httpOnly cookiestrapi_admin_refresh),响应体data.token为短时效 access token
POST /admin/register/POST /admin/register-admin注册/创建首个管理员,行为同上
POST /admin/reset-password重置密码:先吊销该用户全部既有会话,再签发新会话
POST /admin/access-token读取 refresh cookie,轮换后返回{ data: { token } }(新 access token)
POST /admin/logout清除 cookie 并吊销 refresh token;body 可带{ deviceId }只吊销单设备家族
GET /admin/users/me/sessions列出当前管理员的活跃会话(设备标签、登录时间、最近使用)
DELETE /admin/users/me/sessions/:sessionId吊销当前用户拥有的单个会话
DELETE /admin/users/me/sessions吊销全部会话;query?keepCurrent=true保留当前请求所依赖的会话("退出其他设备")

登录/注册时的可选请求字段:

  • deviceId(UUID):提供后启用设备维度的吊销能力;
  • rememberMe(boolean):为true时使用长时效 refresh 家族并写入持久化 cookie;否则使用 session 家族(session cookie)。

从 authentication.ts 控制器 可以看到各端点统一通过buildCookieOptionsWithExpiry构造 cookie 选项后写入REFRESH_COOKIE_NAME(即strapi_admin_refresh);/admin/access-token分支会调用rotateRefreshToken并用返回的新 token 覆写 cookie(L300-L332),而logout无论 token 是否有效都会先清空 cookie(L344-L345)。

配置项

配置键默认值说明
admin.auth.secret应用 secret对称算法(HS256/HS384/HS512)的 JWT 密钥
admin.auth.sessions.options.algorithmHS256JWT 算法
admin.auth.sessions.options.privateKey非对称算法(RS256/RS512/ES256 等)私钥
admin.auth.sessions.options.publicKey非对称算法公钥
admin.auth.sessions.options.*其余 JWT 选项透传(issuer、audience、subject 等)
admin.auth.sessions.accessTokenLifespan1800 秒源码中回退值即30 * 60(见 bootstrap.ts)
admin.auth.sessions.maxRefreshTokenLifespan30 天refresh 家族绝对上限
admin.auth.sessions.idleRefreshTokenLifespan14 天refresh 家族空闲超时
admin.auth.sessions.maxSessionLifespan1 天session 家族绝对上限
admin.auth.sessions.idleSessionLifespan2 小时session 家族空闲超时

已废弃(Deprecated)

  • admin.auth.options.*:请改用admin.auth.sessions.options.*。bootstrap 中保留了兼容逻辑:当用户仍配置了旧的admin.auth.options.expiresIn且未设置新的maxRefreshTokenLifespan/maxSessionLifespan时,会打印警告提示该配置将在 Strapi 6 中移除(bootstrap.ts),旧值会被折算后作为回退默认值。
  • Cookie 相关选项:
    • admin.auth.cookie.name(默认jwtToken)——session 登录(rememberMe=false)时非 httpOnly access-token cookie 的名称,也用于 EE SSO 交接。若同一父域名下还有其他应用也写jwtTokencookie,应改名避免冲突。修改后需要重新构建 admin(该值在构建期内联进 admin bundle)。
    • admin.auth.cookie.domain(或admin.auth.domain)——作用于strapi_admin_refresh与非 httpOnly access-token cookie。未设置时默认 host-only cookie。设置父域名可让子域间共享 admin 会话;不设置则各主机隔离。修改后需要重新构建 admin
    • admin.auth.cookie.path(默认/admin)——同上两个 cookie 的路径,同父域名下托管多个 Strapi 实例时应按实例区分。修改后需要重新构建 admin
    • admin.auth.cookie.sameSite(默认lax)——作用于strapi_admin_refresh

Admin 端关键文件:

  • Bootstrap/配置:bootstrap.ts
  • 路由:routes/authentication.ts、routes/users.ts
  • 控制器:controllers/authentication.ts、controllers/authenticated-session.ts
  • Bearer access token 校验策略:strategies/admin.ts

四、Content API 认证(users-permissions 插件)

UP 插件通过plugin::users-permissions.jwtManagement提供两种模式,默认值为legacy-support(config.js):

模式行为
legacy-support(默认)签发由plugin::users-permissions.jwt配置决定的长时效jwt,无 refresh/rotation 机制
refresh接入SessionManager:签发短时效 access token(jwt)+ 独立的refreshToken,支持轮换与会话管理

auth.js 控制器 中每个关键动作(login、register、change-password、reset-password 等)都会先读取jwtManagement再分叉处理,jwt.js 服务 则根据模式决定生成一次性 JWT 还是双令牌。

jwtManagementrefresh时:

  • 登录/注册/Provider 回调的响应体包含{ jwt, refreshToken }
  • 新增端点:
    • POST /api/auth/refresh——body 为{ refreshToken },返回{ jwt }并完成 refresh token 轮换;
    • POST /api/auth/logout——默认只吊销当前请求所依赖的会话;可选 body:
      • { scope: 'all' }——吊销该用户的全部会话(会话管理引入前的旧默认行为);
      • { deviceId }——吊销指定设备家族的全部会话;
    • GET /api/auth/sessions——列出当前认证用户的活跃会话;
    • DELETE /api/auth/sessions/:sessionId——吊销认证用户拥有的单个会话。

配置键:

// config/plugins.js 示例 plugin::users-permissions: { jwtManagement: 'refresh', // 'legacy-support' | 'refresh' sessions: { accessTokenLifespan: 1800, // 秒 maxRefreshTokenLifespan: 30 * 24 * 3600, idleRefreshTokenLifespan: 14 * 24 * 3600, maxSessionLifespan: 24 * 3600, idleSessionLifespan: 2 * 3600, }, }

UP 端关键文件:

  • 插件 bootstrap/配置:bootstrap/index.js、config.js
  • 控制器:controllers/auth.js
  • 路由:routes/content-api/auth.js
  • JWT 服务:services/jwt.js

相关测试可参考 auth-sessions.test.js 与 jwt.test.js,其中验证了两种模式下的令牌下发、refresh 端点与 404 边界(非 refresh 模式访问 sessions 端点返回 not found,见 validation auth 测试)。

五、凭据变更时的会话自动吊销

出于安全考虑,修改/重置密码会自动吊销该用户所有设备上的活跃 refresh/session token

  • Admin:通过PUT /admin/users/me(携带currentPasswordpassword)修改密码时,吊销该管理员的全部会话(包括当前会话),用户必须重新认证;
  • Admin:通过POST /admin/reset-password重置密码时,在签发新会话之前先吊销全部既有会话;
  • Content API(refresh 模式):通过POST /api/auth/change-passwordPOST /api/auth/reset-password变更/重置密码时,吊销该用户的全部 users-permissions 会话,并为当前请求签发新的 refresh token。

该行为的底层就是invalidateRefreshToken(userId)不带deviceId时执行的全量deleteBy(session-manager.ts)。其安全意义在于:即使 refresh token 已被窃取,只要用户完成一次密码变更,攻击者手中的旧令牌立即失效,可显著缓解持久化会话劫持(persistent session hijacking)攻击。

六、实践要点与注意事项

  • access token 一律通过Authorization: Bearer <token>头传递,不落在 cookie 或 localStorage(session 登录场景的非 httpOnly access-token cookie 是兼容/SSO 用途的例外,见上文废弃章节);
  • Admin 的 refresh token 存放在 httpOnly cookiestrapi_admin_refresh中,JavaScript 不可读取,天然规避 XSS 窃取 refresh token;
  • 设备绑定会话支持按deviceId精确登出单个设备家族,deviceId在轮换时会从父记录继承到子记录,保证整个家族始终归属同一设备;
  • 会话列表接口返回的lastActiveAt表示该会话最近一次 refresh token 轮换的时间戳,而不是逐请求的活动跟踪——未轮换的活跃会话不会刷新该值。

七、延伸阅读:关键源码索引

模块路径
核心 SessionManager 实现packages/core/core/src/services/session-manager.ts
origin 服务类型定义packages/core/types/src/modules/session-manager.ts
Admin bootstrap 与 origin 注册packages/core/admin/server/src/bootstrap.ts
Admin 认证控制器packages/core/admin/server/src/controllers/authentication.ts
Admin Bearer 校验策略packages/core/admin/server/src/strategies/admin.ts
UP 插件配置默认值packages/plugins/users-permissions/server/src/config.js
UP 认证控制器packages/plugins/users-permissions/server/src/controllers/auth.js
认证机制文档原文docs/docs/docs/01-core/authentication/00-sessions-and-jwt.md

【免费下载链接】strapi🚀 Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询