1. Guard 이론 & 구현 스펙 정리
- 공식문서 : https://docs.nestjs.com/guards
- Guard란?
@injectable데코레이터된 클래스다.CanActivate인터페이스를 상속(implements)받는다.
- 잘못된 요청으로 인한 에러를 막기 위한 Guard를 만들 수 있다.
auth/guard/basic-token.guard.ts파일을 만든다.
basic-token.guard.ts
1/*** 구현할 기능2* 1) 요청객체(request)를 불러오고,3* authorization header로부터 토큰을 가져온다.4* 2) authService.extractTokenFormHeader를 이용해서5* 사용할 수 있는 형태의 토큰을 추출한다.6* 3) authService.decodedBasicToken을 실행해서7* email과 password를 추출한다.8* 4) email과 password를 이용해서 사용자를 가져온다.9* authService.authenticateWithEmailAndPassword10* 5) 찾아낸 사용자를 (1) 요청 객체에 붙여준다.11* req.user = user;12*/
2. BasicTokenGuard 구현
Guard를 구현하는 건 Pipe를 구현하는 것과 매우 유사하다. NestJS에서 일부러 이렇게 만들었다.
basic-token.guard.ts
1import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'2import { AuthService } from '../auth.service'34/*** 구현할 기능5* 1) 요청객체(request)를 불러오고,6* authorization header로부터 토큰을 가져온다.7* 2) authService.extractTokenFormHeader를 이용해서8* 사용할 수 있는 형태의 토큰을 추출한다.9* 3) authService.decodedBasicToken을 실행해서10* email과 password를 추출한다.11* 4) email과 password를 이용해서 사용자를 가져온다.12* authService.authenticateWithEmailAndPassword13* 5) 찾아낸 사용자를 (1) 요청 객체에 붙여준다.14* req.user = user;15*/16@Injectable()17export class BasicTokenGuard implements CanActivate {18constructor(private readonly authService: AuthService) {}1920async canActivate(context: ExecutionContext): Promise<boolean> {21const req = context.switchToHttp().getRequest()22const rawToken = req.headers['authorization'] // {authorization : 'Basic asdfafa'}2324if (!rawToken) throw new UnauthorizedException('토큰이 없습니다!')2526const token = this.authService.extractTokenFromHeader(rawToken, false)27const { email, password } = this.authService.decodeBasicToken(token)28const user = await this.authService.authenticateWithEmailAndPassword({29email,30password,31})32req.user = user3334return true35}36}
auth 컨트롤러에 생성한 가드를 추가한다.
auth.controller.ts
1// auth.controller.ts 생략2@Post('login/email')3@UseGuards(BasicTokenGuard) // 추가4postLoginEmail(@Headers('authorization') rawToken: string) {5const token = this.authService.extractTokenFromHeader(rawToken, false)6const credentials = this.authService.decodeBasicToken(token)7return this.authService.loginWithEmail(credentials)8}
포스트맨에 authorization 토큰을 넣지 않는 경우를 확인한다.
guard에 작성한 req가 컨트롤러에 그대로 남는다.
auth.controller.ts
1// auth.controller.ts 생략2@Post('login/email')3@UseGuards(BasicTokenGuard)4postLoginEmail(5@Headers('authorization') rawToken: string, //6@Request() req, // 가드에 생성한 req를 가져와서 쓰기(지우거나 주석처리)7) {8const token = this.authService.extractTokenFromHeader(rawToken, false)9const credentials = this.authService.decodeBasicToken(token)10return this.authService.loginWithEmail(credentials)11}
req가 남아있는 걸 봤다면, 지우거나, 주석처리한다.
3. BearerTokenGuard 구현
auth/guard/bearer-token.guard.ts 파일을 만든다.
auth/guard/bearer-token.guard.ts
1import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'2import { AuthService } from '../auth.service'3import { UsersService } from 'src/users/users.service'45@Injectable()6export class BearerTokenGuard implements CanActivate {7constructor(8private readonly authService: AuthService,9private readonly usersService: UsersService,10) {}1112async canActivate(context: ExecutionContext): Promise<boolean> {13const req = context.switchToHttp().getRequest()14const rawToken = req.headers['authorization'] // {authorization : 'Basic asdfafa'}1516if (!rawToken) throw new UnauthorizedException('토큰이 없습니다!')1718const token = this.authService.extractTokenFromHeader(rawToken, true)19const result = await this.authService.verifyToken(token)2021/** request에 넣을 정보22* 1) 사용자 정보 - user23* 2) token - token24* 3) tokenType - access | refresh25*/26const user = await this.usersService.getUserByEmail(result.email)27req.user = user28req.token = token29req.tokenType = result.type3031return true32}33}3435@Injectable()36export class AccessTokenGuard extends BearerTokenGuard {37async canActivate(context: ExecutionContext): Promise<boolean> {38await super.canActivate(context)39const req = context.switchToHttp().getRequest()4041if (req.tokenType !== 'access') {42throw new UnauthorizedException('Access Token이 아닙니다.')43}4445return true46}47}4849@Injectable()50export class RefreshTokenGuard extends BearerTokenGuard {51async canActivate(context: ExecutionContext): Promise<boolean> {52await super.canActivate(context)53const req = context.switchToHttp().getRequest()5455if (req.tokenType !== 'refresh') {56throw new UnauthorizedException('Refresg Token이 아닙니다.')57}5859return true60}61}
생성한 가드들을 auth 컨트롤러에 추가한다.
auth.controller.ts
1// 생략2@Controller('auth')3export class AuthController {4constructor(private readonly authService: AuthService) {}56@Post('token/access')7@UseGuards(RefreshTokenGuard) // 추가8postTokenAccess(@Headers('authorization') rawToken: string) {9// 생략10}1112@Post('token/refresh')13@UseGuards(RefreshTokenGuard) // 추가14postTokenRefresh(@Headers('authorization') rawToken: string) {15// 생략16}17}1819@Post('login/email')20@UseGuards(BasicTokenGuard) // 추가21postLoginEmail(22@Headers('authorization') rawToken: string, //23// @Request() req, // 가드에 생성한 req를 가져와서 쓰기24) {25// 생략26}27}
이제 포스트맨에서 확인해본다.