You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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的adminUpdateUserAttributes API实现
    • 方案二:更新DynamoDB的用户表,添加subscribed字段存储状态

    注意:创建Stripe订阅时,要把Cognito用户的sub(唯一标识)存在Stripe订阅的metadata里,这样Webhook能关联到对应的AWS用户

示例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)配置自定义解析器,先校验订阅状态:

  1. 第一步查询用户的订阅状态(从Cognito或DynamoDB)
#set($userSub = $context.identity.sub)
#set($getUser = {
  "version": "2018-05-29",
  "operation": "GetItem",
  "key": { "userId": { "S": $userSub } }
})
#return($util.toJson($getUser))
  1. 第二步判断状态,不满足则抛出错误
#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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 09:43:39