Angular 具有动态 header 授权的 Apollo WebSocketLink
Angular Apollo WebSocketLink with dynamic header authorization
我正在使用 graphql 订阅;最初 header 有一个空的授权令牌,登录后在本地存储中生成一个令牌变量。我想要做的是在登录后自动更新订阅 header 中的 token 变量,我已经按照文档中的说明进行了尝试,但令牌永远不会更新并且总是将其发送为空。
我需要 WebSocketLink 中的 header 在 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 {
}
我分享了我的解决方案,我不知道它是否是最佳选择,但它在客户端对我有用:
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'] };
}
}
},
我正在使用 graphql 订阅;最初 header 有一个空的授权令牌,登录后在本地存储中生成一个令牌变量。我想要做的是在登录后自动更新订阅 header 中的 token 变量,我已经按照文档中的说明进行了尝试,但令牌永远不会更新并且总是将其发送为空。
我需要 WebSocketLink 中的 header 在 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 {
}
我分享了我的解决方案,我不知道它是否是最佳选择,但它在客户端对我有用:
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'] };
}
}
},