ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

RedwoodJS 基于角色的访问控制(RBAC)实战指南:从认证到授权的完整实现

RedwoodJS 基于角色的访问控制(RBAC)实战指南:从认证到授权的完整实现 RedwoodJS 基于角色的访问控制RBAC实战指南从认证到授权的完整实现【免费下载链接】redwoodRedwoodGraphQL项目地址: https://gitcode.com/gh_mirrors/re/redwoodRBACRole-based Access Control基于角色的访问控制是 RedwoodJS 中一套简单、可维护的权限管理方案它以角色为粒度在 Web 端useAuth()钩子和 API 端requireAuth()辅助函数之上统一管控谁能访问路由、看到页面特性、调用 Service 或 Function。读完本文你将掌握在 RedwoodJS 项目中定义角色、把角色挂载到currentUser、并分别在 Web 端路由 / NavLink / 组件 / 页面标记与 API 端Service / Function强制实施访问控制的完整实战方案。本文主体依据仓库文档 docs/versioned_docs/version-6.x/how-to/role-based-access-control.md 编写并结合仓库源码如packages/api/src/auth/parseJWT.ts、packages/auth/src/AuthProvider/useHasRole.ts、packages/router/src/AuthenticatedRoute.tsx、packages/graphql-server/src/errors.ts进行原理级印证。认证Authenticationvs 授权Authorization在动手实现 RBAC 之前必须先厘清两个经常被混用的概念认证Authentication验证用户是否是他们声称的那个人——即确认身份。授权Authorization赋予用户访问某个具体资源或功能的权限——即确认你能做什么。用更直白的话说认证是证明你是谁的过程授权是验证你被允许访问什么的过程。RBAC 属于授权范畴它建立在认证成功的基础之上——你首先要知道用户是谁才能判断他拥有哪些角色。RBAC 例子房子物理世界想象你外出度假时的一栋 你是owner房主把 钥匙交给了邻居neighbor和水管工plumber并给他们分配了关闭 报警器的通行码该通行码标识他们是邻居或水管工。邻居可以进厨房拿食物喂 、进办公室给 浇水也能用 。水管工可以下地下室处理管道、用 、进洗衣房或 厨房修水槽但不能进你的办公室。两者都不允许进入你的 卧室。房主知道他们是谁认证并给了钥匙通行码角色决定了他们各自的访问范围。如果房子能强制 RBAC它必须知道这些规则。房子 RBAC 角色矩阵RoleKitchenBasementOfficeBathroomLaundryBedroomNeighbor✅✅✅Plumber✅✅✅✅Owner✅✅✅✅✅✅RBAC 例子博客数字世界在博客场景中任何人都可以浏览 Post无需登录它是public公开的但写、改、删等操作按角色划分Authors作者可以写新的 Post。Editors编辑可以更新 Post。Publishers发布者可以写、审核、编辑和删除 Post。Admins管理员可以做以上所有事甚至更多比如管理用户。博客 RBAC 角色矩阵RoleViewNewEditDeleteManage UsersAuthor✅✅Editor✅✅Publisher✅✅✅✅Admin✅✅✅✅✅Auth 与 RBAC 集成清单要在 RedwoodJS 应用中集成 RBAC需要依次完成以下步骤实现一个身份即服务Identity as a Service/ 认证提供方Authentication Provider定义并分配角色Define and Assign Roles将角色设置到当前用户Set Roles to Current User强制实施访问控制Enforce Access同时加固 Web 端与 API 端Secure Web and Api sides建议先熟悉 Blog 教程以及 pages、cells、services、authentication 和 routes 等概念再继续阅读本文。身份即服务Identity as a Service正确地实现认证其难度、出错率与风险不亚于自己实现一套加密算法。开发者不再需要自行构建身份服务——身份服务负责管理认证及其相关复杂性。RedwoodJS 为多种常见的身份服务生成了认证 Provider原生支持 RBAC并提供管理用户与角色分配 UI 的Netlify Identity、Auth0仍可使用、但需自行提供角色信息的Magic.link、Custom自定义等。对于后者你必须自行提供currentUser.roles信息例如通过 User-to-Role 数据库表或其他数据源。Netlify Identity Access TokenJWT与应用元数据下面是 Netlify Identity 签发的一个解码后的 JSON Web TokenJWT示例其中包含标准声明exp令牌过期时间。sub令牌主体本例为用户标识符。其他常见声明还有iss签发者和aud受众即 JWT 的目标接收方。该解码令牌还包含两类元数据app_metadata存储会影响用户核心功能的信息如支持计划订阅、安全角色或访问控制组例如应用如何运行、用户能访问什么。用户无法自行编辑app_metadata中的数据。user_metadata存储不影响用户核心功能的用户属性如偏好设置。已登录用户通常可以通过携带access_token调用身份服务的用户资料端点来编辑user_metadata中的数据。角色可能存放在app_metadata中有时也会存放在app_metadata下的authorization字段中{ exp: 1598628532, sub: 1d271db5-f0cg-21f4-8b43-a01ddd3be294, email: exampleauthorexample.com, app_metadata: { roles: [author] }, user_metadata: { full_name: Arthur Author, } }把角色设置到当前用户Set Roles to Current User从 JWT 解析角色parseJWT角色可能存放在app_metadata中也可能存放在app_metadata下的authorization中。RedwoodJS 提供的parseJWT辅助函数会同时考虑这两个位置来提取角色。在api/lib/auth.js或api/src/lib/auth.ts中import { parseJWT } from redwoodjs/api export const getCurrentUser async (decoded) { return context.currentUser || { ...decoded, roles: parseJWT({ decoded }).roles } }从源码看parseJWT的实际解析逻辑位于 packages/api/src/auth/parseJWT.ts其角色提取顺序为若解码后的 JWT 顶层直接存在roles声明直接返回该值否则查找app_metadata若传入namespace参数则查找namespace/app_metadata取其中的roles若app_metadata.roles不存在再尝试app_metadata.authorization.roles兜底返回空数组。parseJWT返回{ appMetadata, roles }两个字段。对应的单元测试见 packages/api/src/auth/tests/parseJWT.test.ts它覆盖了以下场景roles顶层声明、app_metadata.roles、app_metadata.authorization.roles、带命名空间的app_metadata例如https://example.com/app_metadata以及 null / undefined 令牌的兜底行为。从数据库查询角色如果 AuthProvider 没有把角色信息写进令牌你可以从数据库表中查询角色。考虑如下 schema一个User拥有多个UserRolesmodel User { id Int id default(autoincrement()) uuid String unique createdAt DateTime default(now()) updatedAt DateTime default(now()) userRoles UserRole[] } model UserRole { id Int id default(autoincrement()) createdAt DateTime default(now()) updatedAt DateTime default(now()) name String user User? relation(fields: [userId], references: [id]) userId Int? unique([name, userId]) }你可以预先向User和UserRole表插入数据创建一个uuid来自身份服务的新用户并为其分配editor角色const uuid 1683d760-5b4d-2ced-a078-23fdfebe2e19 const newUser await db.user.create({ data: { uuid }, }) const userRole await db.userRole.create({ data: { name: editor, user: { connect: { uuid }, }, }, })由于解码后 JWT 的sub声明包含该uuid你可以通过UserRoles表关联User的uuid来查询角色。拿到UserRole后把它们的name组成的数组设置到currentUser上export const getCurrentUser async (decoded) { const userRoles await db.userRole.findMany({ where: { user: { uuid: decoded.sub } }, select: { name: true }, }) const roles userRoles.map((role) { return role.name }) return context.currentUser || { roles } }Web 端 RBACWeb 端通过useAuth()钩子提供的hasRole()来实施权限控制可用于以下位置路由Routes布局中的 NavLinksLayoutCells / 组件Components页面中的标记Markup in Page需要特别注意的是hasRole()会同时检查当前用户是否已认证。其底层实现在 packages/auth/src/AuthProvider/useHasRole.ts它支持角色字符串 vs 当前用户角色字符串/数组与角色数组 vs 当前用户角色字符串/数组的全部四种组合匹配只要当前用户命中角色列表中任意一个角色即返回true。如何保护一个路由用PrivateSet包裹路由并通过roles属性声明允许的角色。单角色保护import { Router, Route, PrivateSet } from redwoodjs/router const Routes () { return ( Router PrivateSet unauthenticatedhome rolesadmin Route path/admin/users page{UsersPage} nameusers / /PrivateSet /Router ) }多角色保护import { Router, Route, PrivateSet } from redwoodjs/router const Routes () { return ( Router PrivateSet unauthenticatedhome roles{[admin, editor, publisher]} Route path/admin/posts/{id:Int}/edit page{EditPostPage} nameeditPost / /PrivateSet /Router ) }注意如果使用的是Set也可以用它的private属性替代PrivateSet组件。从 packages/router/src/Set.tsx 的源码注释可以看到Set的private与unauthenticated属性已被标记为deprecated官方推荐直接使用PrivateSetroles、whileLoadingAuth等属性两者通用。如果当前用户没有被分配该角色会被重定向到unauthenticated属性指定的页面。因此你可以专门定义一个forbidden禁止访问页面让无权限访问者看到import { Router, Route, PrivateSet } from redwoodjs/router const Routes () { return ( Router PrivateSet unauthenticatedforbidden rolesadmin Route path/settings page{SettingsPage} namesettings / Route path/admin page{AdminPage} namesites / /PrivateSet Route notfound page{NotFoundPage} / Route path/forbidden page{ForbiddenPage} nameforbidden / /Router ) }从 packages/router/src/AuthenticatedRoute.tsx 的源码可以看到路由保护的判定逻辑unauthorized !(isAuthenticated (!roles || hasRole(roles)))。即必须同时满足已认证且未指定roles或命中所给角色之一才放行否则在认证加载中显示whileLoadingAuth缺省为 null加载完成后重定向到unauthenticated路由并附带?redirectTo当前路径查询参数若unauthenticated指向的路由名不存在或需要路由参数会抛出明确错误。如何保护布局中的 NavLinkNavLink是一种专用的Link用于导航/菜单链接并在当前路由激活时应用不同样式。单角色保护import { NavLink, Link, routes } from redwoodjs/router import { useAuth } from redwoodjs/auth const SidebarLayout ({ children }) { const { hasRole } useAuth() return ( ... {hasRole(admin) ( NavLink to{routes.users()} classNametext-gray-600 activeClassNametext-gray-900 Manage Users /NavLink ... )} ) }多角色保护import { NavLink, Link, routes } from redwoodjs/router import { useAuth } from redwoodjs/auth const SidebarLayout ({ children }) { const { hasRole } useAuth() return ( ... {hasRole([admin, author, editor, publisher]) ( NavLink to{routes.posts()} classNametext-gray-600 activeClassNametext-gray-900 Manage Posts /NavLink ... )} ) }注意hasRole()同时也会检查当前用户是否已认证。如何保护一个组件在组件中通过条件渲染来按角色控制内容单角色保护import { useAuth } from redwoodjs/auth const Post ({ post }) { const { hasRole } useAuth() return ( nav classNamerw-button-group {(hasRole(admin)) ( a href# classNamerw-button rw-button-red onClick{() onDeleteClick(post.id)} Delete /a ))} /nav ) }多角色保护import { useAuth } from redwoodjs/auth const Post ({ post }) { const { hasRole } useAuth() return ( nav classNamerw-button-group {(hasRole([admin, publisher])) ( a href# classNamerw-button rw-button-red onClick{() onDeleteClick(post.id)} Delete /a ))} /nav ) }如何保护页面中的标记在页面中可以结合isAuthenticated与hasRole精细控制某段 JSX 的显隐单角色保护import { useAuth } from redwoodjs/auth; import SidebarLayout from src/layouts/SidebarLayout; const SettingsPage () { const { isAuthenticated, userMetadata, hasRole } useAuth(); return ( {isAuthenticated ( div classNameml-4 flex-shrink-0 {hasRole(admin) ( a href{https://app.netlify.com/sites/${process.env.SITE_NAME}/identity/${userMetadata.id}} target_blank relnoreferrer Edit on Netlify /a )} /div )} )} }多角色保护import { useAuth } from redwoodjs/auth; import SidebarLayout from src/layouts/SidebarLayout; const SettingsPage () { const { isAuthenticated, userMetadata, hasRole } useAuth(); return ( {isAuthenticated ( div classNameml-4 flex-shrink-0 {hasRole([admin, userManager]) ( a href{https://app.netlify.com/sites/${process.env.SITE_NAME}/identity/${userMetadata.id}} target_blank relnoreferrer Edit on Netlify /a )} /div )} )} }API 端 RBACAPI 端的核心是requireAuth()用于检查用户是否已登录无论是否分配角色可选地检查角色并在不满足条件时抛出异常Services服务Functions函数使用 Netlify Identity Triggers 设置默认角色示例requireAuth()在 Service 中使用requireAuth()检查用户是否登录、是否被分配了可选的角色并在不满足时抛出错误。检查单个角色requireAuth({ roles: editor })检查多个角色满足其一即可requireAuth({ roles: [admin, author, publisher] })该函数应位于 RedwoodJS 应用的api/src/lib/auth.js即getCurrentUser()所在位置export const requireAuth ({ roles } {}) { if (!isAuthenticated()) { throw new AuthenticationError(You dont have permission to do that.) } if (roles !hasRole(roles)) { throw new ForbiddenError(You dont have access to do that.) } }这里的AuthenticationError与ForbiddenError定义于 packages/graphql-server/src/errors.ts二者均继承自RedwoodGraphQLError其本质是带code扩展字段的 GraphQL 错误AuthenticationError的扩展码为UNAUTHENTICATEDForbiddenError的扩展码为FORBIDDEN。这意味着在 GraphQL 层抛出后客户端可以依据扩展码精确区分未登录与无权限两种失败。如何保护一个 Serviceimport { db } from src/lib/db import { requireAuth } from src/lib/auth const CREATE_POST_ROLES [admin, author, publisher] export const createPost ({ input }) { requireAuth({ role: CREATE_POST_ROLES }) return db.post.create({ data: { ...input, authorId: context.currentUser.sub, publisherId: context.currentUser.sub, }, }) }提示文档示例中同时出现了requireAuth({ role: ... })与requireAuth({ roles: ... })两种键名写法前者在原文的 Service 示例中后者在requireAuth定义示例中。在 dbAuth Provider 的官方 READMEpackages/auth-providers/dbAuth/api/README.md中规范用法为requireAuth({ role: admin })或requireAuth({ role: [editor, author] })。实际使用时请以你项目生成模板中的api/src/lib/auth.js实现为准保持键名一致。如何保护一个 Function由于requireAuth()会抛出异常需要在 handler 中捕获并返回HTTP 401 Unauthorized或HTTP 403 Forbidden客户端错误状态码import { requireAuth } from src/lib/auth import { AuthenticationError, ForbiddenError } from redwoodjs/api export const handler async (event, context) { try { requireAuth({ roles: admin }) return { headers: { Content-Type: application/json, }, statusCode: 200, body: JSON.stringify({ data: Permitted, }), } } catch (e) { if (e instanceof AuthenticationError) { return { statusCode: 401, } } else if (e instanceof ForbiddenError) { return { statusCode: 403, } } else { return { statusCode: 400, } } } }使用 Netlify Identity Triggers 在注册时设置默认角色当某些 Identity 事件发生时可以触发 serverless function 调用例如用户注册时。Netlify Identity 目前支持以下事件identity-validate当 Identity 用户尝试通过 Identity 注册时触发。identity-signup当 Identity 用户通过 Netlify Identity 注册时触发。注意仅对 emailpassword 注册生效外部 Provider 如 Google/GitHub 注册不会触发identity-login当 Identity 用户通过 Netlify Identity 登录时触发。要让 serverless function 在这些事件上触发只需让函数文件名与事件名一致。例如要在identity-signup事件上触发就把函数文件命名为identity-signup.js。如果这些事件函数返回除 200 或 204 之外的状态码注册或登录将被阻止。若返回 200你还可以返回一个包含新user_metadata或app_metadata的 JSON 对象用于写回 Identity 用户export const handler async (req, _context) { const body JSON.parse(req.body) const eventType body.event const user body.user const email user.email let roles [] if (eventType signup) { if (email.includes(author)) { roles.push(author) } if (email.includes(editor)) { roles.push(editor) } if (email.includes(publisher)) { roles.push(publisher) } return { headers: { Content-Type: application/json, }, statusCode: 200, body: JSON.stringify({ app_metadata: { roles: roles } }), } } else { return { statusCode: 200, } } }如何在开发环境中调用 serverless function只要yarn rw dev正在运行就可以使用netlify-cli调用你的函数。步骤如下# 安装 cli yarn add netlify-cli -g # 修改 /functions 后重新构建 api yarn rw build api # 用 CLI 调用函数并指向 rw dev 端口 netlify functions:invoke function-name --port 8910将function-name替换为identity-validate、identity-signup、identity-login或你自己的函数名。注意netlify-cli不会为每次调用生成假的用户数据它总是提供相同的Test Person数据。与 dbAuth 等 Provider 的配合如果你的应用使用 dbAuthRedwoodJS 自带的数据库认证getCurrentUser通常会直接从数据库读取用户此时只需在查询的select中带上roles字段hasRole/requireAuth即可正常工作。关于 dbAuth 中requireAuth的角色参数约定可参阅 packages/auth-providers/dbAuth/api/README.md。更完整的端到端演练含 Prisma schema 中roles String default(moderator)的迁移示例见 docs/versioned_docs/version-6.x/tutorial/chapter7/rbac.md它与本文的 How To 互为补充——前者偏教程实操后者偏方案总结。其他可参考资源RBAC 方案在 GraphQL 层的权限封装可参考 docs/versioned_docs/version-6.x/directives.md基于 directive 的校验方式以及 docs/versioned_docs/version-6.x/graphql.md。认证总体方案与各 Provider 的配置见 docs/versioned_docs/version-6.x/authentication.md 与 docs/versioned_docs/version-6.x/auth/ 目录。安全相关最佳实践可参考 docs/versioned_docs/version-6.x/security.md。【免费下载链接】redwoodRedwoodGraphQL项目地址: https://gitcode.com/gh_mirrors/re/redwood创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表