多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

使用 @tinacms/auth 为 Next.js 后端 API 与媒体服务接入 TinaCloud 鉴权

使用 @tinacms/auth 为 Next.js 后端 API 与媒体服务接入 TinaCloud 鉴权 使用 tinacms/auth 为 Next.js 后端 API 与媒体服务接入 TinaCloud 鉴权【免费下载链接】tinacmsTinaCMS is the leading open-source headless CMS that supports Markdown and Visual Editing. Your content is stored in your own GitHub repo ❤️项目地址: https://gitcode.com/GitHub_Trending/ti/tinacmstinacms/auth是 TinaCMS monorepo 中专为 Next.js 与 TinaCloud 协作场景设计的鉴权包它封装了「向 TinaCloud 身份服务校验用户令牌」的完整逻辑让开发者可以轻松地为自己的后端 API 路由如上传图片到 Cloudinary、S3 等外部服务加上一道「只有拥有 TinaCloud 账号的用户才能访问」的守卫。读完本文你将掌握isAuthorized、TinaCloudBackendAuthProvider的用法、fetchWithToken的前后端配合方式以及该包在鉴权边界上采取的安全设计。本文所有代码与结论均以当前仓库 packages/tinacms/auth 包为准并结合其源码、测试与周边包实现展开。包定位Next.js 专用的 TinaCloud 鉴权能力按 readme.md 的说明tinacms/auth包含所有与Next.js相关的 TinaCloud 鉴权代码。其核心思路是authorize user function 会向 TinaCloud 发起请求若存在当前用户则返回该用户。这样做的目的是让你可以对接外部服务并且只允许拥有 TinaCloud 账号的用户访问后端代码。典型场景就是只允许已登录已登录 TinaCloud的用户上传图片到 Cloudinary 或 S3。也就是说TinaCMS 的前端内容编辑者通过 TinaCloud 完成登录后你的自定义后端接口可以通过本包校验其身份从而决定是否放行。从 package.json 可以看到该包当前版本为1.1.5采用 ESMtype: module自身没有任何运行时依赖Next.js仅作为devDependency用于类型引用。核心 API 一览该包从 src/index.ts 导出了以下关键成员导出作用isAuthorized(req, expectedClientID?)接收 Next.js 的NextApiRequest从请求头读取令牌向 TinaCloud 校验返回TinaCloudUser \| undefinedisAuthorizedNextisAuthorized的别名导出源码中export const isAuthorizedNext isAuthorizedisUserAuthorized({ clientID, token })底层纯函数给定 clientID 与 token 直接调用 TinaCloud 身份接口TinaCloudBackendAuthProvider(clientID?)返回一个可直接接入媒体服务/后端网关的鉴权 provider输出标准化的鉴权结果TinaCloudUser用户对象类型定义后端代码在 Next.js API 路由中校验用户在 Next.js 中后端代码写在pages/api/目录下。原文档给出了一个pages/api/upload.ts的示例下面是保留其完整逻辑并补充安全细节的版本import { NextApiHandler } from next import { isAuthorized } from tinacms/auth const apiHandler: NextApiHandler async (req, res) { // 检查用户是否已登录。令牌无效时返回 undefined。 // 传入站点自己的 clientID令牌会针对你的应用进行校验。 const user await isAuthorized(req, process.env.NEXT_PUBLIC_TINA_CLIENT_ID) if (user user.verified) { console.log(this user is logged in) // 此时可以执行受保护操作例如上传图片 await imageUploadFunction(req, res) res.json({ validUser: true }) return } else { console.log(this user NOT is logged in) res.json({ validUser: false }) } } export default apiHandlerisAuthorized接收req没有用户时返回undefined用户已登录时返回TinaCloudUser。判定的关键是user.verified字段——即使令牌有效、用户存在也只有在verified true时才被视为完全可信。返回的用户对象当校验通过时你会得到一个TinaCloudUserinterface TinaCloudUser { id: string email: string verified: boolean role: admin | user enabled: boolean fullName: string }该接口定义于 src/index.ts。其中verified表示邮箱是否已验证enabled表示账号是否可用role用于区分管理员与普通用户。前端代码用 fetchWithToken 携带令牌发起请求后端校验依赖请求头authorization中的令牌而前端例如媒体管理器或任何被TinaCloudAuthWall包裹的组件可以通过fetchWithToken自动带上正确的令牌const cms useCMS() const tinaCloudClient: Client cms.api.tina const uploadImage async () { const req await tinaCloudClient.fetchWithToken(/api/upload) console.log({ result: await req.json() }) }fetchWithToken本质上就是普通的fetch只是额外在请求头中加入了authorization令牌。它的底层实现位于 packages/tinacms/src/internalClient/authProvider.ts通过getAccessToken()取出访问令牌后拼装headers[Authorization] Bearer accessToken再发起请求当响应为 401 且此前确实携带了令牌时还会触发sessionExpiredListener让登录墙auth wall自动重新武装提示会话过期。为什么前端不带 clientID注意后端校验使用的是站点自己的NEXT_PUBLIC_TINA_CLIENT_ID而不是从请求中读取的任意值。前端请求 URL 可以追加clientID查询参数例如 Cloudinary 媒体存储的做法见下文但后端绝不会信任该参数作为校验依据这是本包重要的安全边界。底层原理令牌如何被校验isAuthorized内部实际执行的是 src/index.ts 中的三步逻辑读取令牌从req.headers.authorization取令牌若不存在则打印错误日志并返回undefined不会发起任何网络请求。解析 clientID优先使用显式传入的expectedClientID否则回退到process.env.NEXT_PUBLIC_TINA_CLIENT_ID并对结果做trim()若二者都为空直接拒绝授权返回undefined。调用身份服务调用底层函数isUserAuthorized向https://identity.tinajs.io/v2/apps/${clientID}/currentUser发起 GET 请求请求头携带Content-Type: application/json与原始的authorization令牌。响应ok时解析并返回用户对象否则返回undefined。isUserAuthorized的实现位于 src/index.ts。需要注意它对网络错误的态度当fetch抛出异常如网络中断时会先console.error再重新抛出不会静默吞掉错误——这区分了「令牌无效」返回undefined与「服务不可用」抛异常两种情形避免调用方把一次瞬时网络故障误判成用户未登录。保护媒体上传TinaCloudBackendAuthProvider 与媒体处理器除手动在 API 路由中调用isAuthorized外本包还提供TinaCloudBackendAuthProvider它返回一个标准的后端鉴权 provider便于接入媒体存储等基础设施。源码见 src/index.tsexport const TinaCloudBackendAuthProvider ( clientID: string | undefined process.env.NEXT_PUBLIC_TINA_CLIENT_ID ) { const backendAuthProvider { isAuthorized: async (req: IncomingMessage, _res: ServerResponse) { const user await isAuthorized(req as NextApiRequest, clientID) if (user user.verified) { return { isAuthorized: true as const } } return { isAuthorized: false as const, errorCode: 401, errorMessage: Unauthorized, } }, } return backendAuthProvider }它复用了isAuthorized的校验逻辑并将结果规范化为{ isAuthorized: true }或{ isAuthorized: false, errorCode: 401, errorMessage: Unauthorized }。未验证邮箱的用户同样会被拒绝返回 401。这个 provider 在仓库中有两处典型落地Cloudinary 媒体处理器packages/next-tinacms-cloudinary/src/handlers.ts 中createMediaHandler接收配置里的authorized(req, res)回调在进入上传/列举/删除逻辑前先调用它!isAuthorized时直接res.status(401).json({ message: sorry this user is unauthorized })前端媒体存储packages/next-tinacms-cloudinary/src/cloudinary-tina-cloud-media-store.ts 中TinaCloudCloudinaryMediaStore的fetchFunction会在请求 URL 上追加clientID查询参数并用client.authProvider.fetchWithToken发起请求保证后端能拿到令牌脚手架模板packages/tinacms/cli/src/cmds/init/prompts/authProvider.ts 中tina-cloud认证方案生成的配置即为TinaCloudBackendAuthProvider(process.env.NEXT_PUBLIC_TINA_CLIENT_ID)并自动从tinacms/auth引入。在自托管示例 examples/next/tina-self-hosted-demo/tina/config.tsx 中可以看到媒体存储的接入点注释中保留了切换到next-tinacms-cloudinary的TinaCloudCloudinaryMediaStore的示例而默认使用 TinaCloud 自带媒体存储。安全边界clientID 固定校验与 fail-closed 行为本包在 1.1.4 版本做了一次重要的安全修复见 CHANGELOG.mdTinaCloud 授权现在被固定到站点自己配置的 clientID而不是从请求中读取的值。isAuthorized接受可选的expectedClientID回退到NEXT_PUBLIC_TINA_CLIENT_ID两者都无法解析时直接拒绝授权。TinaCloudBackendAuthProvider、next-tinacms-azure适配器以及tinacms init模板都会透传站点 clientID。这一点在测试 packages/tinacms/auth/src/index.test.ts 中得到了明确验证请求的req.query.clientID携带some-other-app但校验使用的是显式传入的my-site-app或环境变量中的env-site-app且请求 URL 中不会出现请求方伪造的 app 名当 clientID 无法解析既无参数也无环境变量、或参数为空白字符串时直接返回undefined且不发起任何 fetch 请求缺少authorization请求头时同样直接返回undefined且不调用网络。自托管部署注意事项来自 CHANGELOG授权在站点 clientID 无法于运行时解析时会 fail-closed失败即关闭。请确保NEXT_PUBLIC_TINA_CLIENT_ID存在于服务端运行时而不仅是在构建时被内联或显式传入TinaCloudBackendAuthProvider(...)与媒体存储的authorized回调例如isAuthorized(req, process.env.NEXT_PUBLIC_TINA_CLIENT_ID)若无法解析后端与媒体授权将统一返回 401。此外index.test.ts 还用一组「应用级令牌隔离」测试证明了某站点的令牌只能在对应 app 下通过校验其他 app 的令牌、以及请求中伪造的 app 名都无法通过该站点的授权——即令牌的作用域被严格限定在站点自身 clientID 对应的应用内。测试覆盖与行为清单src/index.test.ts 通过 mockglobal.fetch系统性地验证了本包的全部行为这些行为即是对 API 契约最精确的说明场景期望结果TinaCloud 返回非ok状态令牌过期/畸形返回undefined网络错误重新抛出原始错误并先console.error令牌有效返回TinaCloudUser对象clientID 无法解析无参数、无环境变量返回undefined不调用 fetchclientID 为空白字符串返回undefined不调用 fetch缺少authorization请求头返回undefined不调用 fetch用户存在但verified: falseTinaCloudBackendAuthProvider返回 401用户存在且verified: trueTinaCloudBackendAuthProvider返回{ isAuthorized: true }未配置 clientID返回 401不调用 fetch令牌来自其他 app / 请求伪造 app授权失败这些测试还印证了「clientID 只来自参数或环境变量绝不来自请求」以及「未验证用户一律不放行」两条核心安全约定。小结tinacms/auth以极小的 API 面解决了 Next.js 后端「如何确认调用者确实是登录了 TinaCloud 的用户」这一核心问题isAuthorized适合直接在自定义 API 路由中使用TinaCloudBackendAuthProvider适合作为标准化鉴权 provider 接入媒体存储等基础设施前端则统一通过cms.api.tina.fetchWithToken携带令牌。结合其 fail-closed、clientID 固定校验、忽略请求伪造参数等安全设计你可以放心地将上传、删除等敏感操作对「有 TinaCloud 账号且已验证」的用户开放。更多实现细节可继续阅读 源码 与 测试用例。【免费下载链接】tinacmsTinaCMS is the leading open-source headless CMS that supports Markdown and Visual Editing. Your content is stored in your own GitHub repo ❤️项目地址: https://gitcode.com/GitHub_Trending/ti/tinacms创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表