1. Auth Resource 생성
nest g resource // 다음 'Auth' 로 생성
2. JWT 패키지 설치
yarn add @nestjs/jwt bcrypt // 혹은 npm i @nestjs/jwt bcrypt
jwt와 bcrypt 패키지를 설치한다.
3. Token 발급 메서드
signToken(user: Pick<UsersModel, 'email' | 'id'>, isRefreshToken: boolean) { const payload = { email: user.email, sub: user.id, type: isRefreshToken ? 'refresh' : 'access', }; return this.jwtService.sign(payload, { secret: JWT_SECRET, //seconds expiresIn: isRefreshToken ? 3600 : 300, }) }
참고로 auth.service.ts 에 작성해야하며, 해당 로직에는 비밀번호가 포함되지 않아야 한다.
4. loginUser 메서드
loginUser(user: Pick<UsersModel, 'email' | 'id'>) { return { accessToken: this.signToken(user, false), refreshToken: this.signToken(user, true), } }
loginUser 메서드는 사용자가 로그인을 하거나 회원가입을 할 때, AccessToken과 RefreshToken을 발급받는 공통적인 과정을 하나의 메서드로 작성한 것이다.
5. 회원가입
@Post('register/email') postRegisterEmail( @Body('nickname') nickname: string, @Body('email') email: string, @Body('password') password: string, ) { return this.authService.registerWithEmail({ nickname, email, password, }); }
auth.controller.ts 에 위와 같이 라우터를 구현한다.
import { Module } from '@nestjs/common'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersModel } from './entities/users.entity'; @Module({ imports: [TypeOrmModule.forFeature([UsersModel])], exports: [UsersService], controllers: [UsersController], providers: [UsersService], }) export class UsersModule {}
exports: [UsersService],
우선 로직을 작성하기에 앞서 UsersModel에 접근해야 한다.
그러나 Auth에서 UsersModel에 직접 접근하기보다는 이미 만들어둔 메서드를 사용하기 위해 users.service.ts 에 있는 메서드를 가져오도록 한다.
그러기 위해선 users.module.ts 내에 exports로 UsersService를 등록한다.
@Injectable() export class AuthService { constructor( private readonly jwtService: JwtService, private readonly usersService: UsersService, ) {}
이후 auth.service.ts 내의 생성자에 UsersService를 등록하여 사용할 수 있다.
async registerWithEmail(user: Pick<UsersModel, 'nickname' | 'email' | 'password'>) { const hash = await bcrypt.hash( user.password, HASH_ROUNDS, ); const newUser = await this.usersService.createUser({ ...user, password: hash, }); return this.loginUser(newUser); }
registerWithEmail 메서드는 클라이언트로부터 받은 nickname, email, password를 받아온 뒤 password와 HASH_ROUNTDS(salt)를 이용해 bcrypt 해쉬를 만들고, 이것을 usersService 내에 만들어져있는 createUser 메서드를 호출하여 유저를 DB에 등록한다.
그리고 loginUser 메서드를 호출하여 AccessToken과 RefreshToken을 생성하여 클라이언트에 반환한다.
6. 이메일 로그인
@Post('login/email') postLoginEmail( @Headers('authorization') rawToken: string, ) { // email:password -> base64 // 디코딩 후 email과 password를 나눠야함 const token = this.authService.extractTokenFromHeader(rawToken, false); const credentials = this.authService.decodeBasicToken(token); return this.authService.loginWithEmail(credentials); }
auth.controller.ts 에 위와 같이 라우터를 구현한다. rawToken은 클라이언트에서 'email:password' 로 만든 후 base64 인코딩한 문자열이다.
/** * Header로 부터 토큰을 받을 때 * * {authorization: 'Basic {token}'} * {authorization: 'Bearer {token}'} */ extractTokenFromHeader(header: string, isBearer: boolean) { const splitToken = header.split(' '); const prefix = isBearer ? 'Bearer' : 'Basic'; if(splitToken.length !== 2 || splitToken[0] !== prefix) { throw new UnauthorizedException('잘못된 토큰입니다.'); } const token = splitToken[1]; return token; }
extractTokenFromHeader는 클라이언트에서 보낸 헤더를 기반으로 Basic (토큰생성), Bearer (토큰사용)을 구분하여 토큰을 반환한다.
/** * Basic asdlkfjadlfkjadflkjasdf * * 1) asdlkfjadlfkjadflkjasdf -> email:password (디코딩) * 2) email:password -> [email, password] * 3) {email: email, password: password} */ decodeBasicToken(base64String: string) { const decoded = Buffer.from(base64String, 'base64').toString('utf8'); const split = decoded.split(':'); if(split.length !== 2) { throw new UnauthorizedException('잘못된 유형의 토큰입니다.'); } const email = split[0]; const password = split[1]; return { email, password, } }
decodedBasicToken은 받아온 token(email:password)을 디코딩하는 과정이다. 객체로 만들어 email, password을 반환한다.
async loginWithEmail(user: Pick<UsersModel, 'email' | 'password'>) { const existingUser = await this.authenticateWithEmailAndPassword(user); return this.loginUser(existingUser); }
async authenticateWithEmailAndPassword(user: Pick<UsersModel, 'email' | 'password'>) { const existingUser = await this.usersService.getUserByEmail(user.email); if (!existingUser) { throw new UnauthorizedException('존재하지 않는 사용자입니다.'); } /** * 파라미터 * * 1) 입력된 비밀번호 * 2) 기존 해시(hash) -> 사용자 정보에 저장되어있는 hash */ const passOk = await bcrypt.compare(user.password, existingUser.password); if (!passOk) { throw new UnauthorizedException('비밀번호가 틀렸습니다.'); } return existingUser; }
사용자가 이메일과 비밀번호로 로그인을 진행하면 입력된 이메일로 유저를 DB에서 가져온 뒤, DB에 저장된 패스워드(base64 암호화)와 입력된 비밀번호를 비교하여 일치하는 경우 유저를 반환하고, 이 유저를 가지고 loginUser 메서드를 실행하여 AccessToken과 RefreshToken을 클라이언트에 반환한다.
7. AccessToken 갱신
@Post('token/access') postTokenAccess( @Headers('authorization') rawToken: string, ) { const token = this.authService.extractTokenFromHeader(rawToken, true); const newToken = this.authService.rotateToken(token, false); /** * {accessToken: {token}} */ return { accessToken: newToken, } }
auth.controller.ts 에 postTokenAccess 메서드를 라우팅한다.
/** * Header로 부터 토큰을 받을 때 * * {authorization: 'Basic {token}'} * {authorization: 'Bearer {token}'} */ extractTokenFromHeader(header: string, isBearer: boolean) { const splitToken = header.split(' '); const prefix = isBearer ? 'Bearer' : 'Basic'; if(splitToken.length !== 2 || splitToken[0] !== prefix) { throw new UnauthorizedException('잘못된 토큰입니다.'); } const token = splitToken[1]; return token; }
로그인때와 마찬가지로 클라이언트에서 보낸 헤더를 분해하여 토큰을 반환한다.
rotateToken(token: string, isRefreshToken: boolean) { const decoded = this.verifyToken(token); /** * sub: id * email: email, * type: 'access' | 'refresh' */ if(decoded.type !== 'refresh') { throw new UnauthorizedException('토큰 재발급은 Refresh 토큰으로만 가능합니다!'); } return this.signToken({ ...decoded }, isRefreshToken); }
이후 토큰을 검증 후 signToken을 통해 토큰을 재발급한다.
/** * 토큰 검증 */ verifyToken(token: string) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.jwtService.verify(token, { secret: JWT_SECRET, }); }
verifyToken은 jwtService.verify 를 통해 AccessToken과 RefreshToken을 검증할 수 있다.
8. RefreshToken 갱신
@Post('token/refresh') postTokenRefresh( @Headers('authorization') rawToken: string, ) { const token = this.authService.extractTokenFromHeader(rawToken, true); const newToken = this.authService.rotateToken(token, true); /** * {refreshToken: {token}} */ return { refreshToken: newToken, } }
AccessToken 갱신과 다른 부분은 rotateToken에 두 번째 매개변수 isRefreshToken: boolean차이 뿐이다.
'NestJS' 카테고리의 다른 글
NestJS - Pipe (0) | 2025.02.26 |
---|---|
VSC 디버거 사용하기 (0) | 2025.02.26 |
Authentication - Encryption (암호화) (0) | 2025.02.26 |
Authentication - JWT (0) | 2025.02.26 |
TypeORM - Repository 메서드 종류 (0) | 2025.02.25 |