안녕하세요 오늘은 저번 포스팅에서도 다루었고 대부분의 서비스에서 필수적인 기능인 인증/인가에 대한 보일러 플레이트를 개발해보겠습니다.

사이드 프로젝트를 하면서 인증/인가에 대해서는 나만의 보일러플레이트 코드를 개발해놔야겠다생각했었는데 이제야 개발하게 됬습니다.

일단 해당 포스팅에서는

  1. 클라이언트 단과의 통신에 대한 암호화는 신경쓰지 않고 개발합니다.
  2. 로그인, 회원가입기능을 개발합니다.
  3. jwt기반의 인증/인가 프로젝트를 개발합니다.
  4. nest, typescript, mysql를 사용하여 개발합니다.

병아리 개발자에게 많은 조언 부탁드립니다! :)

 

 

http 통신 인증에서의 암호화

http 통신시에 password를 암호화하기 위해 RSA암호화를 사용합니다.또한 password를 복호화 해야하기때문에 양방향 암호화 알고리즘을 사용합니다 ex) rsa알고리즘

1. Entity 정의하기

CREATE TABLE user(
user_id NVARCHAR(20) NOT NULL,
password char(60) NOT NULL,
role NVARCHAR(20) NOT NULL default basicuser,
CONSTRAINT admin PRIMARY KEY(user_id, system_name))

먼저 user Entity를 정의했습니다.

대부분의 프로젝트에서 필수적으로 있어야하는 user_id, password, role컬럼을 가지게 됩니다.

해당 프로젝트는 보일러플레이트 프로젝트이기때문에 user테이블에서 필수적인 컬럼만 가지도록 설계했습니다.

 

 

2. POST 회원가입(signup) 구현하기

회원가입(signup) 구현에서는 먼저

userId, password, role데이터를 수신합니다.

회원가입을 위한 DTO를 정의했습니다.

최소한의 유효성만을 체크하고 있습니다.

import { IsAlphanumeric, IsNotEmpty, Length } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty()
  @IsAlphanumeric()
  @Length(10, 15)
  userId: string;

  @IsNotEmpty()
  password: string;

  @IsNotEmpty()
  role: string;
}

 

 

먼저 해당 메소드 아래와 같은 로직을 가집니다.

  1. 회원기능을 담당하는 Account 클래스의 멤버변수를 초기화합니다.
  2. DB에 password를 저장하기 위해 단방향 암호화를 수행합니다.
  3. 암호화된 password와 회원 정보를 DB에 저장합니다.
async createAccount(createUserDto: CreateUserDto): Promise<User> {
        const account = new Account();
        account.setSignUpinfo(createUserDto);
        account.setHashPw()
        const newUser = await this.userRepository.createUser(account)
    return newUser;
    }

아래는 Account class코드입니다.

import { CreateUserDto } from "./dto/create-user.dto";
import { SignInUserDto } from "./dto/signIn-user.dto";
import { tLoginRes } from "./dto/types";
import * as bcrypt from 'bcrypt'
import * as jwt from 'jsonwebtoken';

export class Account {
    userId: string;
    private password: string;
    hashedPw: string;
    role: string;
    saltRounds = 10;

    setSignUpinfo(createUserDto: CreateUserDto){
        this.password = createUserDto.password
        this.role = createUserDto.role;
        this.userId = createUserDto.userId;
    }

    setHashPw(): boolean {
        const salt = bcrypt.genSaltSync(this.saltRounds);
        this.hashedPw = bcrypt.hashSync(this.password, salt);
        return true
    }
}

해당 클래스를 작성하면서 평문 pw , hashedPw(암호화된 pw)를 둘 다 멤버변수로 정희하다보니 네이밍이 혼동이 될 수도 있겠다는 생각이 자꾸 들었습니다.

hashedPw 멤버변수는 명확한데 password 멤버변수는 의미가 명확하지 않다는 느낌이 강하게 들어 더 고민을 해봐야할 것 같네요

 

 

3.  GET 로그인 API

import { IsAlphanumeric, IsNotEmpty, Length } from 'class-validator';

export class SignInUserDto {
  @IsNotEmpty()
  @IsAlphanumeric()
  @Length(10, 15)
  userId: string;

  @IsNotEmpty()
  password: string;
}

회원가입을 위한 DTO를 정의했습니다.

로그인이기때문에 id와 password만을 수신합니다.

 

async signIn(signInUserDto: SignInUserDto): Promise<tLoginRes> {
        const account = new Account();
        account.setSignInInfo(signInUserDto);
        const user = await this.userRepository.getUserById(account.userId);

        if (!account.comparePassword(user.password))
            throw new BadRequestException('password가 일치하지 않습니다.')
        if (!account.getJwt())
            throw new BadRequestException('token발급에 실패했습니다.')

    return account.getResform();
    }

회원가입 비즈니스 로직을 담당하는 메소드는 UserService.signIn()으로 이름을 정했습니다.

  1. 해당 user가 존재하는지 DB를 조회
  2. 수신한 password가 DB에 저장된 user의 password와 일치하는지 확인
  3. userId값을 가지고 있는 jwt를 발급
  4. 클라이언트 단에 데이터 송신
import { CreateUserDto } from "./dto/create-user.dto";
import { SignInUserDto } from "./dto/signIn-user.dto";
import { tLoginRes } from "./dto/types";
import * as bcrypt from 'bcrypt'
import * as jwt from 'jsonwebtoken';

export class Account {
    userId: string;
    private password: string;
    hashedPw: string;
    role: string;
    saltRounds = 10;
    token: string;

    setSignUpinfo(createUserDto: CreateUserDto){
        this.password = createUserDto.password
        this.role = createUserDto.role;
        this.userId = createUserDto.userId;
    }

    setHashPw(): boolean {
        const salt = bcrypt.genSaltSync(this.saltRounds);
        this.hashedPw = bcrypt.hashSync(this.password, salt);
        return true
    }

    setSignInInfo(signInUserDto: SignInUserDto){
        this.password = signInUserDto.password
        this.userId = signInUserDto.userId;
    }
    
    comparePassword(DbPw: string): boolean {
        return bcrypt.compareSync(this.password, DbPw);
    }

    getJwt(): boolean {
        const token = jwt.sign(
            {
                type: 'JWT',
                id: this.userId,
            },
            process.env.SECRET_KEY,
            {
                expiresIn: '60m',
                issuer: 'admin',
            }
        );
        this.token = token
        return true
    };

    getResform(): tLoginRes{
        return {
            userId: this.password,
            token: this.token
        }
    }
}

account class에 4개의 메소드가 추가되었습니다.

  • 로그인을 위해 멤버변수를 초기화하는 setSignInInfo 메소드
  • password가 유효한지 확인하는 comparePassword 메소드
  • 인증/인가를 위한 jwt를 발급하는 getJwt 메소드
  • 응답을 위한 형식의 object를 반환하는 getResform 메소드입니다.

개인적으로 객체 지향에서의 추상화, 캡슐화가 필요하다는 느낌을 강하게 받는 소스코드입니다 ㅜ.ㅜ

 

정리

보일러 플레이트를 개발하면서 아래와 같은 것들이 애매하고 궁금해지는 시간이었습니다.

  1. nest에서 타입을 정의하여 사용할 때 어떤 디렉토리에 격리하여 사용하는지
  2. 내가 사용한 Account와 같은 class를 정의하여 사용할 때 어떤 디렉토리에 격리하는게 좋을지
  3. account class를 어떻게 추상화, 캡슐화할 수 있을지

++네이밍은 항상 어렵군요

 

회사에서의 업무에서는 프로젝트에서 사용하는 기술스택을 하나도 사용하지 않다보니 프로젝트를 진행할 수록 고민이 많아지게 됩니다ㅜ

 

또한 현재는 아래와 같은 Access Token만을 사용하는 프로세스이지만

최종적으로는 아래와 같이 Refresh Token + Access Token 기반의 프로세스를 가진 프로젝트를 개발할 예정입니다 :)

 

해당 글은 시리즈로 작성할 예정이며 아래는 github repository 링크입니다.

많이 구경해주세용

 

 

GitHub - Lks9172/auth_boilerplate

Contribute to Lks9172/auth_boilerplate development by creating an account on GitHub.

github.com

 

반응형

+ Recent posts