본문 바로가기

Backend/Backend Engineering

Nest.js를 활용하여 Login, AuthGuard 구현하기

반응형

 

Login

  1. Basic $token으로 이메일, 패스워드 요청
  2. Basic $token인지 검사
  3. base64로 된 요청값을 utf-8을 이용하여 디코딩
  4. 디코딩 된 값은 <email>:<password> 형태로 되어 있음
  5. 이메일, 패스워드 검사 후 access, refresh token 발급

 

 

AuthController

// basic token
@Post('login') // 로그인
loginUser(@Headers('authorization') token: string) {
  return this.authService.login(token);
}
  • Headers pipe에서 authorization 헤더를 받아옴
  • 받은 데이터를 AuthService에 넘겨줌

 

AuthService

async login(token: string) {
  const { email, password } = this.parseBasicToken(token);
  const user = await this.authenticate(email, password);

  return {
    refreshToken: await this.issueToken(user, true),
    accessToken: await this.issueToken(user, false),
  };
}
  • parseBasicToken 메서드에서 Basic Token인지 검사
    parseBasicToken(rawToken: string) {
      const basicSplit = rawToken.split(' ');
    
      if (basicSplit.length !== 2) {
        throw new BadRequestException('토큰 포맷이 잘못됐습니다.');
      }
    
      const [basic, token] = basicSplit;
      if (basic.toLowerCase() !== 'basic') {
        throw new BadRequestException('토큰 포맷이 잘못됐습니다.');
      }
    
      const decoded = Buffer.from(token, 'base64').toString('utf-8');
    
      const tokenSplit = decoded.split(':');
      if (tokenSplit.length !== 2) {
        throw new BadRequestException('토큰 포맷이 잘못됐습니다.');
      }
    
      const [email, password] = tokenSplit;
      return {
        email,
        password,
      };
    }
    
    • 토큰 타입과 Basic인지 검사
    • base64로 인코딩 되어 있는 토큰값을 utf-8을 이용해서 디코딩
    • 디코딩 된 값은 <email>:<password> 형태로 되어 있음
    • 제대로 된 값이 들어왔는지 검사 후 반환
  • parseBasicToken
  • authenticate 메서드에서 email과 password를 검사
    async authenticate(email: string, password: string) {
      const user = await this.userRepository.findOne({
        where: {
          email,
        },
      });
    
      if (!user) {
        throw new BadRequestException('잘못된 로그인 정보입니다.');
      }
    
      const passOk = await bcrypt.compare(password, user.password);
    
      if (!passOk) {
        throw new BadRequestException('잘못된 로그인 정보입니다.');
      }
    
      return user;
    }
    
    • 이메일이 있는지, 올바른 패스워드인지 검사
    • 올바른 이메일과 패스워드라면 user를 반환
  • authenticate
  • 검사가 모두 다 통과하면 issuToken 메서드에서 access, refresh 토큰 발급
    async issueToken(user: { id: number; role: Role }, isRefreshToken: boolean) {
      const refreshTokenSecret = this.configService.get<string>(
        EnvVariableKeys.refreshTokenSecret,
      );
      const accessTokenSecret = this.configService.get<string>(
        EnvVariableKeys.accessTokenSecret,
      );
      return await this.jwtService.signAsync(
        {
          sub: user.id,
          role: user.role,
          type: isRefreshToken ? 'refresh' : 'access',
        },
        {
          secret: isRefreshToken ? refreshTokenSecret : accessTokenSecret,
          expiresIn: isRefreshToken ? '24h' : 300,
        },
      );
    }
    
    • 환경변수를 관리하는 모듈에서 JwtSecret Key를 가져온 후 AccessToken, RefreshToken을 발급
    • 파라미터로 받은 isRefreshToken이 true라면 RefreshToken을, 아니라면 AccessToken 발급
    • 발급 받은 토큰을 반환

 


 

Authorization

 

AuthGuard는 Login, Register API를 제외한 모든 API에 대해서 검사를 진행하기 때문에 전역적으로 등록을 해주어야 함

AppModule

import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth/guard/auth.guard';

providers: [
  {
    provide: APP_GUARD,
    useClass: AuthGuard,
  },
],

이렇게 providers에 전역적으로 AuthGuard를 등록을 해주면 된다

 

 

PublicDecorator

전역적으로 AuthGuard를 등록을 하게 되면 Login API나 Register API도 AuthGuard를 타기 때문에 특별하게 다른 설정을 해주어야 한다

public.decorator.ts

import { SetMetadata } from '@nestjs/common';

export const Public = (data: any) => SetMetadata('public', data);

위와 같이 커스텀 데코레이터를 만들어준 후에 AuthController에서 Login, Register 메서드에 해당되는 곳에 데코레이터를 등록만 해주면 된다

// basic token
@Public(true)
@Post('register') // 회원가입
async registerUser(@Headers('authorization') token: string) {
  return await this.authService.register(token);
}

// basic token
@Public(true)
@Post('login') // 로그인
loginUser(@Headers('authorization') token: string) {
  return this.authService.login(token);
}

AuthGuard(auth.guard.ts)

  • 클래스명을 AuthGuard로 만들었는데, 이 클래스명은 @nestjs/core에도 있는 클래스이기 때문에 제대로 import를 해야됨
  • 아래와 같이 reflector를 이용해서 PublicDecorator 붙여준 메서드는 무조건 true를 반환하여 AuthGuard를 타게 되더라도 통과가 될 수 있게 설정을 해주면 된다
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

export class AuthGuard implements CanActivate {
	constructor(private readonly reflector: Reflector) {}
	
	canActivate(context: ExecutionContext): boolean {
		// 만약에 Public 데코레이터가 존재하면 true를 반환
		const isPublic = this.reflector.get('public', context.getHandler());
		if (isPublic && typeof isPublic === 'boolean') {
			return true;
		}
		
		const req = context.switchToHttp().getRequest();
		
		if (!req.user || req.user.type !== 'access') {
			return false;
		}
		
		return true;
	}
}
반응형