具有动态标头授权的 Angular Apollo WebSocketLink

Posted

技术标签:

【中文标题】具有动态标头授权的 Angular Apollo WebSocketLink【英文标题】:Angular Apollo WebSocketLink with dynamic header authorization 【发布时间】:2021-09-07 22:54:59 【问题描述】:

我正在使用 graphql 订阅;最初,标头有一个空的授权令牌,登录后在本地存储中生成一个令牌变量。我想要做的是在登录后自动更新订阅标头中的令牌变量,我已经按照文档中的说明进行了尝试,但是令牌永远不会更新并且它总是将其发送为空。

我需要 WebSocketLink 中的标头在 connectionParams 中是动态的

这是我的 GraphQLModule 文件,希望有人能帮助我...

import  NgModule  from '@angular/core';
import  ApolloClientOptions, InMemoryCache, split ,ApolloLink from '@apollo/client/core';
import  APOLLO_OPTIONS  from 'apollo-angular';
import  HttpLink  from 'apollo-angular/http';
import WebSocketLink from '@apollo/client/link/ws';
import getMainDefinition from '@apollo/client/utilities';
import  environment  from 'environments/environment';
import * as CryptoJS from 'crypto-js';  
import  setContext  from '@apollo/client/link/context';
import onError from '@apollo/client/link/error';
import Swal from 'sweetalert2';
import  CoreTranslationService  from '@core/services/translation.service';
import  locale as en  from 'app/main/pages/i18n/en';
import  locale as es  from 'app/main/pages/i18n/es';

const uri = environment.apiUrl; // <-- endpoint1 gql
const urisub = environment.apiSubs;// <-- endpoint2 gql



function operationFilter(operationName:string):boolean  
  if(operationName!="checkToken") return true;    
  else return false;      //and the others
 

export function createApollo(httpLink: HttpLink,_coreTranslationService: CoreTranslationService): ApolloClientOptions<any> 

  _coreTranslationService.translate(en, es);

  const basic = setContext((operation, context) => (
    headers: 
      Accept: 'charset=utf-8'
    
  ));

  const auth = setContext((operation, context) => 
      const token = localStorage.getItem('token');
      
  
      if (token === null) 
        return ;
       else 
        let token_decrypt= CryptoJS.AES.decrypt(token,environment.key_decrypt).toString(CryptoJS.enc.Utf8)
        return 
          headers: 
            Authorization: `Bearer $token_decrypt`
          
        ;
      
    );

  const http = httpLink.create(
    uri(operation) 
      return operationFilter(operation.operationName)? uri : urisub;
     
  );
 
  const ws = new WebSocketLink(
    uri:`ws://localhost:3005/subscriptions`,
    options:      
      lazy: true,
      reconnect: true,
      connectionParams: async () => 
        const token =  localStorage.getItem('token');
        let token_decrypt= null
        if (token) 
           token_decrypt= CryptoJS.AES.decrypt(token,environment.key_decrypt).toString(CryptoJS.enc.Utf8) 
                       
        return                  
            Authorization: token ? `Bearer $token_decrypt` : "",         
        
      ,
    
  );


  const error = onError((networkError, graphQLErrors) => 
        if (graphQLErrors)             
          
          graphQLErrors.map((
                  message,
                  locations,
                  path,
                  extensions
              ) =>

                console.log('error graph', localStorage.getItem('token'));
                
                if (extensions && localStorage.getItem('token')!=null) 
                  if (extensions.exception.status==401) 

                    Swal.fire(
                      icon: 'error',
                      title: _coreTranslationService.instant('ErrorSub.Title'),
                      text: _coreTranslationService.instant('ErrorSub.Message'),
                      timer: 6000,
                      timerProgressBar: true,
                      showCancelButton: false, 
                      showConfirmButton: false,
                      allowOutsideClick: false,
                      allowEscapeKey: false
                    ); 
                    
          
                    setTimeout(() =>   
                      localStorage.clear();                 
                      window.location.href = "/pages/authentication/login-v2";                       
                    , 7000);
                    
                    
                  
                
                
              
              
          );
      
      if (networkError) 
          console.log(`[Network error]: $networkError`);
      
  )
  

  const _split = split(
    (query) => 
      const data = getMainDefinition(query);
      return (
        data.kind === 'OperationDefinition' && data.operation === 'subscription'
      );
    ,
    ws,
    //http
    auth.concat(http)
  )

  
  
  

  const cleanTypeName = new ApolloLink((operation, forward) => 
    if (operation.variables) 
      const omitTypename = (key, value) => (key === '__typename' ? undefined : value);
      operation.variables = JSON.parse(JSON.stringify(operation.variables), omitTypename);
    
    return forward(operation).map((data) => 
      return data;
    );
  );

  


  const link =ApolloLink.from([cleanTypeName, basic, error, _split]);
 
  
  
  return 
    link: link,
    cache: new InMemoryCache(
      addTypename: false,
    ),
    defaultOptions: 
      watchQuery: 
        fetchPolicy: 'network-only',
        //errorPolicy: 'all',
      ,
      query: 
          fetchPolicy: 'network-only',
      ,
      mutate: 
        
      
    ,
  ;


@NgModule(
  providers: [
    
      provide: APOLLO_OPTIONS,
      useFactory: createApollo,
      deps: [HttpLink, CoreTranslationService],
    ,
  ],
)
export class GraphQLModule 

【问题讨论】:

您找到解决方案了吗?我也面临同样的问题,我无法继续。 @SureN 是的,登录后我立即关闭订阅,服务会重新连接并发送新标头,我不知道这是否是最佳选择,但它对我有用。 谢谢,我没有用模块实现套接字,而是作为服务实现,我能够解决。 【参考方案1】:

我分享了我的解决方案,我不知道它是否是最佳选择,但它在客户端对我有用:

graphql.module.ts

import  NgModule  from '@angular/core';
import  ApolloClientOptions, InMemoryCache, split ,ApolloLink, Operation, makeVar from '@apollo/client/core';
import  Apollo, APOLLO_OPTIONS  from 'apollo-angular';
import  HttpLink  from 'apollo-angular/http';
import WebSocketLink from '@apollo/client/link/ws';
import getMainDefinition from '@apollo/client/utilities';
import  environment  from 'environments/environment';
import * as CryptoJS from 'crypto-js';  
import  setContext  from '@apollo/client/link/context';
import onError from '@apollo/client/link/error';
import Swal from 'sweetalert2';
import  CoreTranslationService  from '@core/services/translation.service';
import  locale as en  from 'app/main/pages/i18n/en';
import  locale as es  from 'app/main/pages/i18n/es';
import  SubscriptionClient  from 'subscriptions-transport-ws';
import  HttpClientModule  from '@angular/common/http';
import HttpLinkModule from 'apollo-angular-link-http';


const uri = environment.apiUrl; 
const urisub = environment.apiSubs;



function operationFilter(operationName:string):boolean  
  if(operationName!="checkToken") return true;  
  else return false;      


@NgModule(

  exports: [
    HttpClientModule,
    HttpLinkModule
  ]
)
export class GraphQLModule 
    public Clientws: any;
    public subscriptionClient: SubscriptionClient = null;

   constructor(apollo: Apollo, httpLink: HttpLink,_coreTranslationService: CoreTranslationService)

    _coreTranslationService.translate(en, es);

    const getIdToken = () => localStorage.getItem('token') || null;

    const auth = setContext((operation, context) =>       
        return 
          headers: 
            Authorization: `Bearer $getIdToken()`
          
        ;      
    );

    const http = httpLink.create(
        uri(operation) 
          return operationFilter(operation.operationName)? uri : urisub;
         
    );


      
      const wsClient = new SubscriptionClient(`ws://localhost:3005/subscriptions`, 
        reconnect: true,
        connectionParams: async () =>                 
          return                  
              Authorization:`Bearer $getIdToken()`,         
          
        ,
      )



      this.Clientws = wsClient

      const ws = new WebSocketLink(wsClient)

      

      this.subscriptionClient = (<any>ws).subscriptionClient;

        const error = onError((networkError, graphQLErrors) => 

                      
        if (graphQLErrors  && getIdToken()!=null && getIdToken()!='')             
          
          graphQLErrors.map((
                  message,
                  locations,
                  path,
                  extensions
              ) =>

                if (extensions) 
                  if (extensions.exception.status==401 && getIdToken()!=null && getIdToken()!='')                       
                    

                    Swal.fire(
                      icon: 'error',
                      title: _coreTranslationService.instant('ErrorSub.Title'),
                      text: _coreTranslationService.instant('ErrorSub.Message'),
                      timer: 6000,
                      timerProgressBar: true,
                      showCancelButton: false, 
                      showConfirmButton: false,
                      allowOutsideClick: false,
                      allowEscapeKey: false
                    ); 
                    
          
                    setTimeout(() =>   
                      localStorage.clear();                 
                      window.location.href = "/pages/authentication/login-v2";                       
                    , 7000);
                    
                    
                  
                
                
              
              
          );
      
      if (networkError) 
          console.log(`[Network error]:`, networkError);
      
  )

      const _split = split(
        (query) => 
          const data = getMainDefinition(query);
          return (
            data.kind === 'OperationDefinition' && data.operation === 'subscription'
          );
        ,
        ws,
        auth.concat(http)
      )


        const cleanTypeName = new ApolloLink((operation, forward) => 
          if (operation.variables) 
            const omitTypename = (key, value) => (key === '__typename' ? undefined : value);
            operation.variables = JSON.parse(JSON.stringify(operation.variables), omitTypename);
          
          return forward(operation).map((data) => 
            return data;
          );
        );

          const basic = setContext((operation, context) => (
            headers: 
              Accept: 'charset=utf-8'
            
          ));

  


  const link =ApolloLink.from([cleanTypeName, basic, error, _split]);

    apollo.create(
      link:link,
      cache: new InMemoryCache(
        addTypename: false,
      ),
      defaultOptions: 
        watchQuery: 
          fetchPolicy: 'network-only',
          //errorPolicy: 'all',
        ,
        query: 
            fetchPolicy: 'network-only',
        ,
        mutate: 
          
        
      ,
    );
   

login.component.ts

constructor(
    .....,
    private gqlModule: GraphQLModule,
    .....
  )
     .......
  

onSubmit(event)  //submit login form 
......

//if service login response true
this.gqlModule.Clientws.close(false,false); // this closes the websocket and automatically it reconnects with the new information in the header
//Note .. If you do not have the reconnection in true, you need to connect again manually    
......

在服务器端:

配置订阅选项

onConnect: (connectionParams, webSocket, context) =>    
         

          if (connectionParams['Authorization'] != null && connectionParams['Authorization'] != '') 

            if (connectionParams['Authorization'].split(" ")[1]!= null && connectionParams['Authorization'].split(" ")[1]!= '' && connectionParams['Authorization'].split(" ")[1]!= 'null')  
             return  authorization: connectionParams['Authorization'] ;
            
            
           
          
        ,

【讨论】:

以上是关于具有动态标头授权的 Angular Apollo WebSocketLink的主要内容,如果未能解决你的问题,请参考以下文章

Nuxt / Apollo - 设置授权标头

如何使用 apollo graphql 处理授权标头?

如何使用 Express 和 Apollo-Server 获取 HTTP 授权标头

如何在 React 组件中向 Apollo 客户端添加新的授权标头?

Angular2 中的 Apollo 客户端自定义标头和附加变量

Nuxt Apollo 模块请求授权标头,带有双 'Bearer'