이번 포스팅에선 NestJS 에서 제공하는 Guard 기능을 사용해보겠습니다.
- 인증 vs 인가
인증은 Authentication, 인가는 Authorization 으로 의미하는 바는 좀 다릅니다.
인증은 유저의 신원을 검증하는 프로세스로, 자신이 누구인지를 증명하는 것입니다. 인가는 인증 이후의 프로세스로 인증된 사용자가
어떤 리소스에 접근할 수 있는지를 확인하는 것을 인가라고 합니다. NestJS 에선 Guard 를 사용해 인가를 구현할 수 있습니다.
좀 희한한건 일반적으로 응답 상태를 나타낼 때 인증이 실패하면 401, 인가가 실패하면 403 인데
401 - Unauthrozied / 403 - Forbidden 으로 되어있습니다. Forbidden 은 이해가 되지만 Authentication 실패 에러를
Unauthoized 라고 하는건 좀 헷갈리기 쉬울 것 같으니 주의가 필요합니다.
예전엔 인증이라고하면 세션인증을 많이 사용했지만 이제는 토큰인증 방식을 많이 사용하는 것 같습니다.
현재 팀에선 인증을 사용할 일이 없어서 쓸 일은 없지만 전회사에선 인증을 구현할 때 JWT 를 많이 사용했었고,
지금 NestJS 에서도 JWT 를 사용해 인증을 구현하려고 합니다. JWT 자세한 설명 보다는 링크만 남겨두도록 하겠습니다.
- use JWT
지금까지 만든 서비스에는 별도의 로그인은 없으니 JWT 를 단순 발급하는 용도의 API 를 하나 만들어서 사용하도록 하겠습니다.
대략의 시나리오는 해당 API 를 사용해 토큰을 발급받고 광고 생성 또는 조회 API 를 사용할 때 해당 JWT 로 인가를 획득하는 것입니다.
> npm install jsonwebtoken
> npm install @types/jsonwebtoken --save-dev
> mkdir src/auth
먼저 JWT 관련된 모듈들을 설치한 후 인증 관련된 파일들이 위치할 auth 디렉토리를 생성합니다.
> touch src/auth/auth.controller.ts src/auth/auth.guard.ts src/auth/auth.module.ts src/auth/auth.service.ts
> touch config/authConfig.ts
관련 파일들을 생성했습니다. 뭔가 하다보니 저는 하나씩 만들게 됐지만 nest-cli 를 사용해 auth 리소스를 생성하셔도 됩니다.
...
JWT_SECRET=test
JWT 토큰을 생성하기 위해선 secret key 가 필요한데, 일단 테스트용이니 단순하게 test 라는 string 을 사용할까 합니다.
그대로 하드코딩하지 않고 환경변수로 등록해 사용할 수 있도록 .development.env dotenv 파일에 추가해주었습니다.
이 환경변수를 config 로 주입해 사용할 수 있도록 authConfig 관련 작업을 먼저 해봅니다.
import { registerAs } from '@nestjs/config';
export const authConfig = registerAs('auth', () => ({
secret: process.env.JWT_SECRET,
}));
이미 이전 포스팅에서 config 는 여러번 추가했었기 때문에.. config/authConfig.ts 파일을 위와 같이 추가해줬습니다.
app.module.ts 의 load 부분에 authConfig 를 추가하는건 굳이 코드 첨부까지 하진 않겠습니다.
import { Inject, Injectable } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import * as jwt from 'jsonwebtoken';
import { authConfig } from '../config/authConfig';
@Injectable()
export class AuthService {
constructor(
@Inject(authConfig.KEY) private config: ConfigType<typeof authConfig>,
) {}
create(email: string) {
const payload = { email };
return jwt.sign(payload, this.config.secret);
}
}
import { Controller, Get, Param } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Get()
create(@Param('email') email: string) {
return this.authService.create(email);
}
}
auth.service.ts 와 auth.controller.ts 를 위와 같이 작성해줍니다.
일단 추가한 코드는 auth api 를 email parameter 와 같이 호출하면 email 을 사용해 JWT 토큰을 생성해 반환하는 코드입니다.
jwt.sign 메서드의 세번째 인자로 여러 옵션을 추가할 수 있는데, 대표적으로 토큰의 유효기간인 expiresIn 을 추가할 수 있지만
저는 테스트용이니 한번 발급한 토큰을 영구히 사용하기 위해서 아무런 옵션을 넣지 않았습니다.
이제 localhost:3000/auth 를 GET method 로 호출해 토큰을 발급받아봅니다.
e.. 로 시작하는 토큰이 생성되어 해당 요청에 대한 응답으로 반환되었을 것입니다.
- use Guard
이제 광고 API 에 JWT 를 통한 Guard 작업을 추가해보겠습니다.
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import * as jwt from 'jsonwebtoken';
import { authConfig } from '../config/authConfig';
@Injectable()
export class AuthService {
...
verify(token: string) {
try {
const { email } = jwt.verify(token, this.config.secret) as (
| jwt.JwtPayload
| string
) & { email: string };
return { email };
} catch (e) {
throw new UnauthorizedException();
}
}
}
JWT 토큰을 이용해 해당 토큰이 유효한지를 인증하는 auth.service.ts 의 verify 메서드를 추가해줬습니다.
verify 를 할 땐 sign 할 때와 마찬가지로 동일한 secret 을 사용해야 합니다.
verify 에 성공하면 사실 여기선 사용할 일은 없지만 토큰에 심어진 email 을 반환하고,
실패하면 Unauthorized 에러를 뱉도록 구현했습니다.
import { Request } from 'express';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
const email = this.parseRequest(request);
request.decoded = { email };
return true;
}
private parseRequest(request: Request) {
const [, token] = request.headers.authorization.split('Bearer ');
const { email } = this.authService.verify(token);
return email;
}
}
위에서 작성한 auth.service 의 verify 를 사용한 auth.guard.ts 코드입니다.
가드는 위와 같이 CanActivate 인터페이스를 구현해야 합니다. 약간의 설명을 하자면 이 메서드는 ExecutionContext 라는 실행 컨텍스트 인스턴스를 인자로 받는데, 여기에 요청과 응답에 대한 정보를 갖고 있습니다.
이 인터페이스에서 제공하는 함수 중 switchToHttp 메서드를 이용해 필요한 요청 또는 응답에 대한 정보를 가져올 수 있습니다.
이렇게 가져온 정보로 parseRequest 라는 내부 메서드를 통해 canActivate 에서 인가를 진행합니다.
이 부분을 인가라고 하는 이유는 구현하는 canActivate 메서드가 true, false 중 무엇을 반환하느냐에 따라 Guard 를 적용한 요청이 인가를 얻어 다음 프로세스로 진행이 되는지, 또는 403 Forbidden 에러로 빠지는지 결정되기 때문입니다.
전 일단 인증이 되었으면 email 정보만 심고 인가된 사용자로 간주할 것이기 때문에 return true 를 넣었지만 인가된 사용자를 무엇으로
할지에 대한 요구사항에 따라 이 부분을 다르게 구현하시면 될 것 같습니다.
...
import {
...,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '../auth/auth.guard';
@UseGuards(AuthGuard)
@Controller('ads')
export class AdsController {
...
}
각 엔드포인트에도 별도로 @UseGuards 데코레이터를 통해 적용할 수 있지만 저는 ads controller 에 전체 적용을 했습니다.
이제 의도한대로 인증 - 인가가 적용되는지 확인해보겠습니다.
아까 위의 auth.guard.ts 에서 언급하지 못한 내용이 있다면 구현 내용을 보셔도 아시겠지만 JWT 토큰의 경우
요청 헤더에 Authorization 키로 Bearer {token} <- 과 같이 요청해야 합니다.
먼저 생성했던 토큰이 아닌 아무런 키나 집어넣은 토큰을 넣고 요청을 보내보면, 401 Unauthoized 에러가 발생합니다.
아까 발급받았던 토큰을 넣고 다시 요청을 보내보면, 200 응답이 정상적으로 떨어지는 것을 확인할 수 있습니다.
저는 토큰에 집어 넣었던 email 정보가 다른 API 에서 필요 없기 때문에 request.decoded 에 넣어놓고 쓰질 않지만
API 를 어떻게 만드느냐에 따라 충분히 활용할 요소가 있습니다.
NestJS 가 아닌 Express 를 사용하더라도 JWT 를 통한 인증 - 인가 부분은 미들웨어로 보통 빼기 때문에 이 부분에 의해서
코드가 지저분해질 일은 잘 없긴 하지만, NestJS 가 제공하는 Guard 기능을 통해 좀 더 괜찮은 AOP 를 할 수 있게 되었습니다.
'Backend > NestJS' 카테고리의 다른 글
NestJS - 대충 서비스 만들어보기 (14) (0) | 2023.06.17 |
---|---|
NestJS - 대충 서비스 만들어보기 (13) (0) | 2023.06.12 |
NestJS - 대충 서비스 만들어보기 (11) (0) | 2023.05.20 |
NestJS - 대충 서비스 만들어보기 (10) (0) | 2023.05.13 |
NestJS - 대충 서비스 만들어보기 (9) (0) | 2023.05.07 |