본문 바로가기

NestJs - 로그아웃 프로세스 구현 / docker를 사용한 redis 본문

개발/nest.js

NestJs - 로그아웃 프로세스 구현 / docker를 사용한 redis

자전하는명왕성 2023. 2. 22. 00:01

미래의 나에게

yarn add redis
yarn add cache-manager@4.1.0
yarn add cache-manager-redis-store@2.0.0
yarn add --dev @types/cache-manager-redis-store
yarn add jsonwebtoken

 

기존에 만들어 둔 로그인 프로세스의 경우,

refreshToken 이 만료되지 않으면 로그아웃이 되지 않는다는 단점이 있었다.

 

따라서 이번에는 제대로 된 로그아웃 프로세스를 구현하려고 하는데,

이번에는 redis 를 이용하여 API 를 만들어 보도록 한다.

 

about redis 

2023.02.13 - [코딩/알쓸코잡] - Redis 

 

Redis

Redis Redis(REmote DIctionary Server) 란, 비관계형 데이터베이스(NoSQL) 관리 시스템이다. 'key-value'로 데이터를 저장하며, 처리 및 저장이 빠르다는 장점이 있지만, 비영구적 속성을 지니기 때문에 서버가

zynoob.tistory.com

 

구현 과정

  • 1. 도커를 사용한 가상 머신 구동
  • 2. logout API 작성
  • 3. 작성한 API 를 통해 redis 에 accessToken, refreshToken 저장
  • 4. 검증 로직 변경 (토큰 검증이 필요한 API일 경우, 오류 반환)

 

1. 도커를 사용한 가상 머신 구동

위에 있는 라이브러리와 도커를 설정한다.

도커는 아래와 같이 구현된다.

services:
  my-backend:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ./src:/myfolder/src
    ports:
      - 3000:3000
    env_file:
      - ./.env.docker

  my-database:
    #platform:linux/86_64
    image: mysql:latest
    environment:
      MYSQL_DATABASE: 'mydocker'
      MYSQL_ROOT_PASSWORD: 'root'
    ports:
      - 3306:3306

  my-redis:
    image: redis:latest
    ports:
      - 6379:6379

 

이제부터는 도커를 통해 세 가지의 컴퓨터를 구동시킨다고 보면 좋다.

이때, redis 의 포트는 6379 를 쓴다는 것을 기억해두자.

 

2. logout API 작성

도커가 정상적으로 구동이 되었다면, 이제 logout API 를 

이전에 로그인을 만들어 두었던 Auth.resolver 에 작성한다.

// auth.resolver.ts

@UseGuards(GqlAuthGuard('refresh'))
  @Mutation(() => String)
  logout(
    @Context() context: IContext, //
  ): Promise<string> {
    return this.authService.logout({ res: context.res });
  }
  
// 필요한 것은 refreshToken / accessToken 이므로, 
// 해당 데이터가 담겨있는 context 만 있어도 괜찮다.

 

3. 작성한 API 를 통해 redis 에 accessToken, refreshToken 저장

// auth.service.ts

import { Cache } from 'cache-manager';
import * as jwt from 'jsonwebtoken';

async logout({ res }): Promise<string> {
    const accessToken = res.req.headers.authorization.split(' ')[1];
    const refreshToken = res.req.headers.cookie.split('=')[1];

    try {
	// jsonwebtoken 을 활용한 검증 로직, 이떄 해당 로직이 실패하면 오류를 반환하게 된다.
      const jwtAccess = jwt.verify(accessToken, process.env.JWT_ACCESS_KEY); 
      const jwtRefresh = jwt.verify(refreshToken, process.env.JWT_REFRESH_KEY);

	// Cache-manager 을 통한 redis 저장 로직. (key, value, ttl)을 저장한다.
    // accessToken 저장
      await this.cacheManager.set(`accessToken:${accessToken}`,
      'accessToken', {
      // 토큰에 저장된 만료 시간을 의미한다. 만료 시간 - 시작 시간
        ttl: jwtAccess['exp'] - jwtAccess['iat'], 
      });
      
	// refreshToken 저장
      await this.cacheManager.set(
        `refreshToken:${refreshToken}`,
        'refreshToken',{
          ttl: jwtRefresh['exp'] - jwtRefresh['iat'],
        },
      );
    } catch (error) {
      throw new UnauthorizedException('로그아웃에 실패하였습니다.');
    }

    return '로그아웃에 성공했습니다.'; // 위 try&catch문이 정상적으로 작동할 경우 리턴
  }

 

4. 검증 로직 변경 (토큰 검증이 필요한 API일 경우, 오류 반환)

// jwt-access.strategy.ts

import { CACHE_MANAGER, Inject, UnauthorizedException } from '@nestjs/common'; // CACHE_MANAGER
import { PassportStrategy } from '@nestjs/passport';
import { Cache } from 'cache-manager';	// cache-manager 
import { ExtractJwt, Strategy } from 'passport-jwt';

export class JwtAccessStrategy extends PassportStrategy(Strategy, 'access') {
  constructor(
    @Inject(CACHE_MANAGER) // redis 에 접근하기 위한 DI
    private readonly cacheManager: Cache, // redis 에 접근하기 위한 DI
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_ACCESS_KEY,
      passReqToCallback: true, // 요청이 true 일 시, callback함수(여기서는 validate)로 전달
    });
  }

	// 인자로는 req 를 추가로 받아, redis에 저장된 토큰이 존재하는지 파악한다.
  async validate(req, payload) {
    const accessToken = req.headers.authorization.split(' ')[1];
    const redisAccessToken = await this.cacheManager.get(
      `accessToken:${accessToken}`,
    );

	// redis에 해당 accessToken이 저장되어 있다면, 해당 오류를 반환한다.
    if (redisAccessToken)
      throw new UnauthorizedException('이미 로그아웃된 계정입니다.');

    return {
      email: payload.email,
      id: payload.sub,
    };
  }
}

 

playground 에서 로그인을 요청한 경우 

 

도커 내 redis 저장소에 접근해서 키 값을 확인한 이미지

 

docker ps // docker 내 컴퓨터 파악
docker exec -it 'container' /bin/bash // 해당 컨테이너에 접근
redis-cli // redis 저장소 내에 접근
key * // redis에 저장된 모든 키값 확인

 

 

 

로그아웃 후, 토큰이 필요한 타 API 를 요청한 경우

Comments