基于GraphQL与Serverless的微服务架构授权方案咨询
Great question! Given your setup—isolated Serverless services per DynamoDB table, schema stitching via graphql-tools at API Gateway, and a custom authorizer already injecting the userId into the GraphQL context—here’s a clean, modular approach to implement authorization at the gateway layer while keeping your downstream services completely focused on business logic:
1. Build Custom Authorization Directives
GraphQL directives are perfect for declarative, modular authorization. Define reusable directives at the gateway layer to mark which operations/fields need authorization checks, then wire up logic to enforce them during schema stitching.
For example, create directives like:
@requiresAuth: Ensures the user is authenticated (i.e.,userIdexists in the context)@requiresOwnership(resourceField: String!): Ensures the authenticated user owns the target resource (matchesuserIdto a field on the resource)
Here’s a snippet of how to define these in your gateway schema:
directive @requiresAuth on QUERY | MUTATION | FIELD_DEFINITION directive @requiresOwnership(resourceField: String!) on QUERY | MUTATION | FIELD_DEFINITION
2. Centralize Authorization Logic in Reusable Utilities
Create a dedicated authorization utility file at the gateway layer to house all your permission-checking logic. This keeps your code DRY and makes it easy to update rules without touching downstream services.
Example utility functions:
// gateway/utils/authorization.js export const isAuthenticated = (context) => { return !!context.userId; }; export const ownsResource = async (context, resourceId, resourceField = 'userId') => { // Fetch the resource from the appropriate downstream service (or cache if possible) const resource = await fetchResourceById(resourceId); return resource[resourceField] === context.userId; };
3. Use Directive Transformers to Enforce Rules During Schema Stitching
Leverage graphql-tools’ transformer capabilities to wrap resolvers of fields/operations marked with your authorization directives. This happens at the gateway level, so downstream resolvers never even see the authorization logic.
Here’s how to wire this up:
// gateway/schema-stitching.js import { stitchSchemas, wrapResolver } from '@graphql-tools/stitch'; import { isAuthenticated, ownsResource } from './utils/authorization'; const authDirectiveTransformer = (schema) => { return schema.transform({ resolver: (resolverConfig, fieldName, typeName) => { const fieldDirectives = resolverConfig.astNode?.directives; // Check for @requiresAuth const requiresAuth = fieldDirectives?.some(d => d.name.value === 'requiresAuth'); if (requiresAuth) { resolverConfig.resolve = wrapResolver(resolverConfig.resolve, async (args, context, info) => { if (!isAuthenticated(context)) { throw new Error('Unauthorized: You must be logged in'); } return resolverConfig.resolve(args, context, info); }); } // Check for @requiresOwnership const ownershipDirective = fieldDirectives?.find(d => d.name.value === 'requiresOwnership'); if (ownershipDirective) { const resourceField = ownershipDirective.arguments?.find(a => a.name.value === 'resourceField').value.value; resolverConfig.resolve = wrapResolver(resolverConfig.resolve, async (args, context, info) => { if (!isAuthenticated(context)) { throw new Error('Unauthorized: You must be logged in'); } const resourceId = args.id; // Adjust based on your argument naming const hasOwnership = await ownsResource(context, resourceId, resourceField); if (!hasOwnership) { throw new Error('Unauthorized: You do not own this resource'); } return resolverConfig.resolve(args, context, info); }); } return resolverConfig; } }); }; // Apply the transformer when stitching schemas const stitchedSchema = stitchSchemas({ subschemas: [/* your downstream service schemas */], transforms: [authDirectiveTransformer] });
4. Enforce Granular Permissions Per Operation/Field
Mark your stitched schema operations with the directives to enforce granular rules. For example:
type Query { getPrivateUserProfile(userId: ID!): User @requiresAuth getPost(postId: ID!): Post @requiresOwnership(resourceField: "authorId") } type Mutation { updateUserProfile(input: UpdateProfileInput!): User @requiresAuth deletePost(postId: ID!): Post @requiresOwnership(resourceField: "authorId") }
Key Benefits of This Approach
- Downstream Service Purity: Your table-specific Serverless services only handle data access and business logic—no authorization code cluttering them up.
- Modular & Maintainable: All authorization rules live in one place at the gateway, making it easy to update or add new permissions.
- Declarative & Clear: Directives make authorization requirements explicit in your schema, which is great for documentation and developer clarity.
- Scalable: As you add more downstream services, you just apply the same directives to new operations without rewriting authorization logic.
内容的提问来源于stack exchange,提问作者MikeyCarlstein

