Angular 添加令牌并自动刷新的拦截器
Angular Interceptor to add Token and Automatically Refresh
我是第一次使用 angular 拦截器,我几乎得到了我想要的东西,但有些东西我在谷歌搜索了一段时间后还是不太明白。我在本地存储一个刷新令牌,访问令牌每 15 分钟过期一次;我希望能够使用刷新令牌在到期时自动刷新他们的授权令牌。
我的第一次尝试是这样的:
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.toLowerCase().includes('/auth')) {
// It's an auth request, don't get a token
return next.handle(req);
}
// Not an auth endpoint, should have a token
this.authService.GetCurrentToken().subscribe(token => {
// Make sure we got something
if (token == null || token === '') {
return next.handle(req);
}
// Have a token, add it
const request = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next.handle(request);
});
}
这似乎不起作用,我也不知道为什么(我是 Angular 的新手,也是 JS 的新手,如果对其他人来说很明显,我很抱歉)。凭直觉,我想知道是不是 observable 把事情搞砸了,它不喜欢等待 observable 到 return 所以我试了这个:
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.toLowerCase().includes('/auth')) {
// It's an auth request, don't get a token
return next.handle(req);
}
const token = this.authService.GetAccessTokenWithoutRefresh();
const request = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next.handle(request);
}
现在它似乎起作用了!这表明我的预感可能是正确的(或者它是我没有看到的其他代码中的其他内容)。无论如何,工作是好的,但这给我留下了如何刷新的问题。我使用来自 auth 服务的可观察对象的最初原因是以防它需要刷新。基本上,授权服务会查看它的当前令牌并查看它是否已过期。如果不是,它只会 return of(token)
但如果它已过期,它将通过可观察到的 http post 返回到服务器,因此只要服务器响应,字符串就会到达。
所以我想我的问题有两个方面:
- 任何人都可以确认或反驳我关于 observable 弄乱拦截器的说法是正确的吗?这似乎是问题所在,但我想确定一下。
- 我如何在后台为他们刷新令牌,而他们不必每 15 分钟重新登录一次?
编辑
这是认证令牌方法中的逻辑:
GetCurrentToken(): Observable<string> {
if (this.AccessToken == null) {
return null;
}
if (this.Expiry > new Date()) {
return of(this.AccessToken);
}
// Need to refresh
return this.RefreshToken().pipe(
map<LoginResult, string>(result => {
return result.Success ? result.AccessToken : null;
})
);
}
以及刷新方法:
private RefreshToken(): Observable<LoginResult> {
const refreshToken = localStorage.getItem('rt');
if (refreshToken == null || refreshToken === '') {
const result = new LoginResult();
// Set other stuff on result object
return of(result);
}
const refresh = new RefreshTokenDto();
refresh.MachineId = 'WebPortal';
refresh.TokenId = refreshToken;
return this.http.post(ApiData.baseUrl + '/auth/refresh', refresh)
.pipe(
tap<AuthResultDto>(authObject => {
this.SetLocalData(authObject);
}),
map<AuthResultDto, LoginResult>(authObject => {
const result = new LoginResult();
// Set other stuff on the result object
return result;
}),
catchError(this.handleError<LoginResult>('Refresh'))
);
}
编辑
好的,在下面的答案以及 问题的帮助下,这是我想出的:
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.toLowerCase().includes('/auth')) {
// It's an auth request, don't get a token
return next.handle(req.clone());
}
return this.authService.GetCurrentToken().pipe(
mergeMap((token: string) => {
if (token === null || token === '') {
throw new Error('Refresh failed to get token');
} else {
return next.handle(req.clone({setHeaders: {Authorization: `Bearer ${token}`}}));
}
}),
catchError((err: HttpErrorResponse) => {
if (err.status === 401) {
this.router.navigateByUrl('/login');
}
return throwError(err);
})
);
}
所以基本上我的第一次尝试并不遥远,'secret' 是使用管道和合并映射而不是尝试订阅。
您可以尝试以下方法。我可能夸大了其中的 FP 数量:
export class AuthInterceptor {
ctor(private authService: AuthService){}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return of(req.url.toLowerCase().includes('/auth')).pipe(
mergeMap(isAuthRequest => !isAuthRequest
// Missing: handle error when accessing the access token
? this.authService.accessToken$.pipe(map(addAuthHeader(req)))
: of(req)
),
mergeMap(nextReq => next.handle(nextReq))
);
}
}
function addAuthHeader(req: HttpRequest<any>): (token:string)=> HttpRequest<any> {
return token => req.clone({setHeaders: {Authorization: `Bearer ${token}`}})
}
以及身份验证服务:
export class AuthService {
ctor(private http: HttpClient){}
get accessToken$(): Observable<string> {
return of(this.AccessToken).pipe(
mergeMap(token => token === null
? throwError("Access token is missing")
: of(this.Expiry > new Date())
),
mergeMap(accessTokenValid => accessTokenValid
? of(this.AccessToken)
: this.refreshToken()
)
);
}
refreshToken(): Observable<string> {
return of(localStorage.getItem('rt')).pipe(
mergeMap(refreshToken => !refreshToken
? of(extractAccessTokenFromLogin(createLoginResult())
: this.requestAccessToken(this.createRefreshToken(refreshToken))
)
);
}
private requestAccessToken(refreshToken: RefreshTokenDto): Observable<string> {
return this.http.post<AuthResultDto>(ApiData.baseUrl + '/auth/refresh', refreshToken)
.pipe(
tap(auth => this.SetLocalData(auth )),
map(auth => this.mapAuthObjToLoginRes(auth)),
map(extractAccessTokenFromLogin)
catchError(this.handleError<string>('Refresh'))
)
}
private createRefreshToken(tokenId: string): RefreshTokenDto{...}
private createLoginRes(): LoginResult {...}
private mapAuthObjToLoginRes(val: AuthResultDto): LoginResult{...}
}
function extractAccessTokenFromLogin(login: LoginResult): string
=> login.Success ? login.AccessToken : null;
我是第一次使用 angular 拦截器,我几乎得到了我想要的东西,但有些东西我在谷歌搜索了一段时间后还是不太明白。我在本地存储一个刷新令牌,访问令牌每 15 分钟过期一次;我希望能够使用刷新令牌在到期时自动刷新他们的授权令牌。
我的第一次尝试是这样的:
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.toLowerCase().includes('/auth')) {
// It's an auth request, don't get a token
return next.handle(req);
}
// Not an auth endpoint, should have a token
this.authService.GetCurrentToken().subscribe(token => {
// Make sure we got something
if (token == null || token === '') {
return next.handle(req);
}
// Have a token, add it
const request = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next.handle(request);
});
}
这似乎不起作用,我也不知道为什么(我是 Angular 的新手,也是 JS 的新手,如果对其他人来说很明显,我很抱歉)。凭直觉,我想知道是不是 observable 把事情搞砸了,它不喜欢等待 observable 到 return 所以我试了这个:
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.toLowerCase().includes('/auth')) {
// It's an auth request, don't get a token
return next.handle(req);
}
const token = this.authService.GetAccessTokenWithoutRefresh();
const request = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next.handle(request);
}
现在它似乎起作用了!这表明我的预感可能是正确的(或者它是我没有看到的其他代码中的其他内容)。无论如何,工作是好的,但这给我留下了如何刷新的问题。我使用来自 auth 服务的可观察对象的最初原因是以防它需要刷新。基本上,授权服务会查看它的当前令牌并查看它是否已过期。如果不是,它只会 return of(token)
但如果它已过期,它将通过可观察到的 http post 返回到服务器,因此只要服务器响应,字符串就会到达。
所以我想我的问题有两个方面:
- 任何人都可以确认或反驳我关于 observable 弄乱拦截器的说法是正确的吗?这似乎是问题所在,但我想确定一下。
- 我如何在后台为他们刷新令牌,而他们不必每 15 分钟重新登录一次?
编辑
这是认证令牌方法中的逻辑:
GetCurrentToken(): Observable<string> {
if (this.AccessToken == null) {
return null;
}
if (this.Expiry > new Date()) {
return of(this.AccessToken);
}
// Need to refresh
return this.RefreshToken().pipe(
map<LoginResult, string>(result => {
return result.Success ? result.AccessToken : null;
})
);
}
以及刷新方法:
private RefreshToken(): Observable<LoginResult> {
const refreshToken = localStorage.getItem('rt');
if (refreshToken == null || refreshToken === '') {
const result = new LoginResult();
// Set other stuff on result object
return of(result);
}
const refresh = new RefreshTokenDto();
refresh.MachineId = 'WebPortal';
refresh.TokenId = refreshToken;
return this.http.post(ApiData.baseUrl + '/auth/refresh', refresh)
.pipe(
tap<AuthResultDto>(authObject => {
this.SetLocalData(authObject);
}),
map<AuthResultDto, LoginResult>(authObject => {
const result = new LoginResult();
// Set other stuff on the result object
return result;
}),
catchError(this.handleError<LoginResult>('Refresh'))
);
}
编辑
好的,在下面的答案以及
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.toLowerCase().includes('/auth')) {
// It's an auth request, don't get a token
return next.handle(req.clone());
}
return this.authService.GetCurrentToken().pipe(
mergeMap((token: string) => {
if (token === null || token === '') {
throw new Error('Refresh failed to get token');
} else {
return next.handle(req.clone({setHeaders: {Authorization: `Bearer ${token}`}}));
}
}),
catchError((err: HttpErrorResponse) => {
if (err.status === 401) {
this.router.navigateByUrl('/login');
}
return throwError(err);
})
);
}
所以基本上我的第一次尝试并不遥远,'secret' 是使用管道和合并映射而不是尝试订阅。
您可以尝试以下方法。我可能夸大了其中的 FP 数量:
export class AuthInterceptor {
ctor(private authService: AuthService){}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return of(req.url.toLowerCase().includes('/auth')).pipe(
mergeMap(isAuthRequest => !isAuthRequest
// Missing: handle error when accessing the access token
? this.authService.accessToken$.pipe(map(addAuthHeader(req)))
: of(req)
),
mergeMap(nextReq => next.handle(nextReq))
);
}
}
function addAuthHeader(req: HttpRequest<any>): (token:string)=> HttpRequest<any> {
return token => req.clone({setHeaders: {Authorization: `Bearer ${token}`}})
}
以及身份验证服务:
export class AuthService {
ctor(private http: HttpClient){}
get accessToken$(): Observable<string> {
return of(this.AccessToken).pipe(
mergeMap(token => token === null
? throwError("Access token is missing")
: of(this.Expiry > new Date())
),
mergeMap(accessTokenValid => accessTokenValid
? of(this.AccessToken)
: this.refreshToken()
)
);
}
refreshToken(): Observable<string> {
return of(localStorage.getItem('rt')).pipe(
mergeMap(refreshToken => !refreshToken
? of(extractAccessTokenFromLogin(createLoginResult())
: this.requestAccessToken(this.createRefreshToken(refreshToken))
)
);
}
private requestAccessToken(refreshToken: RefreshTokenDto): Observable<string> {
return this.http.post<AuthResultDto>(ApiData.baseUrl + '/auth/refresh', refreshToken)
.pipe(
tap(auth => this.SetLocalData(auth )),
map(auth => this.mapAuthObjToLoginRes(auth)),
map(extractAccessTokenFromLogin)
catchError(this.handleError<string>('Refresh'))
)
}
private createRefreshToken(tokenId: string): RefreshTokenDto{...}
private createLoginRes(): LoginResult {...}
private mapAuthObjToLoginRes(val: AuthResultDto): LoginResult{...}
}
function extractAccessTokenFromLogin(login: LoginResult): string
=> login.Success ? login.AccessToken : null;