Chapter 07

认证与权限控制

构建安全的 GraphQL API——JWT 认证、字段级权限与 RBAC

Context 中的用户注入

import jwt from 'jsonwebtoken';

interface Context {
  db: PrismaClient;
  user: User | null;
  loaders: DataLoaders;
}

async function getUserFromToken(token: string | undefined): Promise<User | null> {
  if (!token?.startsWith('Bearer ')) return null;
  try {
    const decoded = jwt.verify(token.slice(7), process.env.JWT_SECRET!) as { userId: string };
    return db.users.findUnique({ where: { id: decoded.userId } });
  } catch {
    return null;
  }
}

await startStandaloneServer(server, {
  context: async ({ req }) => ({
    db,
    user: await getUserFromToken(req.headers.authorization),
    loaders: createLoaders(db),
  }),
});

在 Resolver 中检查权限

import { GraphQLError } from 'graphql';

function requireAuth(user: User | null): User {
  if (!user) {
    throw new GraphQLError('Must be authenticated', {
      extensions: { code: 'UNAUTHENTICATED' },
    });
  }
  return user;
}

function requireRole(user: User | null, role: UserRole): User {
  const authedUser = requireAuth(user);
  if (authedUser.role !== role && authedUser.role !== 'ADMIN') {
    throw new GraphQLError('Not authorized', {
      extensions: { code: 'FORBIDDEN' },
    });
  }
  return authedUser;
}

const resolvers = {
  Query: {
    // 公开查询
    posts: (_, __, { db }) => db.posts.findMany({ where: { published: true } }),

    // 需要认证
    me: (_, __, { user }) => requireAuth(user),

    // 需要特定角色
    adminDashboard: (_, __, { user }) => {
      requireRole(user, 'ADMIN');
      return { /* ... */ };
    },
  },
  Mutation: {
    deleteUser: async (_, { id }, { user, db }) => {
      const authed = requireRole(user, 'ADMIN');
      await db.users.delete({ where: { id } });
      return true;
    },
  },
};

graphql-shield:声明式权限规则

import { rule, shield, and, or, not } from 'graphql-shield';

// 定义规则
const isAuthenticated = rule({ cache: 'contextual' })(
  async (parent, args, ctx) => ctx.user !== null
);

const isAdmin = rule({ cache: 'contextual' })(
  async (parent, args, ctx) => ctx.user?.role === 'ADMIN'
);

const isPostOwner = rule({ cache: 'strict' })(
  async (parent, args, ctx) => {
    const post = await ctx.db.posts.findUnique({ where: { id: args.id } });
    return post?.authorId === ctx.user?.id;
  }
);

// 应用权限到 Schema
const permissions = shield({
  Query: {
    '*': isAuthenticated,      // 默认所有 Query 需要认证
    posts: not(isAuthenticated), // 覆盖:posts 公开
    adminStats: isAdmin,
  },
  Mutation: {
    createPost: isAuthenticated,
    updatePost: and(isAuthenticated, isPostOwner),
    deletePost: or(isAdmin, isPostOwner),
  },
  User: {
    // 字段级权限:email 只有本人和 ADMIN 可见
    email: or(isAdmin, rule()(async (parent, _, ctx) => parent.id === ctx.user?.id)),
  },
});

// 将权限中间件应用到 Schema
const schemaWithPermissions = applyMiddleware(schema, permissions);

JWT 登录 Mutation

const resolvers = {
  Mutation: {
    login: async (_, { email, password }, { db }) => {
      const user = await db.users.findUnique({ where: { email } });
      if (!user) throw new GraphQLError('Invalid credentials', {
        extensions: { code: 'UNAUTHENTICATED' }
      });

      const valid = await bcrypt.compare(password, user.passwordHash);
      if (!valid) throw new GraphQLError('Invalid credentials', {
        extensions: { code: 'UNAUTHENTICATED' }
      });

      const token = jwt.sign(
        { userId: user.id },
        process.env.JWT_SECRET!,
        { expiresIn: '7d' }
      );
      return { token, user };
    },
  },
};
GraphQL 安全最佳实践

认证 vs 授权:概念区分

认证(Authentication)
"你是谁?"——验证用户身份的过程。GraphQL 中通常在 context 函数中完成:解析 HTTP 请求头中的 JWT Token,验证签名和有效期,提取用户 ID 并从数据库加载用户信息,注入到 context.user 中。
授权(Authorization)
"你能做什么?"——检查已认证用户是否有权限执行某操作。GraphQL 中可以在 Resolver 内部(命令式)或通过 graphql-shield 中间件(声明式)实现,支持操作级和字段级两种粒度。
RBAC(基于角色的访问控制)
Role-Based Access Control:用户被分配角色(USER/EDITOR/ADMIN),权限绑定在角色上而非用户上。GraphQL 中通常将角色存入 context.user.role,Resolver 检查角色而非具体权限,便于维护。

查询复杂度限制(防 DoS 攻击)

import { createComplexityLimitRule } from 'graphql-validation-complexity';
import depthLimit from 'graphql-depth-limit';

// GraphQL 查询可以无限嵌套,恶意查询可能导致服务崩溃:
// query { users { posts { author { posts { author { posts { ... } } } } } } }

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    // 限制查询深度(防止无限嵌套)
    depthLimit(7),

    // 限制查询复杂度(每个字段计分,超出阈值拒绝)
    createComplexityLimitRule(1000, {
      scalarCost: 1,       // 标量字段:1分
      objectCost: 2,       // 对象字段:2分
      listFactor: 10,      // 列表字段乘以10(可能返回大量数据)
    }),
  ],

  // 生产环境禁用 introspection(防止 Schema 信息泄漏)
  introspection: process.env.NODE_ENV !== 'production',
});

Resource-Level 权限:资源所有权校验

const resolvers = {
  Mutation: {
    updatePost: async (_, { id, data }, { user, db }) => {
      const authed = requireAuth(user);

      // 加载资源并校验所有权
      const post = await db.posts.findUnique({ where: { id } });
      if (!post) {
        throw new GraphQLError('Post not found', {
          extensions: { code: 'NOT_FOUND' }
        });
      }

      // 只有作者或 ADMIN 才能修改
      if (post.authorId !== authed.id && authed.role !== 'ADMIN') {
        throw new GraphQLError('Not authorized to update this post', {
          extensions: { code: 'FORBIDDEN' }
        });
      }

      return db.posts.update({ where: { id }, data });
    },
  },
};

本章小结

本章核心要点