Django+GraphQL字段权限控制:异常时避免返回null字段
我用Django + GraphQL构建后端,在models.py中定义了User模型的部分字段:
email = models.EmailField(_("email address"), max_length=320, unique=True) telephone = models.CharField(max_length=20, null=True, default=None, blank=True)
对应的UserType定义如下:
import graphene from graphene_django import DjangoObjectType from main.graphql import validators from main.models import User class UserType(DjangoObjectType): class Meta: model = User fields = ( "id", "first_name", "last_name", "email", "birthdate", "birthplace", ) def resolve_telephone(self, info): validators.verify_request_from_staff_user_or_from_superuser_if_target_staff( info, self, info.context.user, "telephone" ) return self.telephone def resolve_verified_email(self, info): validators.verify_request_from_staff_user_or_from_superuser_if_target_staff( info, self, info.context.user, "verified_email" ) return self.verified_email
其中verify_request_from_staff_user_or_from_superuser_if_target_staff函数会在用户无权限访问字段时抛出graphql.GraphQLError异常。
我定义了testUser查询,根据ID返回用户。请求email字段时,返回的data中不包含该字段(符合预期),但请求telephone字段时,data中会出现telephone: null的内容。
示例查询及返回
查询email:
query testEmail { testUser(id: 9) { email } }
返回结果:
{ "errors": [ { "message": "Cannot access field 'email'", "locations": [ { "line": 3, "column": 5 } ], "path": [ "testUser", "email" ] } ], "data": { "testUser": null } }
查询telephone:
query testTelephone { testUser(id: 9) { telephone } }
返回结果:
{ "errors": [ { "message": "Cannot access field 'telephone'", "locations": [ { "line": 8, "column": 5 } ], "path": [ "testUser", "telephone" ] } ], "data": { "testUser": { "telephone": null } } }
我知道这是因为telephone字段在模型中设置了null=True,但不想修改数据库字段。请问如何实现无权限时响应中不包含该字段?另外,当前为每个字段编写解析器做权限控制的方式是否正确?有没有更高效的实现方法?
一、实现无权限时telephone字段不返回的方法
问题根源在于:email字段在模型中是非nullable的,因此GraphQL类型也是非nullable,当解析抛出异常时,父对象testUser会被置为null;而telephone是nullable字段,抛出异常时会返回null并保留父对象。
要实现无权限时字段不出现在data中,同时保留错误信息,可以使用graphene.UNSET常量:
修改resolve_telephone方法,捕获权限验证异常,添加错误到请求上下文,然后返回UNSET:
import graphene from graphql import GraphQLError def resolve_telephone(self, info): try: validators.verify_request_from_staff_user_or_from_superuser_if_target_staff( info, self, info.context.user, "telephone" ) return self.telephone except GraphQLError as e: # 将错误添加到请求的错误列表 info.errors.append(e) # 返回UNSET,该字段不会被包含在data中 return graphene.UNSET
同样的逻辑可以应用到resolve_verified_email方法上。这样处理后,无权限时data中不会出现该字段,同时errors数组会保留对应的错误提示。
二、更高效的权限控制实现方式
当前为每个字段编写单独解析器的方式是可行的,但重复代码较多,推荐以下几种优化方案:
1. 使用装饰器封装权限逻辑
把权限验证逻辑封装成装饰器,减少重复代码:
from functools import wraps import graphene from graphql import GraphQLError def staff_or_superuser_required(field_name): def decorator(resolver): @wraps(resolver) def wrapper(self, info, **kwargs): try: validators.verify_request_from_staff_user_or_from_superuser_if_target_staff( info, self, info.context.user, field_name ) return resolver(self, info, **kwargs) except GraphQLError as e: info.errors.append(e) return graphene.UNSET return wrapper return decorator # 在解析器上应用装饰器 class UserType(DjangoObjectType): # ... 省略Meta部分 @staff_or_superuser_required("telephone") def resolve_telephone(self, info): return self.telephone @staff_or_superuser_required("verified_email") def resolve_verified_email(self, info): return self.verified_email
2. 使用字段级权限中间件
编写全局中间件,统一处理所有字段的权限验证,无需为每个字段编写解析器:
import graphene from graphql import GraphQLError class FieldPermissionMiddleware: def resolve(self, next_resolver, root, info, **kwargs): # 定义需要权限控制的字段列表 restricted_fields = {"telephone", "verified_email", "email"} field_name = info.field_name if field_name in restricted_fields: try: validators.verify_request_from_staff_user_or_from_superuser_if_target_staff( info, root, info.context.user, field_name ) except GraphQLError as e: info.errors.append(e) return graphene.UNSET # 执行后续解析逻辑 return next_resolver(root, info, **kwargs) # 在创建Schema时添加中间件 schema = graphene.Schema( query=YourQueryType, mutation=YourMutationType, middleware=[FieldPermissionMiddleware()] )
3. 自定义权限控制基类
创建带有权限处理逻辑的DjangoObjectType基类,让需要权限控制的类型继承该基类:
from graphene_django import DjangoObjectType import graphene from graphql import GraphQLError class PermissionControlledDjangoObjectType(DjangoObjectType): class Meta: abstract = True def resolve_restricted_field(self, info, field_name): try: validators.verify_request_from_staff_user_or_from_superuser_if_target_staff( info, self, info.context.user, field_name ) return getattr(self, field_name) except GraphQLError as e: info.errors.append(e) return graphene.UNSET # 继承基类实现UserType class UserType(PermissionControlledDjangoObjectType): class Meta: model = User fields = ("id", "first_name", "last_name", "email", "birthdate", "birthplace") telephone = graphene.String() verified_email = graphene.Boolean() def resolve_telephone(self, info): return self.resolve_restricted_field(info, "telephone") def resolve_verified_email(self, info): return self.resolve_restricted_field(info, "verified_email")
内容的提问来源于stack exchange,提问作者Zeno Dalla Valle

