graphql 基于角色的授权

Posted

技术标签:

【中文标题】graphql 基于角色的授权【英文标题】:graphql role based authorization 【发布时间】:2018-07-21 23:41:54 【问题描述】:

我是 GraphQL 新手,打算使用 GraphQL 构建解决方案。

一切看起来都很酷,但只关心如何在 GraphQL 服务器中实现基于角色的授权(我正在考虑使用 GraphQL.js/apollo 服务器)

我将有一个包含所有用户的用户表。在 users 表中有一个角色字段,其中包含特定用户的角色。查询和变更将根据用户的角色授予。

如何实现这个结构?

谢谢!

【问题讨论】:

【参考方案1】:

对于 apollo 服务器开发者来说,Graphql 中实现授权的方式一般有 3 种:

    基于模式:向您要保护的 graphql 类型和字段添加指令

    基于中间件:添加中间件(在您的 graphql 解析器执行之前和之后运行的代码)。这是graphql-shield 和其他建立在graphql-middleware 之上的授权库所使用的方法。

    业务逻辑层:这是最原始但最细粒度的方法。基本上,返回数据的函数(即数据库查询等)将实现自己的权限/授权检查。

基于架构

    通过基于模式的授权,我们将定义自定义模式指令并将它们应用到任何适用的地方。

来源:https://www.apollographql.com/docs/graphql-tools/schema-directives/

//schema.gql

directive @auth(
  requires: Role = ADMIN,
) on OBJECT | FIELD_DEFINITION

enum Role 
  ADMIN
  REVIEWER
  USER
  UNKNOWN


type User @auth(requires: USER) 
  name: String
  banned: Boolean @auth(requires: ADMIN)
  canPost: Boolean @auth(requires: REVIEWER)

// main.js

class AuthDirective extends SchemaDirectiveVisitor 
  visitObject(type) 
    this.ensureFieldsWrapped(type);
    type._requiredAuthRole = this.args.requires;
  

  visitFieldDefinition(field, details) 
    this.ensureFieldsWrapped(details.objectType);
    field._requiredAuthRole = this.args.requires;
  

  ensureFieldsWrapped(objectType) 
    if (objectType._authFieldsWrapped) return;
    objectType._authFieldsWrapped = true;

    const fields = objectType.getFields();

    Object.keys(fields).forEach(fieldName => 
      const field = fields[fieldName];
      const  resolve = defaultFieldResolver  = field;
      field.resolve = async function (...args) 
        // Get the required Role from the field first, falling back
        // to the objectType if no Role is required by the field:
        const requiredRole =
          field._requiredAuthRole ||
          objectType._requiredAuthRole;

        if (! requiredRole) 
          return resolve.apply(this, args);
        

        const context = args[2];
        const user = await getUser(context.headers.authToken);
        if (! user.hasRole(requiredRole)) 
          throw new Error("not authorized");
        

        return resolve.apply(this, args);
      ;
    );
  


const schema = makeExecutableSchema(
  typeDefs,
  schemaDirectives: 
    auth: AuthDirective,
    authorized: AuthDirective,
    authenticated: AuthDirective
  
);

基于中间件

    使用基于中间件的授权,大​​多数库都会拦截解析器的执行。以下示例特定于apollo-server 上的graphql-shield

Graphql-shield 来源:https://github.com/maticzav/graphql-shield

apollo-server 源码的实现:https://github.com/apollographql/apollo-server/pull/1799#issuecomment-456840808

// shield.js

import  shield, rule, and, or  from 'graphql-shield'

const isAdmin = rule()(async (parent, args, ctx, info) => 
  return ctx.user.role === 'admin'
)

const isEditor = rule()(async (parent, args, ctx, info) => 
  return ctx.user.role === 'editor'
)

const isOwner = rule()(async (parent, args, ctx, info) => 
  return ctx.user.items.some(id => id === parent.id)
)

const permissions = shield(
  Query: 
    users: or(isAdmin, isEditor),
  ,
  Mutation: 
    createBlogPost: or(isAdmin, and(isOwner, isEditor)),
  ,
  User: 
    secret: isOwner,
  ,
)

// main.js

const  ApolloServer, makeExecutableSchema  = require('apollo-server');
const  applyMiddleware  = require('graphql-middleware');
const shieldMiddleware = require('shieldMiddleware');

const schema = applyMiddleware(
  makeExecutableSchema( typeDefs: '...', resolvers: ... ),
  shieldMiddleware,
);
const server = new ApolloServer( schema );
app.listen( port: 4000 , () => console.log('Ready!'));

业务逻辑层

    通过业务逻辑层授权,我们将在解析器逻辑中添加权限检查。这是最乏味的,因为我们必须在每个解析器上编写授权检查。下面的链接建议将授权逻辑放在业务逻辑层(即有时称为“模型”或“应用程序逻辑”或“数据返回函数”)。

来源:https://graphql.org/learn/authorization/

选项 1:解析器中的身份验证逻辑

// 解析器.js

const Query = 
  users: function(root, args, context, info)
    if (context.permissions.view_users) 
      return ctx.db.query(`SELECT * FROM users`)
    
    throw new Error('Not Authorized to view users')
  

选项 2(推荐):从解析器中分离出授权逻辑

// 解析器.js

const Authorize = require('authorization.js')

const Query = 
  users: function(root, args, context, info)
    Authorize.viewUsers(context)
  

//授权.js

const validatePermission = (requiredPermission, context) => 
  return context.permissions[requiredPermission] === true


const Authorize = 
  viewUsers = function(context)
    const requiredPermission = 'ALLOW_VIEW_USERS'

    if (validatePermission(requiredPermission, context)) 
      return context.db.query('SELECT * FROM users')
    

    throw new Error('Not Authorized to view users')
  ,
  viewCars = function(context)
     const requiredPermission = 'ALLOW_VIEW_CARS';

     if (validatePermission(requiredPermission, context))
       return context.db.query('SELECT * FROM cars')
     

     throw new Error('Not Authorized to view cars')
  

【讨论】:

graphql-shield 有一些限制,如果任何单个权限失败,它往往会阻止/杀死整个查询。例如,可用的功能是allow / deny。它(到目前为止)无法allowSome。例如,如果某些权限通过而某些权限失败,则它不能选择性地允许部分查询成功。【参考方案2】:

我最近使用GraphQL Shield 实现了基于角色的授权,我发现使用该包是最简单的方法。否则,您可以添加自定义架构指令,这是一篇关于如何做到这一点的好文章:https://dev-blog.apollodata.com/reusable-graphql-schema-directives-131fb3a177d1。

您需要执行几个步骤来设置 GraphQL Shield:

1 - 编写一个身份验证函数,这是一个粗略的示例,您需要做的远不止这些,即使用 JWT 而不传递 id:

export const isAdmin = async ( id ) => 
  try 
    const exists = await ctx.db.exists.User(
      id: userId,
      role: 'ADMIN',
    );

    return exists
   catch (err) 
    console.log(err);
    return false
  

2 - 在导出所有突变和查询的文件中添加检查:

const resolvers = 
 ...your queries and mutations


const permissions = 
   Query: 
     myQuery: isAdmin
   


export default shield(resolvers, permissions);

现在,每次请求您的查询时,这将是 isAdmin 函数。

希望对你有帮助

【讨论】:

以上是关于graphql 基于角色的授权的主要内容,如果未能解决你的问题,请参考以下文章

GraphQL 和 Laravel 如何进行授权?

使用 @PreAuthorize 的 GraphQL 和 Spring Security?

GraphQL 服务器中的授权

前端的用户角色/权限 - React / GraphQL / Apollo Client

基于字段值的 AWS Amplify GraphQL 授权

公共 GraphQL 指令