在GraphQL(四):GraphQL工程化實踐中說到權(quán)限管理井仰,是用 Instrumentation 來實現(xiàn)养晋,這其實是很坑的温兼,因為API和權(quán)限的關(guān)系需要自己實現(xiàn),如果能夠像spring注解一樣帖族,直接在定義API的時候就關(guān)聯(lián)上相應(yīng)的權(quán)限栈源,然后在后端根據(jù)用戶權(quán)限和API權(quán)限做判斷,那么現(xiàn)有的spring的權(quán)限控制的邏輯都可以復(fù)用竖般,并且要擴(kuò)展更多需求也更優(yōu)雅甚垦。說到底,根本上是期望能夠有一種更便捷的方式攔截GraphQL的API。
Derectives
在GraphQL規(guī)范中有定義一個叫做Derectives的家伙
Directives provide a way to describe alternate runtime execution and type validation behavior in a GraphQL document.
乍一看艰亮,這不就是我們需要的功能嗎闭翩?
然而,這個這么有用的功能GraphQL-Java在9.0版本才實現(xiàn)迄埃,實在是姍姍來遲啊疗韵。而如果是通過GraphQL-Java-Tools來使用GraphQL需要使用5.2.4以上的版本才能享受Derectives。
利用Derectives實現(xiàn)GraphQLAPI攔截
總共分成三個步驟:
1. 在schema中定義并使用Derectives
directive @role(roles : [Int!]!) on FIELD_DEFINITION
type Query{
stages:[Stage!]! @role(roles : [101])
subjects:[Subject!]! @role(roles : [100]) @deprecated(reason:"is old value")
}
我們定義了一個叫做“role"的Derective侄非,它帶有一個Int的數(shù)組作為參數(shù)蕉汪。當(dāng)我們在API上使用Derective,通過不同的參數(shù)就可以區(qū)分不同的權(quán)限逞怨。
2. 實現(xiàn)Derective的攔截
class RoleDirective : SchemaDirectiveWiring {
override fun onField(environment: SchemaDirectiveWiringEnvironment<GraphQLFieldDefinition>?): GraphQLFieldDefinition {
val targetRoles = environment!!.directive.getArgument("roles").value
val field = environment!!.element
val originalDataFetcher = field.dataFetcher
val dataFetcher = DataFetcher {
val user = AuthInfoHolder.authInfo.user
val userRole= getUserRole(user)
val requireRoles = targetRoles as List<Int>
if (!requireRoles.contains(userRole)) {
throw Exception("requireRoles")
}
originalDataFetcher.get(it)
}
return field.transform { it.dataFetcher(dataFetcher) }
}
}
SchemaDirectiveWiring 是充當(dāng)膠水的一個類者疤,它將schema中的Derective和GraphQL引擎關(guān)聯(lián)起來。
3. 將RoleDirective注入GraphQL引擎
如果是用GraphQL-Java-Tools叠赦,在構(gòu)建SchemaParserBuilder時直接注入:
schemaParserBuilder.resolvers(resolvers!!)
.directive("role", RoleDirective())
.build()
如果沒用GraphQL-Java-Tools呢驹马?這就要做更多的事情了,在介紹原理時再來介紹這部分眯搭。
Derectives原理(基于GraphQl-JAVA-TOOLS 5.5.2)
在介紹Derectives原理前需要了解一個概念:DataFetcher窥翩。
GraphQL通過DataFetcher獲取每一個字段的值,比如上面的stages鳞仙、subjects寇蚊,包括stage里面的子屬性stageId、stageName等棍好,都對應(yīng)了一個DataFetcher仗岸。后續(xù)會專門寫一篇文章來介紹DataFetcher,這里先簡單了解借笙。
回到上面第二步的代碼:
class RoleDirective : SchemaDirectiveWiring {
// 重寫onField方法扒怖,攔截標(biāo)記了Directive的API
override fun onField(environment: SchemaDirectiveWiringEnvironment<GraphQLFieldDefinition>?): GraphQLFieldDefinition {
// 獲取Directive中的參數(shù)值(類似于獲取注解中的參數(shù)值)
val targetRoles = environment!!.directive.getArgument("roles").value
val field = environment!!.element
// 獲取原來的DataFetcher
val originalDataFetcher = field.dataFetcher
// 包裝了原始DataFetcher的新的DataFetcher
val dataFetcher = DataFetcher {
val user = AuthInfoHolder.authInfo.user
val userRole= getUserRole(user)
val requireRoles = targetRoles as List<Int>
// 如果沒有權(quán)限則拋出異常
if (!requireRoles.contains(userRole)) {
throw Exception("requireRoles")
}
// 如果有權(quán)限則走原始DataFetcher的數(shù)據(jù)獲取邏輯
originalDataFetcher.get(it)
}
// 將原來的DataFetcher替換成增加了攔截功能的新的DataFetcher
return field.transform { it.dataFetcher(dataFetcher) }
}
}
類似增加了一個代理,在代理中處理了權(quán)限攔截业稼。
前面的文章有說過盗痒,Instrumentation也可以在獲取數(shù)據(jù)前做攔截,它和Derective的區(qū)別在于后者是配合了schema中定義的Derective低散,而前者是純后端對整個流程的攔截俯邓。
除了Field,SchemaDirectiveWiring 還允許我們攔截對象熔号、參數(shù)稽鞭、枚舉等等。那么引镊,當(dāng)我們把Directive注入到GraphQL引擎后朦蕴,是怎么影響到GraphQL的執(zhí)行流程的呢篮条?
我們寫的 SchemaDirectiveWiring 會和其他用戶自定義的東東( GraphQLScalarType、typeResolvers吩抓、EnumValuesProvider 等)包裝成一個 RuntimeWiring 涉茧,一看名字就是做注入的,接著 RuntimeWiring 被傳遞到 SchemaParser 中琴拧, SchemaParser 會把從schema解析得到的對象構(gòu)造成內(nèi)存中的 GraphQLSchema降瞳,核心的方法是 parseSchemaObjects():
fun parseSchemaObjects(): SchemaObjects {
// 省略...
// 創(chuàng)建不同的GraphQLType Object
val interfaces = interfaceDefinitions.map { createInterfaceObject(it) }
val objects = objectDefinitions.map { createObject(it, interfaces) }
val unions = unionDefinitions.map { createUnionObject(it, objects) }
val inputObjects = inputObjectDefinitions.map { createInputObject(it) }
val enums = enumDefinitions.map { createEnumObject(it) }
// 省略...
}
我們把焦點集中到 createObject 上,這里的Object指的是我們在schema中定義的type蚓胸,比如:
## 這是一個叫Stage的Object
type Stage{
stageCode:String
stageName:String
}
## 這是一個叫Query的Object
type Query{
stages:[Stage!]! @role(roles : [101])
subjects:[Subject!]! @role(roles : [100]) @deprecated(reason:"is old value")
}
我們截取 createObject 中的代碼進(jìn)行分析:
private fun createObject(definition: ObjectTypeDefinition, interfaces: List<GraphQLInterfaceType>): GraphQLObjectType {
// 解析到Query這個Object挣饥,建造者模式構(gòu)造一個GraphQLObjectType
val name = definition.name
val builder = GraphQLObjectType.newObject()
.name(name)
.definition(definition)
.description(if (definition.description != null) definition.description.content else getDocumentation(definition))
// 注入綁定到Object上的Derective
builder.withDirectives(*buildDirectives(definition.directives, setOf(), Introspection.DirectiveLocation.OBJECT))
// 注入實現(xiàn)的接口
definition.implements.forEach { implementsDefinition ->
val interfaceName = (implementsDefinition as TypeName).name
builder.withInterface(interfaces.find { it.name == interfaceName }
?: throw SchemaError("Expected interface type with name '$interfaceName' but found none!"))
}
// 這里是重點啦,這里會找到當(dāng)前Object的字段的FieldDefinition
definition.getExtendedFieldDefinitions(extensionDefinitions).forEach { fieldDefinition ->
fieldDefinition.description
// 根據(jù)schema的FieldDefinition構(gòu)造GraphQLFieldDefinition沛膳,也就是構(gòu)造stages這個API扔枫,不過對于GraphQL來講,都是GraphQLFieldDefinition
builder.field { field ->
createField(field, fieldDefinition)
field.dataFetcher(fieldResolversByType[definition]?.get(fieldDefinition)?.createDataFetcher()
?: throw SchemaError("No resolver method found for object type '${definition.name}' and field '${fieldDefinition.name}', this is most likely a bug with graphql-java-tools"))
// 將經(jīng)過SchemaDirectiveWiring處理的stages的GraphQLFieldDefinition返回
val wiredField = directiveGenerator.onField(field.build(), DirectiveBehavior.Params(runtimeWiring))
GraphQLFieldDefinition.Builder(wiredField)
.clearArguments()
.argument(wiredField.arguments)
}
}
val objectType = builder.build()
return directiveGenerator.onObject(objectType, DirectiveBehavior.Params(runtimeWiring))
}
沿著 val wiredField = directiveGenerator.onField(field.build(), DirectiveBehavior.Params(runtimeWiring)) 繼續(xù)跟锹安,跟到 SchemaGeneratorDirectiveHelper 中找到了 SchemaDirectiveWiring 的處理:
private <T extends GraphQLDirectiveContainer> T wireForEachDirective(
Parameters parameters, T element, List<GraphQLDirective> directives,
EnvBuilder<T> envBuilder, EnvInvoker<T> invoker) {
T outputObject = element;
for (GraphQLDirective directive : directives) {
SchemaDirectiveWiringEnvironment<T> env = envBuilder.apply(outputObject,directive);
Optional<SchemaDirectiveWiring> directiveWiring = discoverWiringProvider(parameters, directive.getName(), env);
if (directiveWiring.isPresent()) {
SchemaDirectiveWiring schemaDirectiveWiring = directiveWiring.get();
// 執(zhí)行SchemaDirectiveWiring中的onField方法短荐,完成DataFetcher的攔截
T newElement = invoker.apply(schemaDirectiveWiring, env);
assertNotNull(newElement, "The SchemaDirectiveWiring MUST return a non null return value for element '" + element.getName() + "'");
outputObject = newElement;
}
}
return outputObject;
}
我們回到最開始的問題,如果沒用GraphQL-Java-Tools怎么注入 SchemaDirectiveWiring 呢叹哭?
答案就很顯然了忍宋,我們需要自己寫 GraphQLFieldDefinition,然后自己調(diào)用 val wiredField = directiveGenerator.onField(field.build(), DirectiveBehavior.Params(runtimeWiring)) 即可风罩,調(diào)用 directiveGenerator 很簡單糠排,但是每一個 GraphQLFieldDefinition 都要自己寫就比較麻煩了。