GraphQL(五):GraphQL身份認(rèn)證

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 都要自己寫就比較麻煩了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末超升,一起剝皮案震驚了整個濱河市入宦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌室琢,老刑警劉巖乾闰,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異盈滴,居然都是意外死亡涯肩,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門巢钓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來病苗,“玉大人,你說我怎么就攤上這事竿报。” “怎么了继谚?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵烈菌,是天一觀的道長阵幸。 經(jīng)常有香客問我,道長芽世,這世上最難降的妖魔是什么挚赊? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮济瓢,結(jié)果婚禮上荠割,老公的妹妹穿的比我還像新娘。我一直安慰自己旺矾,他們只是感情好蔑鹦,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著箕宙,像睡著了一般嚎朽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上柬帕,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天哟忍,我揣著相機(jī)與錄音,去河邊找鬼陷寝。 笑死锅很,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的凤跑。 我是一名探鬼主播爆安,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼饶火!你這毒婦竟也來了鹏控?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤肤寝,失蹤者是張志新(化名)和其女友劉穎当辐,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鲤看,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡缘揪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了义桂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片找筝。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖慷吊,靈堂內(nèi)的尸體忽然破棺而出袖裕,到底是詐尸還是另有隱情,我是刑警寧澤溉瓶,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布急鳄,位于F島的核電站谤民,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏疾宏。R本人自食惡果不足惜张足,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望坎藐。 院中可真熱鬧为牍,春花似錦、人聲如沸岩馍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兼雄。三九已至吟逝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赦肋,已是汗流浹背块攒。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留佃乘,地道東北人囱井。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像趣避,于是被迫代替她去往敵國和親庞呕。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354