To Do
(버전 및 환경 정리하기!)
1. resource 생성
** app.module.ts에 AuthModule이 잘 추가됐는지 확인해야함!
(아래와 비슷한 느낌으로!)
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [
TypeOrmModule.forRoot({
...
}),
AuthModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
2. 구현할 로직 정리
우리가 만드려는 기능
1) registerWithEmail
- email, nickname, password를 입력받고 사용자를 생성한다.
- 생성이 완료되면 accessToken과 refreshToken을 반환!
(회원가입 후 다시 로그인해주세요 <- 이런 쓸데없는 과정을 방지하기 위해서!)
2) loginWithEmail
- email, password를 입력하는 사용자 검증 진행
- 검증이 완료되면 accessToken과 refreshToken을 반환한다.
3) loginUser ( 반복적인 토큰 발급 제거하기 위해!)
- (1)과 (2)에서 필요한 accessToken과 refreshToken을 반환하는 로직
4) signToken
- (3)에서 필요한 accessToken과 refreshToken을 sign하는 로직
5) authenticateWithEmailAndPassword
- (2)에서 로그인을 진행할 때 필요한 기본적인 검증 진행
1. 사용자가 존재하는지 확인 (email)
2. 비밀번호가 맞는지 확인
3. 모두 통과되면 찾은 사용자 정보 반환
4. loginWithEmail에서 반환된 데이터를 기반으로 토큰 생성
3) 필수 패키지 설치
yarn add @nestjs/jwt bcrypt
4) AuthModule에 jwt import 하기
// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [JwtModule.register({})],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}
// src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService) {}
}
jwt 사용할 준비 끝!
5) auth.service.ts 구현
5-1. signToken
페이로드를 토큰으로 변환하는 역할.
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,
});
}
}
5-2. loginUser
유저가 로그인 된 경우, 유저의 email과 id를 받아 토큰을 발급하는 로직이다.
loginUser(user: Pick<UsersModel, 'email' | 'id'>) {
return {
accessToken: this.signToken(user, false),
refreshToken: this.signToken(user, true),
};
}
5-3. authenticateWithEmailAndPassword
유저가 로그인을 시도할 경우, email과 password를 받아서 검증을 진행하는 메서드
async authenticateWithEmailAndPassword(
user: Pick<UsersModel, 'email' | 'password'>,
) {
/**
* 1. 사용자가 존재하는지 확인 (email)
* 2. 비밀번호가 맞는지 확인
* 3. 모두 통과되면 찾은 사용자 정보 반환
*/
const existingUser = await this.usersService.getUserByEmail(user.email);
if (!existingUser) {
throw new UnauthorizedException('존재하지 않는 사용자입니다.');
}
const ok = await bcrypt.compare(user.password, existingUser.password);
if (!ok) {
throw new UnauthorizedException('비밀번호가 일치하지 않습니다.');
}
return existingUser;
}
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UsersModel } from './entities/users.entity';
import { Repository } from 'typeorm';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(UsersModel)
private readonly usersRepository: Repository<UsersModel>,
) {}
async getUserByEmail(email: string) {
return await this.usersRepository.findOne({
where: {
email,
},
});
}
}
5-4. loginWithEmail
실제 로그인을 하는 메서드
유저로부터 email과 passoword를 받으면 , authenticateWithEmailAndPassword 메서드로 정보를 검증한 뒤, loginUser 메서드를 통해 토큰을 반환한다.
async loginWithEmail(user: Pick<UsersModel, 'email' | 'password'>) {
const existingUser = await this.authenticateWithEmailAndPassword(user);
return this.loginUser(existingUser);
}
5-5. registerWIthEmail
회원가입을 하는 로직.
async registerWithEmail(
user: Pick<UsersModel, 'email' | 'nickname' | 'password'>,
) {
/*
* 1) registerWithEmail
* - email, nickname, password를 입력받고 사용자를 생성한다.
* - 생성이 완료되면 accessToken과 refreshToken을 반환!
* (회원가입 후 다시 로그인해주세요 <- 이런 쓸데없는 과정을 방지하기 위해서!)
* */
const hashedPassword = await bcrypt.hash(user.password, HASH_ROUNDS);
const existingUser = await this.usersService.createUser(user);
return this.loginUser(existingUser);
}
전체 코드
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersModel } from 'src/users/entities/users.entity';
import { HASH_ROUNDS, JWT_SECRET } from './const/auth.const';
import { UsersService } from 'src/users/users.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
private readonly usersService: UsersService,
) {}
/**
* 우리가 만드려는 기능
* 1) registerWithEmail
* - email, nickname, password를 입력받고 사용자를 생성한다.
* - 생성이 완료되면 accessToken과 refreshToken을 반환!
* (회원가입 후 다시 로그인해주세요 <- 이런 쓸데없는 과정을 방지하기 위해서!)
*
* 2) loginWithEmail
* - email, password를 입력하는 사용자 검증 진행
* - 검증이 완료되면 accessToken과 refreshToken을 반환한다.
*
* 3) loginUser ( 반복적인 토큰 발급 제거하기 위해!)
* - (1)과 (2)에서 필요한 accessToken과 refreshToken을 반환하는 로직
*
* 4) signToken
* - (3)에서 필요한 accessToken과 refreshToken을 sign하는 로직
*
* 5) authenticateWithEmailAndPassword
* - (2)에서 로그인을 진행할 때 필요한 기본적인 검증 진행
* 1. 사용자가 존재하는지 확인 (email)
* 2. 비밀번호가 맞는지 확인
* 3. 모두 통과되면 찾은 사용자 정보 반환
* 4. loginWithEmail에서 반환된 데이터를 기반으로 토큰 생성
*/
/**
* Payload에 들어갈 정보
*
* 1) email
* 2) sub -> id
* 3) type :'access' | 'refresh'
*/
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. 사용자가 존재하는지 확인 (email)
* 2. 비밀번호가 맞는지 확인
* 3. 모두 통과되면 찾은 사용자 정보 반환
*/
const existingUser = await this.usersService.getUserByEmail(user.email);
if (!existingUser) {
throw new UnauthorizedException('존재하지 않는 사용자입니다.');
}
const ok = await bcrypt.compare(user.password, existingUser.password);
if (!ok) {
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' | 'nickname' | 'password'>,
) {
/*
* 1) registerWithEmail
* - email, nickname, password를 입력받고 사용자를 생성한다.
* - 생성이 완료되면 accessToken과 refreshToken을 반환!
* (회원가입 후 다시 로그인해주세요 <- 이런 쓸데없는 과정을 방지하기 위해서!)
* */
const hashedPassword = await bcrypt.hash(user.password, HASH_ROUNDS);
const existingUser = await this.usersService.createUser({
...user,
password: hashedPassword,
});
return this.loginUser(existingUser);
}
}
6) auth.controller.ts 구현
import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
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('nickname') nickname: string,
@Body('email') email: string,
@Body('password') password: string,
) {
return this.authService.registerWithEmail({ nickname, email, password });
}
}