
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 APIusers-permissions 插件两套认证体系的会话管理实现从SessionManager的 origin 多租户模型、access token / refresh token 双令牌设计到刷新令牌轮换rotation、空闲/绝对生命周期、设备维度吊销以及密码变更时自动失效所有会话的安全策略。读完本文你可以完整理解 Strapi 中一条 refresh token 从签发、轮换到吊销的完整生命周期并能正确配置admin.auth.sessions.*与plugin::users-permissions相关的认证参数。一、整体架构两条认证链路共用一个 SessionManagerStrapi 仓库内的认证分为两个来源originAdmin后台管理员登录、注册、重置密码等走admin.auth.*配置Content API经由 users-permissions 插件下文简称 UP面向内容 API 的终端用户认证走plugin::users-permissions.*配置。两条链路在 v5 架构下共享同一套核心会话服务。Core 提供的SessionManager统一负责签发短时效 access tokenJWT客户端以Authorization: Bearer token携带长时效 refresh/session tokenJWT按来源origin以不同方式存储——Admin 端存 httpOnly cookieContent API 端直接下发给客户端。其实现入口位于 session-manager.tscreateSessionManager默认绑定数据库 provider 并写入隐藏内容类型admin::session返回一个既可调用又可挂方法的流式 API——strapi.sessionManager(admin)返回绑定adminorigin 的OriginSessionManager同时暴露generateSessionId、defineOrigin、hasOrigin等全局方法。// 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 有效期maxRefreshTokenLifespan、idleRefreshTokenLifespan秒refresh 家族的绝对上限 / 空闲超时maxSessionLifespan、idleSessionLifespan秒session 家族rememberMefalse的绝对上限 / 空闲超时algorithmJWT 算法默认HS256constants.ts 中的DEFAULT_ALGORITHMjwtOptions透传给jsonwebtoken的其他选项issuer、audience、subject、privateKey 等会话数据模型各 origin 的会话记录统一落在隐藏内容类型admin::session中底层表strapi_sessions核心字段包括userId、sessionId、deviceId、origin、expiresAt、absoluteExpiresAt、status、type另有一个自由格式的metadata字段供各 origin 存放自定义数据如设备名、登录时间SessionManager 只负责原样存取、不做解释SessionData 定义。关键字段的语义type: refresh | session——refresh是长期令牌家族rememberMesession是绑定浏览器会话的短家族status: active | rotated | revoked——只有active状态的记录才有资格换取 access tokenvalidateRefreshToken 校验childId——轮换链的父子指针父记录被标记rotated并指向子记录这是实现一次有效轮换的关键结构expiresAt是空闲到期时间absoluteExpiresAt是整个令牌家族的绝对到期时间。对外公开 API按 originOriginSessionManager暴露的方法类型定义见 types/src/modules/session-manager.tsgenerateRefreshToken(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. 签发generateRefreshTokengenerateRefreshToken 的流程是按 token 类型选择空闲生命周期与绝对生命周期refresh用idleRefreshTokenLifespan/maxRefreshTokenLifespansession用idleSessionLifespan/maxSessionLifespan在数据库创建根记录childId: null、status: activesessionId由 16 字节随机数的 hex 生成generateSessionId用记录的createdAt计算iat/exp以noTimestamp: true方式签名 JWTpayload 为{ userId, sessionId, type: refresh, iat, exp }返回{ token, sessionId, absoluteExpiresAt }。值得注意的细节对称算法下签名使用config.jwtSecret非对称算法RS*/ES*/PS* 前缀则要求jwtOptions.privateKey签名或jwtOptions.publicKey验签存在否则直接抛错getJwtKey。另外签名前会剔除expiresIn/privateKey/publicKey等与 payload、密钥选择冲突的选项避免jsonwebtoken行为歧义。2. 校验validateRefreshToken 的四重防线validateRefreshToken 依次检查JWT 验签通过且payload.type refresh拒绝拿 access token 冒充 refresh token数据库中存在对应sessionId的记录expiresAt空闲到期与absoluteExpiresAt家族绝对到期均未过记录status仍为active且userId与 payload 一致。validateAccessToken则是纯签名校验L399-L427无数据库查询因此适合放进每个请求的热路径。3. 轮换rotateRefreshToken 的子令牌模型rotateRefreshToken 实现的是典型的 refresh token rotation几个关键行为重放检测若当前记录的父记录已经有childId说明旧 token 已被轮换过一次——此时不再创建新记录而是重新返回同一个子 tokenL611-L650。这是为了避免客户端并发双发时误伤合法请求空闲窗口从当前 token 记录的createdAt起计算超过 idle 生命周期则返回idle_window_elapsed家族窗口absoluteExpiresAt一旦过去则返回max_window_elapsed即无论多活跃令牌家族达到最大寿命后必须重新登录正常路径下创建一个active的子记录继承deviceId、metadata与家族的absoluteExpiresAt并把父记录更新为status: rotated、childId: 新 sessionId每次轮换都会顺带触发惰性清理maybeCleanupExpired每 50 次调用执行一次deleteExpired删除absoluteExpiresAt已过期的记录L307-L314。generateAccessToken(refreshToken)则先走validateRefreshToken通过后再签发一个以accessTokenLifespan为expiresIn的短令牌payload 为{ userId, sessionId, type: access }L521-L562。4. 吊销invalidateRefreshToken / revokeSessionByIdinvalidateRefreshToken(userId, deviceId?)直接deleteBy({ userId, origin, deviceId })——不传deviceId即吊销该用户在此 origin 下的全部会话传了则只吊销该设备家族L489-L491revokeSessionById有归属校验只有会话的userId和origin都与请求方匹配时才删除并返回true防止跨用户越权吊销L503-L519listSessions仅返回status: active的记录按createdAt倒序因此每个活跃登录家族只产生一条条目。三、Admin 端认证登录、Cookie 与会话管理端点Admin 定义了adminorigin配置位于admin.auth.sessions.*。端点清单端点说明POST /admin/login登录签发 refresh/session token写入 httpOnly cookiestrapi_admin_refresh响应体data.token为短时效 access tokenPOST /admin/register/POST /admin/register-admin注册/创建首个管理员行为同上POST /admin/reset-password重置密码先吊销该用户全部既有会话再签发新会话POST /admin/access-token读取 refresh cookie轮换后返回{ data: { token } }新 access tokenPOST /admin/logout清除 cookie 并吊销 refresh tokenbody 可带{ deviceId }只吊销单设备家族GET /admin/users/me/sessions列出当前管理员的活跃会话设备标签、登录时间、最近使用DELETE /admin/users/me/sessions/:sessionId吊销当前用户拥有的单个会话DELETE /admin/users/me/sessions吊销全部会话query?keepCurrenttrue保留当前请求所依赖的会话退出其他设备登录/注册时的可选请求字段deviceIdUUID提供后启用设备维度的吊销能力rememberMeboolean为true时使用长时效 refresh 家族并写入持久化 cookie否则使用 session 家族session cookie。从 authentication.ts 控制器 可以看到各端点统一通过buildCookieOptionsWithExpiry构造 cookie 选项后写入REFRESH_COOKIE_NAME即strapi_admin_refresh/admin/access-token分支会调用rotateRefreshToken并用返回的新 token 覆写 cookieL300-L332而logout无论 token 是否有效都会先清空 cookieL344-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.tsadmin.auth.sessions.maxRefreshTokenLifespan30 天refresh 家族绝对上限admin.auth.sessions.idleRefreshTokenLifespan14 天refresh 家族空闲超时admin.auth.sessions.maxSessionLifespan1 天session 家族绝对上限admin.auth.sessions.idleSessionLifespan2 小时session 家族空闲超时已废弃Deprecatedadmin.auth.options.*请改用admin.auth.sessions.options.*。bootstrap 中保留了兼容逻辑当用户仍配置了旧的admin.auth.options.expiresIn且未设置新的maxRefreshTokenLifespan/maxSessionLifespan时会打印警告提示该配置将在 Strapi 6 中移除bootstrap.ts旧值会被折算后作为回退默认值。Cookie 相关选项admin.auth.cookie.name默认jwtToken——session 登录rememberMefalse时非 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.tsBearer access token 校验策略strategies/admin.ts四、Content API 认证users-permissions 插件UP 插件通过plugin::users-permissions.jwtManagement提供两种模式默认值为legacy-supportconfig.js模式行为legacy-support默认签发由plugin::users-permissions.jwt配置决定的长时效jwt无 refresh/rotation 机制refresh接入SessionManager签发短时效 access tokenjwt 独立的refreshToken支持轮换与会话管理auth.js 控制器 中每个关键动作login、register、change-password、reset-password 等都会先读取jwtManagement再分叉处理jwt.js 服务 则根据模式决定生成一次性 JWT 还是双令牌。当jwtManagement为refresh时登录/注册/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.jsJWT 服务services/jwt.js相关测试可参考 auth-sessions.test.js 与 jwt.test.js其中验证了两种模式下的令牌下发、refresh 端点与 404 边界非 refresh 模式访问 sessions 端点返回 not found见 validation auth 测试。五、凭据变更时的会话自动吊销出于安全考虑修改/重置密码会自动吊销该用户所有设备上的活跃 refresh/session tokenAdmin通过PUT /admin/users/me携带currentPassword与password修改密码时吊销该管理员的全部会话包括当前会话用户必须重新认证Admin通过POST /admin/reset-password重置密码时在签发新会话之前先吊销全部既有会话Content APIrefresh 模式通过POST /api/auth/change-password或POST /api/auth/reset-password变更/重置密码时吊销该用户的全部 users-permissions 会话并为当前请求签发新的 refresh token。该行为的底层就是invalidateRefreshToken(userId)不带deviceId时执行的全量deleteBysession-manager.ts。其安全意义在于即使 refresh token 已被窃取只要用户完成一次密码变更攻击者手中的旧令牌立即失效可显著缓解持久化会话劫持persistent session hijacking攻击。六、实践要点与注意事项access token 一律通过Authorization: Bearer token头传递不落在 cookie 或 localStoragesession 登录场景的非 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.tsorigin 服务类型定义packages/core/types/src/modules/session-manager.tsAdmin bootstrap 与 origin 注册packages/core/admin/server/src/bootstrap.tsAdmin 认证控制器packages/core/admin/server/src/controllers/authentication.tsAdmin Bearer 校验策略packages/core/admin/server/src/strategies/admin.tsUP 插件配置默认值packages/plugins/users-permissions/server/src/config.jsUP 认证控制器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),仅供参考