基于AWS、Stripe与React的订阅服务GraphQL权限控制咨询
仅允许订阅用户调用特定GraphQL接口的实现方案
结合你用React+Stripe+AWS Cognito+DynamoDB+GraphQL的技术栈,核心思路是在后端GraphQL层面强制校验用户订阅状态,同时确保Stripe的订阅状态能实时同步到AWS生态,避免未订阅用户绕过前端限制调用敏感接口。以下是具体落地步骤:
1. 同步Stripe订阅状态到AWS
首先要确保用户的订阅状态(活跃/过期/取消)能从Stripe同步到Cognito或DynamoDB,这是权限校验的基础:
- 配置Stripe Webhook,监听
customer.subscription.created、customer.subscription.updated、customer.subscription.deleted这几个核心事件 - 用AWS Lambda处理Webhook请求,根据Stripe事件更新用户的订阅状态:
- 方案一:更新Cognito用户池的自定义属性(比如
custom:subscribed,布尔值类型),调用Cognito的adminUpdateUserAttributesAPI实现 - 方案二:更新DynamoDB的用户表,添加
subscribed字段存储状态
注意:创建Stripe订阅时,要把Cognito用户的
sub(唯一标识)存在Stripe订阅的metadata里,这样Webhook能关联到对应的AWS用户 - 方案一:更新Cognito用户池的自定义属性(比如
示例Lambda处理Stripe Webhook代码:
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const { CognitoIdentityServiceProvider } = require('aws-sdk'); const cognito = new CognitoIdentityServiceProvider(); exports.handler = async (event) => { const stripeEvent = stripe.webhooks.constructEvent( event.body, event.headers['Stripe-Signature'], process.env.STRIPE_WEBHOOK_SECRET ); const subscription = stripeEvent.data.object; const userId = subscription.metadata.userId; // 从Stripe元数据拿Cognito用户ID switch (stripeEvent.type) { case 'customer.subscription.created': case 'customer.subscription.updated': await cognito.adminUpdateUserAttributes({ UserPoolId: process.env.COGNITO_POOL_ID, Username: userId, UserAttributes: [ { Name: 'custom:subscribed', Value: subscription.status === 'active' ? 'true' : 'false' } ] }).promise(); break; case 'customer.subscription.deleted': await cognito.adminUpdateUserAttributes({ UserPoolId: process.env.COGNITO_POOL_ID, Username: userId, UserAttributes: [ { Name: 'custom:subscribed', Value: 'false' } ] }).promise(); break; } return { statusCode: 200, body: 'OK' }; };
2. 在GraphQL层面添加订阅状态校验
不管你用AWS AppSync还是自建Apollo Server,都要在resolver(解析器)层面添加校验逻辑,这是阻止未订阅用户的核心屏障:
方案A:AWS AppSync + VTL解析器
给需要保护的GraphQL操作(比如getPremiumContent)配置自定义解析器,先校验订阅状态:
- 第一步查询用户的订阅状态(从Cognito或DynamoDB)
#set($userSub = $context.identity.sub) #set($getUser = { "version": "2018-05-29", "operation": "GetItem", "key": { "userId": { "S": $userSub } } }) #return($util.toJson($getUser))
- 第二步判断状态,不满足则抛出错误
#if($context.result.subscribed != true) $util.error("仅订阅用户可访问此资源") #end #set($result = $util.dynamodb.toMapValues($ctx.result)) #return($util.toJson($result))
方案B:自建GraphQL服务器(比如Apollo Server部署在Lambda)
自定义一个权限校验中间件或指令,标记需要订阅权限的字段:
const { ApolloServer, gql, AuthenticationError } = require('apollo-server-lambda'); const { DynamoDB } = require('aws-sdk'); const db = new DynamoDB.DocumentClient(); // 自定义指令:标记需要订阅权限的字段 const requiresSubscription = async (resolve, parent, args, context) => { const userId = context.identity.sub; const user = await db.get({ TableName: 'Users', Key: { userId } }).promise(); if (!user.Item || !user.Item.subscribed) { throw new AuthenticationError('仅订阅用户可执行此操作'); } return resolve(parent, args, context); }; const typeDefs = gql` directive @requiresSubscription on FIELD_DEFINITION type Query { getPremiumContent: Content @requiresSubscription publicContent: Content } `; const resolvers = { Query: { getPremiumContent: () => { /* 实际业务逻辑 */ }, publicContent: () => { /* 实际业务逻辑 */ } } }; const server = new ApolloServer({ typeDefs, resolvers, schemaDirectives: { requiresSubscription }, context: ({ event }) => ({ identity: event.requestContext.identity // 从Lambda事件中拿Cognito身份信息 }) });
3. 关于Cognito自定义令牌字段的补充
你提到的自定义访问令牌字段方案是可行的,但要注意令牌时效性问题:
- Cognito访问令牌默认有效期1小时,若用户订阅状态在令牌有效期内变化,令牌里的
custom:subscribed字段不会自动更新 - 解决办法:当Stripe同步状态后,前端可以监听Stripe的webhook或状态变化,调用Cognito的刷新令牌接口(
refreshToken)获取新的令牌,确保前端和后端的状态一致 - 这个方案的优势是:GraphQL解析器可以直接从
$context.identity.claims['custom:subscribed']获取状态,无需额外查询数据库,性能更高
4. 前端辅助拦截(仅做体验优化)
前端可以根据令牌里的custom:subscribed字段,隐藏/禁用需要订阅权限的操作按钮,但绝对不能依赖前端作为唯一的权限控制——因为前端代码可以被篡改,后端校验才是最后防线。
兜底:防止未订阅用户调用成功的关键
- 所有需要订阅权限的GraphQL操作,必须在resolver执行业务逻辑前强制校验状态
- 禁止依赖前端传递的订阅状态参数,必须从后端存储(Cognito/DynamoDB)获取真实状态
- 定期测试未订阅用户的调用场景,确保权限拦截生效
内容的提问来源于stack exchange,提问作者user16257618
相关产品推荐
相关产品推荐

