상똥이의 Back-End 공부방
[Nest.js] 토큰으로 로그인, 로그아웃 구현하기 본문
목표
1. JWT를 사용해서 로그인, 로그아웃을 처리한다.
2. 로그인 시 토큰을 발급한다.
3.필요시 토큰을 새로 발급한다.
목차
0. 기본 설정 (스키마, 회원가입 로직)
1. 로그인 시 토큰 발급과 저장
2. 로그아웃 시 토큰 삭제
3. 로그인 유지를 위한 리프레시 토큰 발급
[0. 기본 설정]
1. 유저 스키마
model User {
id Int @id @default(autoincrement())
email String @unique
password String
refreshToken String?
}
2. 회원가입 로직
//user.service.ts
async createUser(dto: User) {
try {
const check = await this.prismaService.user.findUnique({
where: { email: dto.email },
});
if (check) throw new BadRequestException('이미 사용중인 이메일입니다.');
const salt = bcrypt.genSaltSync(12);
const hash = bcrypt.hashSync(dto.password, salt);
return await this.prismaService.user.create({
data: {
email: dto.email,
password: hash,
},
});
} catch (e) {
console.log(e);
}
}
// user.controller.ts
@Post('sign-up')
async createUser(@Body() dto: User) {
return await this.userService.createUser(dto);
}
// user.dto.ts
export class User {
email: string;
password: string;
}
[1. 로그인 시 토큰 발급과 저장]
0. 로그인 처리 과정 요약
- 클라이언트에서 서버로 로그인 요청
- 아이디와 비밀번호 확인
- 액세스토큰, 리프레시 토큰 생성
- 리프레시 토큰 데이터베이스에 저장
- 액세스토큰, 리프레시 토큰 클라이언트에 쿠키로 전달
2. 토큰 발급 로직
- auth.service.ts
// .env파일에 JWT_SECRET을 설정한다. 필자는 8자리 무작위 문자 조합으로 설정했다.
private jwtSecret: string;
constructor(
private readonly jwtService: JwtService,
private readonly prismaService: PrismaService,
) {
this.jwtSecret = process.env.JWT_SECRET;
}
// 토큰 생성 로직
async generateToken(payload: jwt.JwtPayload, type: string): Promise<string> {
// type이 access면 유효기간을 짧게 설정
// type이 refresh면 유효기간을 길게 설정
const expiresIn =
type === 'access' ? '10m' : type === 'refresh' ? '15d' : null;
if (!expiresIn)
throw new BadRequestException('존재하지 않는 액세스 토큰 타입입니다.');
// 토큰 생성 후 반환
const token = jwt.sign(payload, this.jwtSecret, { expiresIn });
return token;
}
3. 로그인 로직
- user.service.ts
async signIn(dto: User): Promise<{ accessT: string; refreshT: string }> {
try {
// 사용자 확인
const user = await this.prismaService.user.findUnique({
where: { email: dto.email },
});
if (!user) throw new NotFoundException('존재하지 않는 사용자입니다.');
// 비밀번호 확인
const comparePassword = await bcrypt.compare(dto.password, user.password);
if (!comparePassword)
throw new BadRequestException('비밀번호가 일치하지 않습니다.');
// 페이로드(담을 정보) 설정 후 authService의 generateToken에 전달한다
// accessToken, refreshToken에 따라서 type 설정을 다르게 해준다
const payload = { type: 'user', id: user.id, email: dto.email };
const accessT = await this.authService.generateToken(payload, 'access');
const refreshT = await this.authService.generateToken(payload, 'refresh');
// 발급된 리프레시토큰을 유저 데이터베이스에 저장
await this.prismaService.user.update({
where: { id: user.id },
data: { refreshToken: refreshT },
});
// 두 토큰을 모두 반환
return { accessT, refreshT };
} catch (e) {
console.log(e);
}
}
- user.controller.ts
import { Body, Controller, Delete, Post, Req, Res } from '@nestjs/common';
import { UserService } from './user.service';
import { CookieOptions, Request, Response } from 'express';
import { User } from './user.dto';
import { AuthService } from '../auth/auth.service';
...
// constructor에서 쿠키옵션을 설정해준다
// 액세스토큰은 유효기간을 짧게 하고, 리프레시토큰은 유효기간을 길게 잡는다
// maxAge 설정은 jwt 생성 시에 설정하는 expire보다 우선한다
// 개발환경에서는 secure를 false로 하는게 편하지만 배포시 true로 설정하여 https와 상호작용하도록 한다
private accessCookieOptions: CookieOptions;
private refreshCookieOptions: CookieOptions;
constructor(
private readonly userService: UserService,
private readonly authService: AuthService,
) {
this.accessCookieOptions = {
maxAge: 1000 * 60 * 10, // 유효기간 10분
httpOnly: true,
secure: false,
};
this.refreshCookieOptions = {
maxAge: 1000 * 60 * 60 * 24 * 15, // 유효기간 15일
httpOnly: true,
secure: false,
};
}
@Post('sign-in')
async signIn(@Body() dto: User, @Res() res: Response) {
const token = await this.userService.signIn(dto);
// Response를 사용해 쿠키를 설정한다
res.cookie('accessToken', token.accessT, this.accessCookieOptions);
res.cookie('refreshToken', token.refreshT, this.refreshCookieOptions);
return res.send({ message: '토큰 발급 성공' });
}
- postman으로 확인하면 아래와 같이 쿠키 두 개가 발급되었음을 확인할 수 있다
[2. 로그아웃 시 토큰 삭제]
1. 로그아웃 방법
- 웹 브라우저의 쿠키를 삭제한다
- 클라이언트에서 단독으로 처리할 수 있긴 하지만 그냥 같이 쓰고싶었다
2. 로그아웃 로직
import { Body, Controller, Delete, Post, Req, Res } from '@nestjs/common';
import { UserService } from './user.service';
import { CookieOptions, Request, Response } from 'express';
import { User } from './user.dto';
import { AuthService } from '../auth/auth.service';
...
@Delete()
async signOut(@Res({ passthrough: true }) res: Response) {
res.clearCookie('accessToken');
res.clearCookie('refreshToken');
return { message: '토큰 삭제 완료' };
}
- 쿠키 삭제가 완료된다
[3. 로그인 유지를 위한 리프레시 토큰 발급]
1. 액세스토큰과 리프레시토큰을 함께 발급하는 이유
- JWT는 그 자체로 정보를 담고 있어 HTTP에서 유용하게 쓰인다는 장점이 있으나
- 설정한 유효기간이 지나기 전에 인위적으로 말소시키는게 불가능하기 때문에 보안에 취약하다는 단점이 있다
- 이런 이유로 쿠키와 함께 사용하며, 액세스토큰은 유효기간을 짧게 설정하고 리프레시토큰은 유효기간을 길게 설정한다
- 주 서비스는 액세스토큰을 사용해 상호작용하고
- 액세스토큰이 만료될 때마다 리프레시토큰으로 토큰을 재발급 해준다
2. 리프레시 토큰 발급 로직
- auth.service.ts
async refresh(curT: string) {
try {
// 토큰이 유효한지 확인
// 유효하지 않으면 알아서 에러를 throw해준다
const tokenInfo = this.jwtService.verify(curT, {
secret: this.jwtSecret,
});
// tokenInfo에 담긴 정보를 사용해 사용자 정보 확인
// 사용자가 존재하지 않으면 exception throw
const user = await this.prismaService.user.findUnique({
where: { id: tokenInfo.id },
});
if (!user || user.refreshToken !== curT) {
throw new UnauthorizedException('유효하지 않은 리프레시 토큰입니다.');
}
// payload 설정 후 액세스토큰과 리프레시 토큰 재발급
const payload = {
type: 'refresh',
id: tokenInfo.id,
email: tokenInfo.email,
};
const accessT = await this.generateToken(payload, 'access');
const refreshT = await this.generateToken(payload, 'refresh');
return { accessT, refreshT };
} catch (e) {
console.log(e);
}
}
- user.controller.ts
import * as cookie from 'cookie';
...
@Post('refresh')
async refresh(
@Req() req: Request,
@Res({ passthrough: true }) res: Response,
) {
// request header에서 refreshToken 추출
const cookies = cookie.parse(req.headers.cookie || '');
const curT = cookies.refreshToken;
const token = await this.authService.refresh(curT);
res.cookie('accessToken', token.accessT, this.accessCookieOptions);
res.cookie('refreshToken', token.refreshT, this.refreshCookieOptions);
// res.send()를 사용하면 순환참조 오류가 발생한다
return { message: '토큰 리프레시 완료' };
}
- 토큰 리프레시가 완료된다
'Nest.JS' 카테고리의 다른 글
[Nest.js] REST API 방식으로 소셜로그인 구현 샘플 코드 (naver, google, kakao) (2) | 2024.10.16 |
---|---|
[Nest.js] 프로젝트 초기 설정하는 법 (0) | 2024.09.25 |
[Nest.js] 라이프사이클 정리 (0) | 2024.09.08 |
[Nest.js] 페이지네이션 백엔드 구현 (prisma) + 프론트 구현 (0) | 2024.07.04 |
[Nest.js] 카카오 로그인 API (회원가입, 로그인, 카카오 프로필 가져오기) (0) | 2024.06.23 |