개발

github oauth2 클라이언트와 서버 구현해보기 -3

배우겠습니다 2024. 2. 15. 17:21

사용하는 것

nest

redis

 

저번에 한 것

가입시 db에 유저정보를 넣어줬다.

jwt access token을 쿠키에 넣어줬다.

 

이번에 할것

프로젝트에 redis를 설정한다.

jwt refresh token을 생성한다.

jwt refresh token을 검증한다.

 

왜 redis를 썻는가.

UserEntity에 이전에 설정한 postgresql에 refresh토큰 col을 적용한다면 코드작성이 복잡해지며, 투머치하다고 생각한다.

따로 테이블을 만드는 것 역시 투머치하다고 생각한다.

postgresql엔 상대적으로 복잡한 쿼리나 모델이 필요한 데이터를 넣어줄 것이다.

단순 key:value로만 넣어줄 것이므로 redis가 적당하다고 생각한다.

하지만 이 경우 데이터가 손상되지 않게 주의해야하고 백업이 필요하다.

 

redis docker

docker pull redis

터미널에서 redis이미지를 받아온다.

redis:
    image: redis
    command: redis-server --requirepass 레디스비밀번호 --port 6379
    container_name: 
    hostname: 
    labels:
      - "name=redis"
      - "mode=standalone"
    volumes:
      - ./redis/data:/data

docker compose를 작성해준다.

 

app.module작성

import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { redisConfig } from './config/redis.config';

@Module({
  imports: [
    CacheModule.registerAsync(redisConfig),
  ],
})
export class AppModule {}

 

cachemodule에 redis설정을 넣어준다.

export const redisConfig: CacheModuleAsyncOptions = {
  isGlobal: true,
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => {
    const store = await redisStore({
      socket: {
        host: ,
        port: ,
      },
      password: ,
    });
    return {
      store: () => store,
    };
  },
  inject: [ConfigService],
};

redis설정은 자유롭게.

환경변수 적용을 위해 AsyncOption을 registerAsync에 넣어줬다.

 

jwt refresh token module

import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { JwtRefreshStrategy } from './jwt-refresh.strategy';
import { JwtRefreshService } from './jwt-refresh.service';
import { UsersModule } from 'src/users/users.module';

@Module({
  imports: [
    JwtModule.registerAsync({
      useFactory: async (configService: ConfigService) => {
        return {
          signOptions: { expiresIn: '30d' },
          secret: configService.get<string>('JWT_REFRESH_SECRET'),
        };
      },
      inject: [ConfigService],
    }),
  ],
  providers: [JwtRefreshStrategy, JwtRefreshService],
  exports: [JwtRefreshService],
})
export class JwtRefreshModule {}

accesstoken과 동일하게 module에서 jwt refresh token관련 정보를 config해준다.

 

strategy작성

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { Request } from 'express';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { JwtRefreshService } from './jwt-refresh.service';
import { PayLoadType } from '../type';

@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(
  Strategy,
  'jwt-refresh',
) {
  constructor(
    configService: ConfigService,
    private jwtRefreshService: JwtRefreshService,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // 클라이언트는 authorization: Bearer blabla 형식 헤더를 보낸다.
      ignoreExpiration: false,
      secretOrKey: configService.get<string>('JWT_REFRESH_SECRET'),
      passReqToCallback: true, // 추출되고 디코딩된 토큰이 아닌 쌩 토큰을 이용해야한다.
    });
  }

  async validate(req: Request, payload: PayLoadType) {
    const authorization = req.headers?.authorization;
    if (!authorization || authorization.split(' ').length < 2) {
      throw new HttpException('인증 실패', HttpStatus.UNAUTHORIZED);
    }
    const refreshToken = authorization.split(' ')[1];
    if (!this.jwtRefreshService.matchRefreshToken(payload.key, refreshToken)) {
      throw new HttpException('인증 실패', HttpStatus.UNAUTHORIZED);
    }
    return payload;
  }
}

리프레시 토큰의 검증은, 요청보낸 유저의 쿠키의 리프레시토큰과 db(여기선 redis)의 유저에 대응하는 리프레시토큰이 맞는지 추가로 검증한다.

passReqToCallback: true로 설정해 날것의 req를 받아서 raw refreshtoken을 추출해야한다.

 

 

service 작성

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class JwtRefreshService {
  constructor(private jwtService: JwtService) {}

  login(payload: PayLoadType) {
    return {
      refreshToken: this.jwtService.sign(payload),
    };
  }
}

 

JwtRefreshService에 refresh토큰 검증과 생성로직작성

import { Inject, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PayLoadType } from '../type';
import { CACHE_MANAGER, CacheStore } from '@nestjs/cache-manager';
import * as bcrypt from 'bcryptjs';

@Injectable()
export class JwtRefreshService {
  constructor(
    private jwtService: JwtService,
    @Inject(CACHE_MANAGER) private cacheManager: CacheStore,
  ) {}

  login(payload: PayLoadType) {
    return {
      refreshToken: this.jwtService.sign(payload),
    };
  }
  async setRefreshToken(key: string, refreshToken: string) {
    const hashedToken = await bcrypt.hash(refreshToken, 5);
    return await this.cacheManager.set<string>(key, hashedToken);
  }
  async matchRefreshToken(key: string, refreshToken: string): Promise<boolean> {
    const hashedToken = await this.cacheManager.get<string>(key);
    return await bcrypt.compare(refreshToken, hashedToken);
  }
  // decodeRefreshToken(refreshToken: string) {
  //   return this.jwtService.decode(refreshToken);
  // }
}

리프레시 토큰을 redis에 넣어준다. 따라서 보안을 강화하기 위해 bcrypt로 리프레시 토큰을 추가로 암호화한다.

https://nayounsang1.tistory.com/71  해당 이슈로 인해 bcryptjs모듈 사용을 추천한다.

우린 app.module에 넣은 redis설정으로 인해, cacheManager은 자동으로 우리가 설정한 redis에 데이터를 집어넣고 가져온다.

jwtrefreshmodule에 리프레시토큰 관련 설정을 넣어줬다. 따라서 jwtrefreshservice에서 작성하면 우린 쉽게 jwt토큰을 생성할 수 있다. 구조상으로도 리프레시관련된 동작을 리프레시토큰서비스에 넣어줘 깔끔해진다.

 

가입시 리프레시토큰 발급 및 설정

 @Get('callback')
  @UseGuards(AuthGuard('github'))
  async githubAuthCallback(
    @Req() req: Request,
    @Res({ passthrough: true }) res: Response,
  ): Promise<void> {
    const user = { key: req.user as string };
    const { accessToken } = this.jwtAccessService.login(user);
    const { refreshToken } = this.jwtRefreshService.login(user);
    res.cookie('access_token', accessToken);
    res.cookie('refresh_token', refreshToken);
    await this.jwtRefreshService.setRefreshToken(user.key, refreshToken);
    res.redirect('foobar');
  }

 

payload를 이용해 refreshToken을 생성한다.

cookie에 refreshToken을 넣어준다.

github login시 refreshToken을 redis에 넣어준다.

 

이제 우린 github로그인을 하면 유저정보를 가져와 db에 저장하고 access token과 refresh token을 쿠키에 받아올 수 있다.