やりたいこと
- interceptorsを利用して、トークンのキャッシュがある場合はセットする
- interceptorsを利用して、401が返って来た場合は、トークンを取り直す
- トークン取得関数はメモ化する
- APIの取得結果は一定時間キャッシュする
参考
- Axios Interceptor を使用して401が返ったときにトークン(JWT)を更新する|tanoshima https://zenn.dev/tanoshima/articles/db552b7962086a
- "NestJS tip: how to inject multiple versions of the same provider into one module (e.g.: many Axios instances)" by Micael Levi L. C. https://dev.to/micalevisk/nestjs-tip-how-to-inject-multiple-versions-of-the-same-provider-into-one-module-eg-many-axios-instances-5agc
- [JavaScript] lodashのmemoizeで演算結果をキャッシュして高速化 https://qiita.com/etet-etet/items/e3a790caa2bd83a58498
- Lodash memoize with a timeout http://disq.us/t/429zou9
- https://docs.nestjs.com/techniques/cachin
- https://zenn.dev/kisihara_c/books/nest-officialdoc-jp/viewer/fundamentals-customproviders#エイリアスプロバイダ、useexisting
事前準備
npm i --save @nestjs/axios axios
npm i --save @nestjs/cache-manager cache-manager
npm i --save lodash
ファイル構成
src
└─ pet
├─ http-service
│ ├─ login
│ │ ├─ pet-login.modules.ts
│ │ └─ pet-login.http.service.ts
│ └─ api
│ ├─ pet-api.modules.ts
│ └─ pet-api.http.service.ts
├─ pet.module.ts
└─ pet.repository.ts
// pet-login.modules.ts
import { Module, OnModuleInit } from '@nestjs/common';
import { HttpModule, HttpService } from '@nestjs/axios';
import { PetLoginHttpService } from './pet-login.http.service';
const BASE_URL = '{リフレッシュトークン取得URL}';
@Module({
imports: [
HttpModule.register({
baseURL: BASE_URL,
}),
],
providers: [
{
provide: PetLoginHttpService,
useExisting: HttpService,
},
],
exports: [PetLoginHttpService],
})
export class PetLoginModule implements OnModuleInit {
constructor(private readonly httpService: HttpService) {}
onModuleInit() {
this.httpService.axiosRef.defaults.headers.common['Accept'] =
'application/json';
}
}
// pet-login.http.service.ts
import { HttpService } from '@nestjs/axios';
export abstract class PetLoginHttpService extends HttpService {}
// pet-api.modules.ts
import { Module, OnModuleInit } from '@nestjs/common';
import { HttpModule, HttpService } from '@nestjs/axios';
import { PetApiHttpService } from './pet-api.http.service';
const BASE_URL = '{APIURL}';
@Module({
imports: [
HttpModule.register({
baseURL: BASE_URL,
}),
],
providers: [
{
provide: PetApiHttpService,
useExisting: HttpService,
},
],
exports: [PetAPiHttpService],
})
export class PetApiModule implements OnModuleInit {
constructor(private readonly httpService: HttpService) {}
onModuleInit() {
this.httpService.axiosRef.defaults.headers.common['Accept'] =
'application/json';
}
}
// pet-api.http.service.ts
import { HttpService } from '@nestjs/axios';
export abstract class PetApiHttpService extends HttpService {}
// pet.module.ts
import { Module } from '@nestjs/common';
import { PetRepository } from './pet.repository';
import { PetLoginModule } from './http-service/login/pet-login.modules';
import { PetApiModule } from './http-service/api/pet-api.modules';
@Module({
imports: [PetLoginModule, PetApiModule, CacheModule.register()],
providers: [PetRepository],
exports: [PetRepository],
})
export class PetoModule {}
// pet.repository.ts
import { Inject, Injectable } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { ConfigUtil } from '../../../config/config.util';
import { PetLoginHttpService } from './http-service/login//pet-login.http.service';
import { PetApiHttpService } from './http-service/api/pet-api.http.service';
import { lastValueFrom, map } from 'rxjs';
import _ from 'lodash';
@Injectable()
export class PetRepository {
constructor(
private readonly configUtil: ConfigUtil,
private readonly apiHttpService: PetLoginHttpService,
private readonly loginHttpService: PetApiHttpService,
@Inject(CACHE_MANAGER) private cacheManager: Cache,
) {
// interceptor開始
this.interceptor();
}
async gerPet(): Promise<void> {
const ttl = 60 * 60 * 1000;
// 結果は一定期間キャッシュする
return await this.cacheManager.wrap(
'cashkey-pet',
async () => {
const result = await lastValueFrom(
this.apiHttpService
.get('{APIのURL}')
.pipe(map((response) => response.data)),
);
return result
});
},
ttl,
);
}
private interceptor() {
// トークンのキャッシュがある場合はセットする
this.apiHttpService.axiosRef.interceptors.request.use(
async (config) => {
const token = await this.cacheManager.get(
'cashkey-access-token',
);
if (token) {
config.headers.authorization = `Bearer ${token}`;
} else {
// ここでトークン取りにいったほうがいいかも?
}
return config;
},
(error) => Promise.reject(error),
);
// 401が返ったら、トークンを取得する
this.apiHttpService.axiosRef.interceptors.response.use(
(response) => response,
async (error) => {
if (error?.response?.status === 401) {
// 切れたトークンをキーにする
await memoizedRefreshToken(error.config.headers.authorization);
return await lastValueFrom(this.apiHttpService.request(error.config));
}
return Promise.reject(error);
},
);
}
private async refreshToken() {
const result = await lastValueFrom(
this.loginHttpService
.post('{リフレッシュトークンURL}')
.pipe(map((response) => response.data)),
);
const ttl = 10 * 60 * 1000;
await this.cacheManager.set(
'cashkey-access-token',
result.access_token,
ttl,
);
);
return result.access_token;
};
// メモ化
const memoizedRefreshToken = _.memoize(refreshToken);
}