0
0

Nest.jsで複数インスタンスでhttp通信して、トークン更新する

Last updated at Posted at 2023-10-13

やりたいこと

  • interceptorsを利用して、トークンのキャッシュがある場合はセットする
  • interceptorsを利用して、401が返って来た場合は、トークンを取り直す
  • トークン取得関数はメモ化する
  • APIの取得結果は一定時間キャッシュする

参考

事前準備

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);
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0