Как реализовать токен автоматического обновления в graphql для аутентификации на основе jwt?
Я пытаюсь выяснить этот сценарий для моей аутентификации на основе JWT в сервере graphql на основе Apollo (2.0) .
в основном после входа в систему пользователь получает accessToken и refreshToken с сервера.
AccessToken истекает через определенный период времени, и сервер отправляет сообщение об ошибке, указывающее, что токен истек (TokenExpiredError), а затем клиенту необходимо связаться с сервером для нового доступа через передачу refreshToken.
поток следующий -
- TokenExpiredError происходит
- получить эту ошибку на стороне клиента
- очередь все запросы со старым accessToken(так что сервер не затоплен слишком много вызовов refreshToken и многие accessTokens генерируются сервером)
- вызовите api refreshToken на сервере graphql, чтобы получить новый accessToken
- обновление accessToken для всех разрешенных вызовов с новыми маркер accessToken
- пользователь выхода из системы, если сам refreshToken истек
- предотвратить любой вид гонки состояние B / w звонки
Я уже реализовал мутацию refreshToken на стороне клиента, но не могу понять, когда происходит ошибка остановить все запросы - > запросить новый токен - > сделать все отложенные запросы снова, и если токен обновления истек пользователь выхода из системы.
2 ответов
вы можете использовать сервис auth0 jwl, доступный из https://github.com/auth0/angular2-jwt
используя эту функцию, вы можете реализовать http inteceptor для беспрепятственного управления связью. Допустимый пример можно найти в этом потоке: https://github.com/auth0/angular2-jwt/issues/15
(ищите последний комментарий ZeBigDuck) для удобства я копирую его ответ здесь:
I did a custom interceptor which handle every request passed after token expiration, and renew it with refresh token before send http requests.
This use redux to handle refreshing state.
And a custom lib that you can easily override or hardcode just to configure some routes.
import { Inject, Injectable } from "@angular/core";
import { HttpClient, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
import { Router } from "@angular/router";
import { Observable } from "rxjs/Observable";
import { MAGGLE_CONFIG, MaggleConfig } from "../maggle-config";
import { NgRedux } from "@angular-redux/store";
import { IMaggleState } from "../maggle.store";
import { TokenActions } from "../app/token.actions";
import 'rxjs/add/operator/first';
import 'rxjs/add/operator/skip';
import 'rxjs/add/operator/catch';
import { JwtHelperService } from '@auth0/angular-jwt';
@Injectable()
export class RefreshTokenInterceptor implements HttpInterceptor {
jwtHelper: JwtHelperService;
constructor(private router: Router, @Inject(MAGGLE_CONFIG) private maggleConfig: MaggleConfig,
private ngRedux: NgRedux<IMaggleState>, private http: HttpClient, private tokenActions: TokenActions) {
this.jwtHelper = new JwtHelperService();
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const refreshUrl = this.maggleConfig.apiEndpoint + this.maggleConfig.apiRefreshTokenUrl;
const token = localStorage.getItem('access_token');
const refreshToken = localStorage.getItem('refresh_token');
if ((!token && !refreshToken) || request.url === refreshUrl) {
// continue normally
return next.handle(request).catch(() => this.redirectToLogin());
}
if (this.jwtHelper.isTokenExpired(token)) {
return this.ngRedux.select<boolean>('refreshingToken').first().flatMap(refreshing => {
if (refreshing) {
// Wait for new token before sending the request
return this.ngRedux.select<boolean>('refreshingToken').skip(1).flatMap(refreshing => {
return next.handle(request).catch(() => this.redirectToLogin());
});
} else {
// Refresh token if expired and not already refreshing
this.ngRedux.dispatch(<any>this.tokenActions.refreshToken(true));
return this.http.post(refreshUrl, {refresh_token: refreshToken}).flatMap(
res => {
localStorage.setItem('access_token', res['token']);
localStorage.setItem('refresh_token', res['refresh_token']);
this.ngRedux.dispatch(<any>this.tokenActions.refreshToken(false));
return next.handle(request).catch(() => this.redirectToLogin());
}).catch(() => this.redirectToLogin());
}
});
} else {
return next.handle(request);
}
}
redirectToLogin(): Observable<HttpEvent<any>> {
this.router.navigate(this.maggleConfig.loginRoute);
localStorage.removeItem('refresh_token');
localStorage.removeItem('access_token');
this.ngRedux.dispatch(<any>this.tokenActions.refreshToken(false));
return Observable.empty();
}
}
надеюсь, что это помогает.
я следовал этой подход к решению моей проблемы, наконец,
публикация моего подхода для других
// @flow
import { ApolloLink, Observable } from 'apollo-link';
import type { ApolloClient } from 'apollo-client';
import type { Operation, NextLink } from 'apollo-link';
import { refreshToken2, getToken } from './token-service';
import { GraphQLError } from 'graphql';
export class AuthLink extends ApolloLink {
tokenRefreshingPromise: Promise<boolean> | null;
injectClient = (client: ApolloClient): void => {
this.client = client;
};
refreshToken = (): Promise<boolean> => {
//if (!this.tokenRefreshingPromise) this.tokenRefreshingPromise = refreshToken(this.client);
if (!this.tokenRefreshingPromise) this.tokenRefreshingPromise = refreshToken2();
return this.tokenRefreshingPromise;
};
setTokenHeader = (operation: Operation): void => {
const token = getToken();
if (token) operation.setContext({ headers: { authorization: `Bearer ${token}` } });
};
request(operation: Operation, forward: NextLink) {
// set token in header
this.setTokenHeader(operation);
// try refreshing token once if it has expired
return new Observable(observer => {
let subscription, innerSubscription, inner2Subscription;
try {
subscription = forward(operation).subscribe({
next: result => {
if (result.errors) {
console.log("---->", JSON.stringify(result.errors))
for (let err of result.errors) {
switch (err.extensions.code) {
case 'E140':
console.log('E140', result)
observer.error(result.errors)
break;
case 'G130':
this.refreshToken().then(response => {
if (response.data && !response.errors) {
this.setTokenHeader(operation);
innerSubscription = forward(operation).subscribe(observer);
} else {
console.log("After refresh token", JSON.stringify(response));
observer.next(response)
}
}).catch(console.log);
break;
}
}
}
observer.next(result)
},
complete: observer.complete.bind(observer),
error: netowrkError => {
observer.error(netowrkError);
}
},
});
} catch (e) {
observer.error(e);
}
return () => {
if (subscription) subscription.unsubscribe();
if (innerSubscription) innerSubscription.unsubscribe();
if (inner2Subscription) inner2Subscription.unsubscribe();
};
});
}
}