- Published on
NestJS로 구축하는 사용자 인증 시스템-회원가입 및 로그인 구현
- Authors
- Name
- 황도연
NestJS로 구축하는 사용자 인증 시스템: 회원가입 및 로그인 구현
GitHub Repository: NestJS backend login
필요한 기술 스택
- NestJS: Node.js 서버 사이드 프레임워크
- TypeORM: 데이터베이스 작업을 위한 ORM
- JWT(JSON Web Tokens): 사용자 인증 및 권한 부여
- bcrypt: 비밀번호 암호화
회원가입 기능 구현
1단계: 사용자 엔티티 생성
User
엔티티는 사용자 정보를 저장합니다.
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'
import * as bcrypt from 'bcrypt'
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column({ unique: true, type: 'text' })
name: string
@Column({ unique: true, type: 'text' })
email: string
@Column()
password: string
@Column()
company: string
@Column({ nullable: true })
job: string
@Column({ unique: true })
nickname: string
@CreateDateColumn()
createdAt: Date
@UpdateDateColumn()
updatedAt: Date
// isAdmin: boolean
@Column({ default: false })
isAdmin: boolean
async validatePassword(password: string): Promise<boolean> {
return bcrypt.compare(password, this.password)
}
}
DTO
LoginDto
import { ApiProperty } from '@nestjs/swagger'
import { IsEmail, IsString } from 'class-validator'
export class LoginDto {
@ApiProperty({
description: 'email',
default: 'test2@gmail.com',
required: true,
})
@IsEmail()
email!: string
@ApiProperty({
description: 'password',
default: 'password',
required: true,
})
@IsString()
password!: string
}
SignUpDto
import { ApiProperty } from '@nestjs/swagger'
import { IsEmail, IsString } from 'class-validator'
export class SignupDto {
@ApiProperty({
description: 'name',
default: 'TESTER',
required: true,
})
@IsString()
name!: string
@ApiProperty({
description: 'email',
default: 'test2@gmail.com',
required: true,
})
@IsEmail()
email!: string
@ApiProperty({
description: 'company',
default: 'company',
required: true,
})
@IsString()
company!: string
@ApiProperty({
description: 'job',
default: 'job',
required: true,
})
@IsString()
job: string
@ApiProperty({
description: 'nickname',
default: 'nickname',
required: true,
})
@IsString()
nickname: string
@ApiProperty({
description: 'password',
default: 'password',
required: true,
})
@IsString()
password!: string
}
2단계: 회원가입 로직
signup
메소드는 회원가입을 처리합니다.
public async signup(payload: SignupDto): Promise<Token> {
const { name, email, company, job, nickname, password } = payload;
const hashedPassword = await generatePassword(password);
const token: Token = {
accessToken: '',
refreshToken: '',
};
const addUserValue = this.userRepository.create({
name,
email,
company,
job,
nickname,
password: hashedPassword,
});
const addUserResult = await this.userRepository.save(addUserValue);
const refreshToken = generateRandom(32);
const expiredAt = new Date(Date.now() + 3600 * 1000 * 24);
const addRefreshTokenValue = this.refreshTokenRepository.create({
userId: addUserResult.id,
refreshToken: refreshToken,
expiredAt: expiredAt,
isBlocked: false,
});
await this.refreshTokenRepository.addRefreshToken(addRefreshTokenValue);
token.tokenPayload = {
id: addUserResult.id,
email: addUserResult.email,
};
token.accessToken = generateToken(token.tokenPayload);
token.refreshToken = refreshToken;
return token;
}
3단계: JWT 생성
JWT 토큰 생성 예시입니다.
token.accessToken = generateToken(token.tokenPayload)
token.refreshToken = generateRandom(32)
로그인 기능 구현
1단계: 사용자 검증
login
메소드에서는 사용자를 검증합니다.
public async login(payload: LoginDto): Promise<Token> {
const { email, password } = payload;
const user: User | undefined = await this.userRepository.findOne({
where: { email },
});
const comparePassword = await validatePassword(password, user.password);
}
2단계: JWT 토큰 생성
로그인 성공 시 JWT 토큰을 생성합니다.
token.accessToken = generateToken({ id: user.id, email: user.email })
token.refreshToken = generateRandom(32)
3단계: 로그인 코드와 트랜잭션 관리
트랜잭션 관리 예시와 로그인 코드입니다.
public async login(payload: LoginDto): Promise<Token> {
const { email, password } = payload;
const user: User | undefined = await this.userRepository.findOne({
where: { email },
});
if (!user) {
throw new Error('USER_NOT_FOUND');
}
const comparePassword = await validatePassword(password, user.password);
if (!comparePassword) {
throw new Error('USER_NOT_FOUND');
}
const token: Token = {
accessToken: '',
refreshToken: '',
};
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const refreshToken = generateRandom(32);
const expiredAt = new Date(Date.now() + 3600 * 1000 * 24);
const addRefreshTokenValue = this.refreshTokenRepository.create({
userId: user.id,
refreshToken,
isBlocked: false,
expiredAt,
});
const addRefreshToken =
await this.refreshTokenRepository.addRefreshToken(addRefreshTokenValue);
if (!addRefreshToken) {
throw new Error('AFFECTED_ROWS_ERROR');
}
token.tokenPayload = {
id: user.id,
email: user.email,
isAdmin: user.isAdmin,
};
token.accessToken = generateToken(token.tokenPayload);
token.refreshToken = refreshToken;
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
return token;
}
리프레시 토큰 관리: Redis 사용의 이점
리프레시 토큰을 데이터베이스에 저장하는 대신 Redis와 같은 인-메모리 데이터 저장소를 사용하는 것이 좋습니다.
- 성능 향상 : Redis는 인-메모리 데이터 저장소로, 빠른 읽기 및 쓰기 작업을 제공합니다. 이는 특히 대규모 트래픽과 높은 요청률을 처리할 때 유용합니다.
- 확장성 : Redis는 확장성이 뛰어나며, 클러스터링을 통해 더 큰 데이터 셋과 높은 처리량을 쉽게 관리할 수 있습니다.
- 자동 만료 처리 : Redis는 데이터에 대한 만료 시간을 설정할 수 있어, 리프레시 토큰과 같은 임시 데이터를 자동으로 관리할 수 있습니다. 이는 토큰이 만료되면 자동으로 삭제되어 시스템의 안정성을 높이는 데 도움이 됩니다.
- 보안 강화 : 리프레시 토큰은 사용자의 세션을 재생성할 수 있는 중요한 정보입니다. Redis와 같은 별도의 시스템에 저장함으로써, 데이터베이스가 해킹당할 경우의 리스크를 줄일 수 있습니다.