RedwoodJS 安全实践指南:从认证、GraphQL 到 Serverless 函数与 Webhook 的端到端防护
2026/9/24 2:38:55 网站建设 项目流程
  • 后端
  • 前端
  • Web框架
  • 开发工具

【免费下载链接】redwood

RedwoodGraphQL

项目地址:https://gitcode.com/gh_mirrors/re/redwood
点击查看免费下载

RedwoodJS 把安全当作框架的一等公民:从 Web 端路由守卫、GraphQL 指令鉴权,到 Serverless 函数的访问控制与 Webhook 签名校验,框架默认提供"开箱即安全"的配置,同时把最终责任交还给开发者。读完本文,你将掌握 RedwoodJS 六大安全支柱——认证集成、GraphQL 防滥用、生产环境 Introspection 禁用、函数级鉴权、Webhook 校验与密钥环境管理——并能直接在自己的应用中落地可运行的配置与代码。

本文以 RedwoodJS v6 官方安全文档(security.md)为主体骨架,并结合当前仓库中的框架源码(如 packages/api/src/webhooks/index.ts)与关联文档(graphql.md、serverless-functions.md、webhooks.md)进行纵深展开。

安全是框架的责任,更是你的责任

RedwoodJS 官方文档开篇即强调:框架希望你能构建并部署安全的应用程序,并且严肃对待安全问题。RedwoodJS 在 GitHub 上提供安全公告渠道与 CodeQL 代码扫描,官方安全策略与漏洞上报联系方式也一并在文档中给出。

但请务必记住文档中的警示:

⚠️安全是开发者自己的责任虽然 Redwood 提供了工具、实践和信息来保证应用程序安全,但落实这些措施仍然是你自己的责任。官方强烈建议使用规范的密码、令牌与密钥保护手段,包括受约束的沟通流程、密码管理工具,以及 Doppler 之类的环境管理服务。

这句话划定了 RedwoodJS 安全模型的边界:框架提供机制(mechanism),策略(policy)由你制定。接下来的每一节,我们都将围绕"框架提供了什么机制 + 开发者需要落实什么策略"展开。

RedwoodJS 的端到端安全防线覆盖四个主要攻击面,后续各节逐一展开:

  1. 认证(Authentication):Web 端useAuth钩子 + API 端getCurrentUser/requireAuth
  2. GraphQL:指令鉴权、恶意文档拦截、Introspection 与 Playground 默认禁用;
  3. Serverless 函数(Functions):开放端点默认裸奔,需用useRequireAuth等方案收口;
  4. Webhooks:入站校验签名、出站签名载荷,防篡改、防重放。

认证:@redwoodjs/auth与多提供商支持

认证提供商生态

@redwoodjs/auth是对主流 SPA 认证库的轻量封装,支持以下认证提供商(详见 authentication.md):

  • Netlify Identity Widget
  • Auth0
  • Azure Active Directory
  • Netlify GoTrue-JS
  • Magic Links – Magic.js
  • Firebase 的 GoogleAuthProvider
  • Ethereum
  • Supabase
  • Nhost

此外,Redwood 还提供了自托管方案dbAuth(详见 auth/dbauth.md),让你完全掌控用户数据与认证流程。

官方集成以@redwoodjs作用域区分:例如 Auth0 集成由@redwoodjs/auth-auth0-web@redwoodjs/auth-auth0-api两个 npm 包构成。通过认证设置命令即可完成接入:

yarn rw setup auth auth0

端到端的认证链路

Redwood 将认证从 Web 端打通到 API 端。Web 侧通过web/src/auth.ts中的AuthProvideruseAuth钩子接入认证上下文,RedwoodApolloProvider会在每个 GraphQL 请求中携带 JWT,Router则用useAuth判断用户是否有权访问私有或角色受限路由。

API 侧的处理分两步:解码令牌映射为用户对象,分别由createGraphQLHandlerauthDecodergetCurrentUser两个属性完成(见 authentication.md):

import { authDecoder } from '@redwoodjs/auth-auth0-api' import { createGraphQLHandler } from '@redwoodjs/graphql-server' import directives from 'src/directives/**/*.{js,ts}' import sdls from 'src/graphql/**/*.sdl.{js,ts}' import services from 'src/services/**/*.{js,ts}' import { getCurrentUser } from 'src/lib/auth' import { db } from 'src/lib/db' import { logger } from 'src/lib/logger' export const handler = createGraphQLHandler({ authDecoder, getCurrentUser, loggerConfig: { logger, options: {} }, directives, sdls, services, onException: () => { // Disconnect from your database with an unhandled exception. db.$disconnect() }, })

useAuth钩子常用 API

useAuth为认证提供商客户端 SDK 提供了统一接口,常用成员如下(完整表格见 authentication.md):

名称说明
client创建认证提供商时使用的客户端实例,多数函数在底层使用它
currentUserAPI 端设置好的当前用户信息;未认证时为null
getToken返回一个 JWT
hasRole判断当前用户是否拥有某角色(如"admin")或角色数组中的任一角色
isAuthenticated布尔值,表示用户是否已认证
loading认证上下文是否正在加载
logIn/logOut登录 / 登出
signUp注册
userMetadata直接取自认证提供商客户端的用户元数据;未认证时为null

在路由层,可以用PrivateSet组件包裹受保护路由,实现"未登录不可见",并支持角色级访问控制;在组件内则可以组合useAuth暴露的原语自由构建登录体验。API 侧则"默认锁定":所有生成的 SDL 都带@requireAuth指令,公开访问是显式选择加入而非默认行为。

GraphQL 安全:默认拒绝恶意操作文档

GraphQL 是 Redwood 的核心。解析 GraphQL 操作文档是非常昂贵且计算密集的操作,会阻塞 JavaScript 事件循环——攻击者反复发送略作变化的复杂操作文档,即可轻易拖垮 GraphQL 服务器。因此 graphql.md 的 Security 章节(第 1268 行起)系统阐述了 Redwood 的 GraphQL 安全默认值:

  • 基于 Schema 指令的认证,包含 RBAC 校验
  • 生产部署自动禁用 Introspection 与 GraphQL Playground
  • 拒绝恶意操作文档(Max Aliases、Max Cost、Max Depth、Max Directives、Max Tokens)
  • 防止信息泄露(Block Field Suggestions、Mask Errors)

这些能力通过GraphQL Armor(Escape Technologies 与 The Guild 合作开发的 JS 服务器中间件)以合理默认值内建,开发者无需任何配置即可获得防护。

默认锁定:@requireAuth/@skipAuth/ 自定义指令

默认情况下,GraphQL 端点对全世界开放——SDL 中定义的任何类型与字段,任何人都可以查询。Redwood 鼓励"默认安全",生成 SDL 或 Service 时所有查询与 Mutation 都默认为@requireAuth

应用构建或服务器启动时,Redwood 会检查所有查询和 Mutation 是否都应用了@requireAuth@skipAuth或自定义指令;否则构建失败:

✖ Verifying graphql schema... Building API... Cleaning Web... Building Web... Prerendering Web... You must specify one of @requireAuth, @skipAuth or a custom directive for - contacts Query - posts Query - post Query - updatePost Mutation - deletePost Mutation

或在开发服务器启动时报 "Schema validation failed":

gen | Generating TypeScript definitions and GraphQL schemas... gen | 47 files generated api | Building... Took 593 ms api | [GQL Server Error] - Schema validation failed api | ---------------------------------------- api | You must specify one of @requireAuth, @skipAuth or a custom directive for api | - posts Query api | - createPost Mutation api | - updatePost Mutation api | - deletePost Mutation

修正方式:为相应查询与 Mutation 补上恰当的指令。

@requireAuth用于强制认证——在 SDL 中给任何查询或字段加上它即可:

type Mutation { createPost(input: CreatePostInput!): Post! @requireAuth updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth deletePost(id: Int!): Post! @requireAuth }

该指令会调用你应用api/src/lib/auth.{js|ts}中实现的requireAuth()函数,判断用户是否已认证 / 是否具备期望角色。新应用中的auth.ts是桩实现,接入认证提供商后才会执行真正的认证检查:

// ... export const isAuthenticated = (): boolean => { return true // 👈 replace with the appropriate check } // ... export const requireAuth = ({ roles }: { roles: AllowedRoles }) => { if (isAuthenticated()) { throw new AuthenticationError("You don't have permission to do that.") } if (!hasRole({ roles })) { throw new ForbiddenError("You don't have access to do that.") } }

字段级认证@requireAuth不仅能加在查询 / Mutation 上,也能加在任意字段上:

type Post { id: Int! title: String! body: String! @requireAuth authorId: Int! author: User! createdAt: DateTime! }

基于角色的访问控制(RBAC)@requireAuth支持通过roles参数限定允许执行操作的角色:

type Mutation { createPost(input: CreatePostInput!): Post! @requireAuth(roles: ['AUTHOR', 'EDITOR']) updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth(roles: ['EDITOR']) deletePost(id: Int!): Post! @requireAuth(roles: ['ADMIN']) }

@skipAuth用于显式放行公开操作:

type Query { posts: [Post!]! @skipAuth post(id: Int!): Post @skipAuth }

生产环境默认禁用 Introspection 与 Playground

Introspection 允许客户端查询 schema 支持哪些查询,GraphQL Playground 则提供交互式探索 schema 与执行查询的界面。两者都可能泄露关于数据模型、数据、查询与 Mutation 的敏感信息,因此生产部署的最佳实践是禁用它们——Redwood 默认仅在开发环境process.env.NODE_ENV === 'development')启用 Introspection 与 Playground,即 http://localhost:8911/graphql。

确有需要时,可通过createGraphQLHandlerallowIntrospectionallowGraphiQL选项显式开启(见 graphql.md 第 1447 行起):

export const handler = createGraphQLHandler({ authDecoder, getCurrentUser, loggerConfig: { logger, options: {} }, directives, sdls, services, allowIntrospection: true, // 👈 enable introspection in all environments allowGraphiQL: true, // 👈 enable GraphiQL Playground in all environments onException: () => { // Disconnect from your database with an unhandled exception. db.$disconnect() }, })

⚠️警告:在生产环境开启 Introspection 存在安全风险,它允许用户访问 schema、查询与 Mutation 的信息。请谨慎使用并妥善保护你的 GraphQL API。你可能只想开 Introspection 而不开 GraphiQL,或反之——例如只想测试已知查询,而不共享全部可能的操作与类型。

GraphQL Armor:恶意文档的五道闸门

GraphQL Armor 以逐插件方式完全可配置。只需在createGraphQLHandler中提供自定义armorConfig

export const handler = createGraphQLHandler({ authDecoder, getCurrentUser, loggerConfig: { logger, options: {} }, directives, sdls, services, armorConfig, // 👈 custom GraphQL Security configuration onException: () => { // Disconnect from your database with an unhandled exception. db.$disconnect() }, })

例如默认最大查询深度为 6,改为 2 层:

armorConfig: { maxDepth: { n: 2 } },

五道默认开启的防护闸门如下(参数与示例均来自 graphql.md 第 1530–1897 行):

1. Max Aliases(默认启用,默认 15):限制单个文档中的别名数量。别名可重命名查询结果字段,攻击者可用大量别名构造结构被操纵的昂贵查询。配置方式:

{ maxAliases: { enabled: true, n: 15, } }

2. Cost Limit(默认启用,默认 maxCost 5000):对入站查询做成本分析,阻止过于昂贵的请求(DoS 攻击尝试)。成本由字段种类与深度决定:标量字段值 1,对象字段值 2,深度是乘数因子:

COST = FIELD_KIND_COST * (DEPTH * DEPTH_COST_FACTOR) TOTAL_COST = SUM(COST)

默认参数:objectCost: 2scalarCost: 1depthCostFactor: 1.5。若TOTAL_COST超过maxCost,GraphQL 执行被终止并拒绝请求。配置方式:

{ costLimit: { enabled: true, maxCost: 5000, // maximum cost of a request before it is rejected objectCost: 2, // cost of retrieving an object scalarCost: 1, // cost of retrieving a scalar depthCostFactor: 1.5, // multiplicative cost of depth } }

成本计算示例:查询{ profile { me { id user } } }中,两个标量iduser各值 1,处于深度 1(因子 1.5),即 2 × (1 × 1.5) = 3;父对象me值 2,总成本 2 + 3 = 5。注意操作定义(如queryprofile命名)不计入成本。

3. Max Depth(默认启用,默认 6 层):限制文档深度,防范利用 schema 关系构造的"循环查询"(cyclical query)——这类查询层层嵌套author → posts → author → posts …,会耗尽数据库与计算资源。示例中的循环查询深度已达 8:

query cyclical { author(id: 'jules-verne') { posts { author { posts { author { posts { author { ... # more deep nesting! } } } } } } } }

配置方式:

{ maxDepth: { enabled: true, n: 6, } }

4. Max Directives(默认启用,默认 50):限制文档中指令数量。攻击者可组合@include/@skip构造需要大量计算却只返回少量数据的请求。配置方式:

{ maxDirectives: { enabled: true, n: 50, } }

5. Max Tokens(默认启用,默认 1000):限制文档中的 GraphQL 词法单元(token)数量。例如{ me { id user } }的 token 为query{me{iduser}},共 8 个。若配置maxTokens: { n: 2 },将抛出'Syntax Error: Token limit of 2 exceeded, found 3.'。注意:报错中的 found 值并非 token 总数,而是超限那一刻的值,即 n + 1。配置方式:

{ maxTokens: { enabled: true, n: 1000, } }

防信息泄露:字段建议屏蔽与错误掩码

Block Field Suggestions(默认启用):错误请求时 GraphQL 会建议相似字段(如Cannot query field "sta" on type "Media". Did you mean "stats", "staff", or "status"?),这会泄露 schema 结构——即使已禁用 Introspection。默认启用屏蔽,也可自定义掩码:

{ blockFieldSuggestion: { enabled: true, } } // 或自定义掩码 { blockFieldSuggestion: { mask: '<REDACTED>' }, }

Error Masking(错误掩码):许多 GraphQL 服务器会把错误细节泄露给外部——数据库连接失败、特定字段的存在性等都可能被客户端渲染或记录。Redwood 对意外错误开箱即用地屏蔽敏感堆栈信息:原始错误与消息会写入 GraphQL logger 供你排查,响应中则替换为默认消息"Something went wrong"

自定义默认错误消息,可通过createGraphQLHandlerdefaultError设置:

export const handler = createGraphQLHandler({ loggerConfig: { logger, options: {} }, directives, sdls, services, defaultError: 'Sorry about that', // 👈 Customize the error message onException: () => { db.$disconnect() }, })

若想与客户端共享特定错误消息,则使用 Redwood 内置错误(从@redwoodjs/graphql-server导入,灵感源自 Apollo Server Error codes):

  • SyntaxError—— 发生了未指定的错误
  • ValidationError—— 输入到 Service 的数据无效
  • AuthenticationError—— 认证失败
  • ForbiddenError—— 无权访问
  • UserInputError—— 缺少输入到 Service 的数据

使用这些错误时,所提供的消息不会被掩码,而是直接出现在 GraphQL 响应中:

import { UserInputError } from '@redwoodjs/graphql-server' // ... throw new UserInputError('An email is required.')

需要自定义错误(例如集成第三方 API 时控制错误如何呈现给客户端),可继承RedwoodError

export class MyCustomError extends RedwoodError { constructor(message: string, extensions?: Record<string, any>) { super(message, extensions) } }

可选的纵深防御

在 GraphQL Armor 之外,借助 Yoga Envelop Plugin 生态,Redwood 的 GraphQL 端点还可以扩展CSRF 防护速率限制(Rate Limiting)等能力,进一步收口攻击面。

Serverless 函数:开放端点默认裸奔,需要主动收口

认清威胁模型

部署后,自定义 Serverless 函数就是一个开放的 API 端点——任何人都能访问并执行它要求的一切任务。这在很多场景下是合理且期望的行为;但当函数与第三方交互(如发送邮件)或从数据库检索敏感信息时,你需要确保只有来自可信来源的已验证请求才能调用它。某些场景甚至需要限制单位时间内的调用次数以抵御拒绝服务类攻击。

方案一:用 Redwood 用户认证保护函数

Serverless 函数可以复用 GraphQL 指令保护 Service 的同一套用户认证策略——通过useRequireAuth包装器(详见 serverless-functions.md 第 718 行起)。

useRequireAuth配置 handler 的context,使你在函数中可以使用任何requireAuth相关的认证辅助函数。实施步骤:

  1. @redwoodjs/graphql-server导入useRequireAuth
  2. src/lib/auth导入自定义getCurrentUserisAuthenticated检查;
  3. 导入你的认证提供商的authDecoder
  4. 照常实现函数体,但不要导出(如下例的myHandler);
  5. 将实现、getCurrentUserauthDecoder传给useRequireAuth包装器并导出其返回值;
  6. 检查isAuthenticated(),未认证时返回401状态码:
import type { APIGatewayEvent, Context } from 'aws-lambda' import { authDecoder } from '@redwoodjs/auth-dbauth-api' import { useRequireAuth } from '@redwoodjs/graphql-server' import { getCurrentUser, isAuthenticated } from 'src/lib/auth' import { logger } from 'src/lib/logger' const myHandler = async (event: APIGatewayEvent, context: Context) => { logger.info('Invoked myHandler') if (isAuthenticated()) { logger.info('Access myHandler as authenticated user') return { statusCode: 200, headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ data: 'myHandler function', }), } } else { logger.error('Access to myHandler was denied') return { statusCode: 401, } } } export const handler = useRequireAuth({ handlerFn: myHandler, getCurrentUser, authDecoder, })

此后,任何使用context的地方——如 Service 中使用hasRole()isAuthenticated()——currentUser都会被正确设置,requireAuth相关函数能验证认证状态与角色。简言之,isAuthenticated()hasRole()requireAuth()都可以在 Serverless 函数中直接使用。

调用受保护函数时,请求需携带如下请求头(函数没有登录流程,useRequireAuth假定用户已认证并持有 JWT 访问令牌):

Authorization: Bearer myJWT.accesstoken.signature auth-provider: supabase Content-Type: application/json
  • auth-provider:应用使用的认证提供商类型(如dbAuth);
  • Authorization:Bearer 令牌(JWT 访问令牌);
  • 若使用 dbAuth,还需携带 dbAuth Cookie。

官方提示:如果打算实现需要用户认证的功能,优先选择 GraphQL、认证指令与 Service 的组合方案。

方案二:非用户认证场景考虑 Webhooks

如果你需要保护的端点并非基于用户认证,官方建议考虑使用 webhooks.md 中的签名载荷 + 验证器方案(详见下一节)。

其他防护手段

除认证外,日志(Visibility via Logging)、速率限制(Rate Limiting)与白名单(Whitelisting)也是保护函数免遭滥用或误用的常用手段。完整的 "Other security considerations" 章节位于 serverless-functions.md 第 828 行起。

Webhooks:入站要验签,出站要签名

Webhook 是第三方服务在事件发生时通知 RedwoodJS 应用的常见方式——一种消息 / 自动化形式,允许 Web 应用相互通信并在事件发生时实时推送数据。

由于每个 Webhook 都会调用你 API 中的一个函数端点,你必须确保它只在应该运行时运行。这意味着你需要:

  • 验证它来自你预期的地方(Verify it comes from the place you expect)
  • 信任该方(Trust the party)
  • 确认载荷未被篡改(Know the payload sent in the hook hasn't been tampered with)
  • 确保 Webhook 不会被重复处理或重放(Ensure that the hook isn't reprocessed or replayed)

底层实现:@redwoodjs/api/webhooks的验签机制

入站 Webhook 的验签能力由@redwoodjs/api包中的webhooks模块提供,源码位于 packages/api/src/webhooks/index.ts:

  • DEFAULT_WEBHOOK_SIGNATURE_HEADER = 'RW-WEBHOOK-SIGNATURE':默认签名请求头;
  • signatureFromEvent:从 Lambda 事件的指定请求头(默认RW-WEBHOOK-SIGNATURE,大小写不敏感)提取签名;
  • verifyEvent(type, { event, payload, secret, options }):按验证器类型验证事件载荷签名,验证失败时抛出WebhookVerificationErrorsecret默认取DEFAULT_WEBHOOK_SECRET
  • verifySignature:底层签名比对函数;
  • signPayload:出站 Webhook 签名函数。

eventBody内部处理还会检查event.isBase64Encoded,对 base64 编码的载荷先解码再验证——这是兼容网关编码行为的重要细节。

开发 / 测试环境可跳过验证

在测试或开发环境,可以使用skipVerifier,这样不必与团队其他开发者共享密钥。典型做法是设置环境变量WEBHOOK_VERIFICATION=skipVerifier,并在verifyEvent(process.env.WEBHOOK_VERIFICATION, { event })中使用:

import type { APIGatewayEvent } from 'aws-lambda' import { verifyEvent, WebhookVerificationError } from '@redwoodjs/api/webhooks' import { logger } from 'src/lib/logger' export const handler = async (event: APIGatewayEvent) => { const livestormInfo = { webhook: 'livestorm' } const webhookLogger = logger.child({ livestormInfo }) try { verifyEvent('skipVerifier', { event }) const data = JSON.parse(event.body) webhookLogger.debug({ payload: data }, 'Data from Livestorm') return { headers: { 'Content-Type': 'application/json', }, statusCode: 200, body: JSON.stringify({ data }), } } catch (error) { if (error instanceof WebhookVerificationError) { webhookLogger.warn('Unauthorized') return { statusCode: 401, } } else { webhookLogger.error({ error }, error.message) return { headers: { 'Content-Type': 'application/json', }, statusCode: 500, body: JSON.stringify({ error: error.message }), } } } }

出站 Webhook:签名你的载荷

对于出站 Webhook,@redwoodjs/api/webhooks导出的signPayload会使用某种验证方法为载荷签名,生成"Webhook 签名"。拿到签名后,可以按自定义名称将其加入请求的 HTTP 请求头,再发送请求(完整示例见 webhooks.md 第 771 行起)。

密钥与令牌:环境变量的安全管理

认证提供商密钥、数据库连接串、Webhook 签名密钥等敏感信息应通过环境变量管理。RedwoodJS 对 web 与 api 两侧的环境变量加载有明确约定(详见 environment-variables.md):

  • REDWOOD_ENV_前缀声明的变量会被注入 Web 端代码;
  • 非前缀变量仅在 API 端(Serverless 函数)可用,不会暴露给浏览器;
  • .env文件中的变量由 Redwood 在开发与构建时读取,但生产环境的敏感值应由部署平台的密钥管理能力注入。

原则:密钥永不落入客户端代码,API 端密钥通过部署平台的安全机制注入,Web 端只保留经过REDWOOD_ENV_显式标记的非敏感配置。

安全基线小结

结合 security.md 与仓库源码,RedwoodJS 应用的安全基线可归纳为一张自检清单:

攻击面框架默认机制开发者落实事项
认证@redwoodjs/auth多提供商封装、useAuthPrivateSet路由守卫实现getCurrentUser/requireAuth,选择并配置认证提供商
GraphQL指令鉴权(@requireAuth默认)、GraphQL Armor 五道闸门、Introspection/Playground 生产禁用、错误掩码为所有操作声明恰当的指令;按需调整armorConfig;谨慎开启生产 Introspection
Serverless 函数useRequireAuth包装器、Webhook 验签、日志 / 限流 / 白名单建议按威胁模型为每个开放端点选择防护方案
WebhooksverifyEvent/signPayload/skipVerifier,默认签名头RW-WEBHOOK-SIGNATURE管理好签名密钥,确认来源、信任方、载荷完整性与防重放
密钥管理.env+REDWOOD_ENV_前缀约定使用密码管理器与 Doppler 等环境管理服务,杜绝密钥进代码库

RedwoodJS 的哲学是"默认安全、显式放开":生成即带@requireAuth、生产即禁 Introspection、GraphQL 端点即配 Armor。剩下的工作——选择认证策略、维护密钥、为每个端点评估威胁模型——则是每位 RedwoodJS 开发者必须亲自完成的安全功课。沿着本文引用的 graphql.md、serverless-functions.md、webhooks.md 与 authentication.md 继续深入,即可把每道防线打磨到生产级。

  • 后端
  • 前端
  • Web框架
  • 开发工具

【免费下载链接】redwood

RedwoodGraphQL

项目地址:https://gitcode.com/gh_mirrors/re/redwood
点击查看免费下载

相关推荐

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

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

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

立即咨询