You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
实现思路:为GraphQL Schema中定义的Object Type,比如User, Post, Config,创建一个单例Controller,比如UserController,PostController,当然叫Controller还是其他名称都可以,示例代码主要是参考使用Express.js做开发时,我们给路由定义的路由controller。那么我将resolver作为"路由"层,并为每个"路由"定义controller。
UserController.ts
import{Role}from'../db';import{auth}from'./decorator';import{defaultFieldResolver}from'graphql';classUserController{
@auth({roles: [Role.admin,Role.editor,Role.viewer]})publicstaticuser(_,{ id },{ db }){returndb.users.find((user)=>user.id.toString()===id);}
@auth({roles: [Role.admin]})publicstaticadminUsers(_,__,{ db }){returndb.users.find((user)=>user.role===Role.admin);}
@auth({roles: [Role.admin]})publicstaticcreateUser(_,{ input },{ db }){constuser={id: db.users.length,
...input,};db.users.push(user);return{code: 0,message: 'ok'};}
@auth({roles: [Role.admin]})publicstaticbitcoinAddress(_,__,___){returndefaultFieldResolver;}privateconstructor(){}}export{UserController};
在GraphQL中实现用户认证和授权的5种方式
前言
用户的认证和授权是大多数web服务具备的功能,对于提供RESTful API的web服务,以Node.js社区的web框架express.js为例,基本流程是将用户认证和授权的功能实现放在中间件(middleware)中,比如express-jwt中间件,使用基于JWT的用户认证,该中间件会验证和解析客户端发送的请求的请求头上设置的
Authorization: Bearer <jwt>
字段,并将解析出来的用户信息设置在请求对象req
上,在下一个中间件或者我们定义的路由controller中,可以拿到该用户信息,比如id和email,通过id或email查询数据库,从而获取用户角色role,进行权限验证。相关的文档已经很多,不再叙述。这篇文章主要讲构建GraphQL Web服务时,由于其不同于传统RESTful API的路由和中间件定义方式,严格意义上没有路由的概念,所以技术实现上不同于使用RESTful架构风格构建的web服务。不过很多原理和思想依旧是想通的,希望这篇文章能给开发者们一个启发。
阅读本文需要熟练掌握以下编程技术,思想,模式:
解决方案
首先使用可视化工具给示例代码的GraphQL Schema生成一份可视化图表,可以直观的看到定义的GraphQL Schema及其关系。
Query Type中定义了4个查询字段,分别是
user
,posts
,adminUsers
,config
,可以理解为定义了4个查询API。再来看下数据库中user的角色role,为了简单起见,示例代码构造了一个简单的内存数据库。user的role:现在需要对这4个接口进行认证和授权,规则如下:
user
: 访问需要用户的角色为viewer
或editor
或admin
posts
: 公开的API,不需要用户认证和授权,任何互联网上的用户都能访问adminUsers
: 访问需要用户角色为admin
config
: 访问需要用户的角色为viewer
或editor
或admin
访问上述4个查询API的用户权限规则很容易理解,目前我们定义的用户权限规则都是在API级别上的,下面来看定义在GraphQL Schema中User这个Object Type上字段级别的用户权限规则:
User.bitcoinAddress
: 客户端发送GraphQL查询,查询GraphQL Schema定义的User上的bitcoinAddress
字段,需要admin
的用户权限。介绍完我们要实现的用户认证和授权规则,接下来设计数据库表和关系,并插入一些测试数据,内存数据库如下:
GraphQL Schema:
GraphQL Resolvers:
server.ts
:示例代码在
contextFunction
中模拟JWT的认证过程,const token = req.get('authorization');
语句表示校验并解析出用户信息,我在GraphQL客户端中设置内存数据库中用户的id,用来模拟JSON web token。const user = db.users.find((u) => u.id.toString() === token);
语句表示根据用户id查询数据库,获取user。req.user = user;
语句表示将查询出的user添加到req
请求对象上,由于apollo-server
默认做了和express.js框架的集成,所以该req
请求对象就是express.js提供的req
。return { db, req };
语句表示将内存数据库db和req
对象注入到context
中并返回。const server = new ApolloServer({ schema, context: contextFunction });
语句表示实例化apollo server,传入GraphQL Schema和contextFunction
,contextFunction
会在每次客户端有请求访问GraphQL web服务时执行一次,因此每次请求的req
对象都是新的,代表不同的客户端请求。context
对象会被传入GraphQL的所有resolver函数。我们就可以通过context
获取内存数据库db和req
请求对象。1. 使用GraphQL Middleware
prisma社区开源了一个叫做graphql-middleware的工具,该工具的工作原理是,我们可以将定义好的中间件在GraphQL resolver函数被调用之前或之后运行,使用中间件这种方式可以改善代码结构,提高代码的可读性,可维护性,可扩展性,提升代码复用能力(我们将公共代码逻辑放入中间件中,而不是在每个GraphQL resolver中都写一遍),以及清晰的职责分离。
Authentication和Authorization这两种场景是非常适合使用中间件的,定义好的auth middleware代码如下:
首先定义了
resolverAuthMap
变量,其结构和前面定义的GraphQL resolver结构保持一致。定义了每个resolver需要的用户权限规则,不同角色role之间通过:
符号分割,当然也可以定义为数组形式,如['viewer', 'editor', 'admin']
。紧接着定义
authMiddleware
,函数签名中的resolve
表示GraphQL resolver函数,比如开发者定义的resolver函数Query.user
,Query.posts
,Post.author
, 以及GraphQL Scalar Type默认的resolver。因此,就可以在每个GraphQL resolver函数被调用之前或之后加入我们自己的代码逻辑,这里就是用户认证和授权的代码。接着看,
authMiddleware
函数签名中的parent
,args
,context
和info
和resolver函数签名一致。具体解释请看apollographql社区提供的解释说明。通过
info.parentType.name
,可以获取到当前执行的resolver函数父级类型的名称,info.fieldName
表示当前resolver函数解析的字段名称,这就是为什么resolverAuthMap
变量的数据结构要和resolver的结构匹配,可以获取到API级别resolver或者字段级别resolver需要的相应的用户权限规则,即view:editor:admin
,admin
等。稍后将通过例子来更直观的说明。通过
context.req
可以获取到数据库中查询出来的user
, 上述代码38-40行,判断用户是否存在(没有经过JWT认证的用户不存在),以及是否有足够的权限访问该resolver。接下来创建GraphQL Schema,并应用创建好的
authMiddleware
:至此,GraphQL Server端的关键代码说明完毕。开始在客户端发起GraphQL查询,进行验证(手动e2e测试)。测试1: 不设置请求头,用来模拟没有进行JWT认证的用户:
查询
user
,查询和返回结果如下:访问
user
接口需要view
,editor
,admin
权限,用户没有经过JWT认证,因此返回no permission
错误。GraphQL Server端authMiddleware
中打印的日志如下:测试通过。
测试2: 设置请求头,模拟经过JWT认证的用户,并且用户的role是
viewer
用户id为2的用户有
viewer
权限,查询user
,查询和返回结果如下:server端日志如下:
测试通过。
测试3: 拥有
viewer
权限的用户访问GraphQL Schema定义的User上的bitcoinAddress
字段,访问该字段需要用户拥有admin
权限。查询
user
,查询和返回结果如下:这里GraphQL给出了部分失败(partial failures)的返回结果,即有访问权限的接口
user
和字段id
,name
,email
,role
正常返回,没有访问权限的字段bitcoinAddress
返回null
,并通过errors
和path
字段给出错误字段解析路径。测试通过。
测试4: 拥有
viewer
权限的用户访问需要admin
权限的adminUser
接口查询和返回结果如下:
server端日志:
得到
no permission
错误,测试通过。其他接口测试类似,这里不再测试。基于graphql-middleware的认证与授权,可以使用已有的轮子graphql-shield package.
2. 使用GraphQL Schema Directive
关于GraphQL Schema Directive的概念这里不再介绍,直接来看示例代码,相比于第一种方式,这种方式需要修改GraphQL Schema,添加了用户认证和授权
directive
的GraphQL Schema定义如下:代码第4行定义一个
auth
directive
,该directive
接收一个requires
参数,类型是GraphQL枚举类型Role的数组。OBJECT
表示该指令可以标记在GraphQL Object Type上,如type User
,type Post
,也可以标记在字段级别上,对字段级别进行权限校验,如Query.user
,Query.adminUsers
,User.bitcoinAddress
。示例代码将auth
指令都标记在了字段级别。auth
指令实现如下:代码24行,我们可以获取到标记了
auth
指令的字段及其原始的resolver。代码25-40行,重写该字段的resolver,加入用户权限校验代码逻辑。生成GraphQL Schema:测试结果和和方式1一样,这里不再给出。
3. 使用组合Resolver
通过编写组合函数
combineResolvers
,可以组合多个resolver,最后返回一个resolver,这些resolver会使用相同的参数依次被调用,直到某个resolver函数的返回值不是undefined
,该返回值会被立即返回,排在该resolver后面的resolver函数将被忽略。combineResolvers
实现如下:改造后的resolver如下:
isAuthenticated
resolver函数用来判断进行用户认证,isAuthorized
resolver函数用来进行用户权限校验。这种方式很像express.js框架中间件的使用方式,这里的
skip
变量就类似于next()
函数,用来调用下一个中间件。开源社区已经有人写了一些帮助函数,graphql-resolvers,源码很容易理解。4. 使用Decorator(monkey patch)
我们可以利用类和装饰器(decorator)的方式,实现用户认证和授权,装饰器模式可以使我们在不侵入原有代码逻辑的情况下,给代码增加额外的功能,拥有良好的可读性,可维护性和可扩展性,代码重用及职责分离。
实现思路:为GraphQL Schema中定义的Object Type,比如
User
,Post
,Config
,创建一个单例Controller
,比如UserController
,PostController
,当然叫Controller
还是其他名称都可以,示例代码主要是参考使用Express.js做开发时,我们给路由定义的路由controller。那么我将resolver作为"路由"层,并为每个"路由"定义controller。UserController.ts
在
UserController
类中定义的静态方法命名规则保持和resolver函数要解析的字段名称一致,这里为了简单起见,只定义了controller层,如果项目变大,逻辑变复杂,要使用分层架构,我们可以引入service layer,data access layer等,通过context
注入到controller的各个方法(实际上就是resolver)中。改造后的resolver如下:
装饰器实现如下:
测试结果一样,这里不再给出。
5. 直接在resolver中加入认证和授权逻辑
这种方式具有侵入性,不符合开闭原则,如下:
authService.auth(req.user);
语句可能会在每个resolver中都出现,导致代码重复,尽管我们将用户认证和授权逻辑放在authService.auth
方法中,但调用authService.auth(req.user);
语句是重复出现在resolver中的,这种方式简单直观,容易理解,没有引入任何技术概念。总结
使用GraphQL middleware的方式,引入middleware的概念,实质上就是使用中间件函数对GraphQL resolver函数进行包装,遵循洋葱模型,每一个middleware都像是一层洋葱皮包裹着最内层的resolver函数,示意图如下:
这种方式的重点在于要设计好中间件,比如方式1代码中的定义的
resolverAuthMap
变量,就是将resolver和访问该resolver需要的用户权限做一个映射,又通过resolver的info
参数获取到解析的字段名称,这样才建立了映射关系。但是info
参数,正如apollographql官方文档所描述:进阶使用,对于一般的日常开发,使用prisma或者apollographql来构建一个GraphQL Web服务,几乎用不到该参数,目前关于如何使用该参数的文档并不多,因此如果要写中间件,特别是复杂的中间件,可能需要对
info
对象进行深入学习了解,并对中间件充分测试,这提升了开发成本。本示例中间件方式的另一个缺点是,在schema,resolver这两层上看不到任何用户权限,需要在authMiddleware
这个文件才能看到resolver相对应的用户权限,可能不够直观,降低了代码的可读性。使用GraphQL Schema Directive方式,相比于方式1在GraphQL resolver这一层上做用户认证和授权,directive将这一功能移动到了GraphQL Schema这一层,通过
@auth
指令直接在schema上标记字段需要的用户权限,很直观。但和使用middleware方式一样,该方案的实现成本可能过高,尽管是GraphQL规格中的一个概念,但目前对于GraphQL Schema Directive的文档也不多,很多技术细节没有交代清楚,对于复杂指令,可能会踩坑。使用类+装饰器的方式,优点是没有引入graphql middleware和graphql schema directive的概念,装饰器模式及其技术实现都很成熟,概念也相对容易理解。不过需要将编程范式改为OOP,装饰器只能配合类来使用,如果用FP,实现装饰器的功能需要用高阶函数(high-order function)。
使用组合式resolver的方式,利用了FP的概念,代码实现也很简单,缺点是会导致resolver代码略显凌乱,可读性变差。
示例代码
https://github.com/mrdulin/apollo-graphql-tutorial/tree/master/src/graphql-authentication-and-authorization
参考
The text was updated successfully, but these errors were encountered: