Search
🔑

따라하며 배우는 NestJS | Auth

Date
2024/08/28
Category
WEB
Tag
Typescript
NestJS
목차

준비 과정

CLI를 통한 모듈, 컨트롤러, 서비스 생성

nest g module auth
nest g congroller auth --no-spec
nest g service auth --no-spec

UserEntity 생성

유저에 대한 인증이니 유저가 필요
유저 데이터를 위한 유저 엔티티 생성

Repository 생성

User Entity를 생성, 수정, 삭제 등의 로직 처리를 위해 Repository 생성
생성된 User Repository 사용을 위해 module의 provider에 추가 + User Entity를 module에 import

I. 로직 구현

I. 회원가입

dto 생성을 통해 username, password로 회원가입을 할 수 있도록 구현

II. 유저 데이터 유효성 체크

유저를 생성할 때 원하는 이름의 길이, 비밀번호 길이를 갖는지 Column에 조건 생성
class-validator 모듈 사용
DTO 파일에서 Request로 들어오는 값을 정의해주고 있기 때문에 DTO파일의 값에 유효성 조건을 넣음

ValidationPipe

요청이 컨트롤러에 있는 핸들러로 들어왔을 때 DTO에 있는 유효성 조건에 맞게 체크해주기 위해 ValidationPipe를 넣어야 함

III. 유저 이름에 유니크 값 주기

이미 사용되는 유저 이름이 있는 경우 에러 발생시키기

I. Repository에서 findOne 메소드 사용

이미 같은 유저 이름을 가진 아이디가 있는지 findOne으로 확인 후 데이터를 저장
데이터베이스 처리를 두번 해주어야 함

II. 데이터베이스 레벨

데이터베이스 레벨에서 같은 이름이 있다면 에러 발생
이 방법으로 구현
user.entity.ts에서 원하는 유니크한 값을 원하는 필드 값을 정하면 됨
이미 있는 유저를 다시 생성하려 하면 500 에러가 발생함
NestJS에서 에러가 발생하고 try catch가 없으면 이 에러가 Controller 레벨로 가기 때문
해결을 위해 try catch 문 사용
... async createUser(authCredentialsDto: AuthCredentialsDto): Promise<void> { const { username, password } = authCredentialsDto; const user = this.create({ username, password }); try { await this.save(user); } catch (error) { if (error.code === '23505') { throw new ConflictException('Existing username'); } else { throw new InternalServerErrorException(); } } } }
TypeScript
복사
console.log(error)를 했을 경우 에러코드가 23505 이기 때문에 if 문을 위와 같이 사용

IV. 비밀번호 암호화

암호화하지 않으면 데이터베이스에 비밀번호가 그대로 저장됨

bcryptjs

암호화하는데 사용하는 모듈
npm install bcryptjs --save
import * as bcrypt from ‘bcryptjs’;

비밀번호를 데이터베이스에 저장하는 방법

1.
원본 비밀번호를 저장 → 최악
2.
비밀번호를 암호화 키(Encryption Key)와 함께 암호화 (양방향)
어떠한 암호를 이용해 비밀번호를 암호화
그 암호를 이용해 복호화도 가능
암호화 키가 노출되면 알고리즘은 대부분 오픈되어있기 때문에 위험도 높음
3.
SHA256 등 Hash로 암호화해서 저장 (단방향)
복호화가 불가능함
레인보우 테이블을 만들어서 암호화된 비밀번호를 비교해서 비밀번호를 알아낼 수 있음
레인보우 테이블 - 대부분 유저들은 비슷한 암호를 사용하기 때문에 (키, 해쉬값)을 저장한 빅데이터
4.
솔트 + 비밀번호를 해시로 암호화해서 저장
암호화할 때 원래 비밀번호에 salt를 붙인 후 Hash로 암호화
같은 비밀번호라도 다른 비밀번호가 저장되어 알기 어려움

V. 로그인 구현

username이 있으며, password가 암호화된 password와 비교했을 때 같으면 로그인

VI. 인증된 유저만 게시물 보고 쓰게 하기

유저에게 게시물 접근 권한 주기

1.
인증에 관한 모듈을 board 모듈에서 쓸 수 있어야 함
a.
board module에서 인증 모듈 imports
AuthModule의 exports 부분을 board Module에서 사용 가능
2.
UseGuards(AuthGuard())를 이용해 올바른 토큰을 가지고 요청을 하는지 확인 후 게시물에 대한 접근 권한을 줌
a.
AuthGuard는 각각의 라우트 별로 줄 수 있고, 한번에 하나의 컨트롤러 안에 있는 모든 라우트에 줄 수 있음

II. JWT

로그인을 할 때 로그인한 고유 유저를 위한 토큰을 생성해야 함
토큰을 생성할 때 JWT라는 모듈을 사용

JWT (JSON Web Token)

당사자 간에 정보를 JSON 개체로 안전하게 전송하기 위한 개방형 표준(RFC 7519)
디지털 서명이 되어 있으므로 확인하고 신뢰할 수 있음
정보를 안전하게 전할 때, 또는 유저의 권한을 체크하기 위해 사용하는 모듈

JWT의 구조

Header
토큰에 대한 메타 데이터 포함
타입, 해싱 알고리즘 (SHA256, RSA, …)
Payload
유저 정보, 만료 기간, 주제 등
Verify Signature
토큰이 보낸 사람에 의해 서명되었으며 어떤 식으로든 변경되지 않았는지 확인하는데 사용되는 서명

JWT 사용 흐름

1.
Admin만 볼 수 있는 글을 Admin 유저가 보려 함
2.
요청을 보낼 때 보관하고 있던 Token을 Header에 넣어 같이 보냄
3.
서버에서 JWT를 통해 Token을 다시 생성한 후 비교
4.
통과가 되면 Admin 유저가 원하는 글을 볼 수 있음

I. JWT를 이용한 토큰 생성

필요 모듈
@nestjs/jwt
nestjs에서 jwt를 사용하기 위해 필요한 모듈
@nestjs/passport
nestjs에서 passport를 사용하기 위해 필요한 모듈
passport
passport-jwt
jwt 모듈
npm install @nestjs/jwt @nestjs/passport passport passport-jwt --save
1.
애플리케이션에 JWT 모듈 등록
auth 모듈 imports에 넣어주기
secret - 토큰을 만들 때 이용하는 secret text (아무 텍스트나 가능)
expiresIn - 정해진 시간 이후 토큰이 유효하지 않게 됨. (60 * 60 은 한시간)
2.
애플리케이션에 Passport 모듈 등록
auth 모듈 imports에 넣어주기
3.
로그인 성공 시 JWT를 토큰에 넣어주기
Service에 SignIn 메소드에서 생성
Token을 만드려면 Secret과 Payload 필요
Payload에는 자신이 전달하고자 하는 정보를 넣으면 됨
민감 정보는 넣지 말것

II. Passport, JWT 이용해서 토큰 인증 후 유저 정보 가져오기

토큰이 유효한 토큰인지 서버에서 secret-text를 이용해 알아내면 payload 안에 정보를 이용해 데이터베이스 안에 있는 정보를 가져올 수 있음
이 처리를 쉽게 해주는 것이 Passport 모듈

구현

1.
@types/passport-jwt 모듈 설치
passport-jwt 모듈을 위한 타입 정의 모듈
npm install @types/passport-jwt --save
2.
jwt.strategy.ts 생성
3.
jwtStrategy를 사용하기 위해 AuthModule Providers 항목에 넣어주고, 다른 곳에서도 JwtStrategy와 PassportModule도 사용해줘야 하기 때문에 exports항목에 넣어줌
4.
요청 안에 유저 정보(유저 객체)가 들어가게 해야 함
validate 메소드에 return 값으로 user 객체를 주었지만 요청을 보낼 때는 user 객체가 없음
이를 해결하기 위해 UseGuards를 사용
5.
UseGuards 안에 @nestjs/passport에서 가져온 AuthGuard()를 이용해 요청 안에 유저 정보를 넣음
@Post('/authTest') @UseGuards(AuthGuard()) authTest(@Req() req) { console.log('req', req); }
TypeScript
복사

참고

I. Custom Decorator

req.user를 통해 유저 객체를 얻을 수 있음
req.user가 아닌 바로 user라는 파라미터로 가져올 수 있는 방법은 커스텀 데코레이터 사용
사용 안해도 됨