Post

[Nest.js 학습일기] 로그인/회원가입 구현 (-토큰 발급까지)

[Nest.js 학습일기] 로그인/회원가입 구현 (-토큰 발급까지)

진행 과정

  1. CLI활용해서 auth 관련 module을 만든다.
  2. @nestjs/jwt와 bcrypt 설치
  3. service => controller 순으로 코드를 작성한다. (import는 module파일 확인)

auth.service.ts

만들어야하는 기능은 다음과 같다.

  1. register (회원가입)
    • 이메일, 닉네임, 비밀번호를 입력받고 회원가입
    • 회원가입 이후 토큰을 발급하여 바로 로그인 처리한다.
  2. login
    • 이메일과 비밀번호를 통해 사용자 검증을 진행한다.
    • 검증이 완료되면 토큰을 반환한다.
  3. 토큰 반환
    • 토큰을 반환하는 로직이다.
  4. 토큰 생성
    • 3에서 필요한 토큰을 생성하는 로직이다.
  5. 사용자 검증 과정
    • 사용자가 존재하는지 확인한다.
    • 비밀번호가 일치하는지 확인한다.
    • 둘다 유효하다면 사용자 정보와 토큰을 반환한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
@Injectable()
export class AuthService {
  constructor(
    // jwtService를 주입하기 위해선 summary.module에 imports 해야한다.
    private readonly jwtService: JwtService,
    private readonly usersService: UsersService
  ) {}
  /**
   * 만드려는 기능
   * 1. register (email 기반)
   * - 이메일 닉네임 password 입력 받는다.
   * - 생성이 완료된 경우 token을 반환하여 바로 로그인 처리
   *
   * 2. 로그인
   * - 이메일과 pw로 사용자 검증
   * - 검증이 완료되면 토큰 반환
   *
   * 3. 토큰 반환 (LoginUser)
   * - 토큰을 반환하는 로직
   *
   * 4. 토큰 생성
   * - 3에서 필요한 access, refreshToken을 sign하는 로직
   *
   * 5. 로그인 검증 과정 (authenticateWithEmailAndPassword)
   * - 사용자가 존재하는지 확인
   * - 비밀번호 일치여부 확인
   * - 통과된 경우 사용자 정보 반환 + 토큰
   */

  /**
   * Payload에 들어갈 정보
   * 1. 이메일
   * 2. sub => id
   * 3. type (access or refresh)
   * signToken => 토큰을 발급하는 함수.
   */
  
  // Pick 유틸리티를 사용하기 위해선 UsersModel을 가져와야하는데.
  // 이는 해당 모듈을 export하는 코드를 작성해야함. (usersModule 에서 exports)
  /*
  @Module({
  imports: [
    TypeOrmModule.forFeature([UsersModel]),
  ],
  // export 해야 다른 모듈에서 사용 가능
  exports: [
    UsersService,
  ],
  controllers: [UsersController],
  providers: [UsersService],
  })
  export class UsersModule {}
   */
  
  signToken(user: Pick<UsersModel, 'email' | 'id'>, isRefreshToken: boolean) {
    const payload = {
      email: user.email,
      id: user.id,
      type: isRefreshToken ? 'refresh' : 'access',
    };
    return this.jwtService.sign(payload, {
      secret: JWT_SECRET,
      expiresIn: isRefreshToken ? 3600 : 300,
    });
  }
  
  // 로그인시 토큰을 발급하는 함수
  loginUser(user: Pick<UsersModel, 'email' | 'id'>) {
    return {
      accessToken: this.signToken(user, false),
      refreshToken: this.signToken(user, true)
    }
  }

  // 사용자 검증 과정
  async authenticateWithEmailAndPassword(user: Pick<UsersModel, 'email' | 'password'>) {
    // 1. 사용자 정보 확인
    const existingUser = await this.usersService.getUserByEmail(user.email);
    if (!existingUser) {
      throw new UnauthorizedException('존재하지 않는 사용자입니다.');
    }

    // 2. 비밀번호의 비교
    // 앞엔 일반 비밀번호 뒤엔 hash값
    const checkPass = await bcrypt.compare(user.password, existingUser.password);
    if (!checkPass) {
      throw new UnauthorizedException('비밀번호가 틀렸습니다.')
    }

    return existingUser;
  }

  // 로그인시 토큰 발급 함수와 사용자 검증 과정을 둘다 활용한 함수로 로그인 기능
  async loginWithEmail(user: Pick<UsersModel, 'email' | 'password'>) {
    const existingUser = await this.authenticateWithEmailAndPassword(user);
    return this.loginUser(existingUser);
  }
 
  // 회원가입
  async registerWithEmail(user: Pick<UsersModel, 'email' | 'password' | 'nickname'>) {
    // bcrypt의 경우 해시화 하고 싶은 패스워드 , round를 변수로 적는다.
    // rounds는 hash에 소요되는 시간을 의미한다.
    const hash = await bcrypt.hash(user.password, HASH_ROUNDS);

    const newUser = await this.usersService.createUser({
      ...user,
      password: hash,
    });
    return this.loginUser(newUser);
  }
}

auth.controller.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Controller('summary')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('login/email')
  loginEmail(
    @Body('email') email: string,
    @Body('password') password: string,
  ) {
    return this.authService.loginWithEmail({email, password});
  }

  @Post('register/email')
  registerEmail(
    @Body('email') email: string,
    @Body('password') password: string,
    @Body('nickname') nickname: string,
  ) {
    return this.authService.registerWithEmail({email, password, nickname})
  }
}
This post is licensed under CC BY 4.0 by the author.