06. 가드와 유저 인증
NestJs의 가드(Guard)는 요청이 특정 조건을 만족하는지 확인하여 요청을 처리할지 여부를 결정하는 데 사용된다. 이를 통해 인증/인가 및 기타 요청 전 조건을 쉽게 설정할 수 있다. 가드는 미들웨어와 유사하지만 실행 컨텍스트 인스턴스에 접근할 수 있어 더 세밀한 컨트롤을 제공한다.
// 글로벌 가드
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new AuthGuard());
await app.listen(3000);
}
// 컨트롤러 가드
@Controller('')
@UseGuards(AuthGuard)
export class Controller {
@Get()
findAll() {}
}
// 핸들러 가드
@Get()
@UseGuards(AuthGuard)
findAll() {}
커스텀 가드를 사용한 유저 인증
NestJs에서 모든 커스텀 가드는 CanActivate를 구현한다. canActivate 함수는 실행 콘텍스트를 인수로 받는데, 이를 통해 요청과 응답에 대한 정보를 제공 받게 된다. 우리는 HTTP로 기능을 제공하고 있으므로 switchToHttp() 함수의 getRequest와 getResponse 메소드로 필요한 정보를 가져올 수 있다.
import { Request } from 'express';
import { Observable } from 'rxjs';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
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();
return this.validateRequest(request);
}
private validateRequest(request: Request) {
const accessToken = request.cookies['accessToken'];
if (accessToken) {
try {
request.user = this.authService.verify(accessToken, 'access');
return true;
} catch (error) {
// 뭔가 오긴 했는데 토큰이 아닌 이상한 문자열일 때
throw new UnauthorizedException('로그인이 필요합니다');
}
} else {
throw new UnauthorizedException('로그인이 필요합니다');
}
}
}
canActivate는 boolean을 리턴하게 되는데, false면 403 응답이 돌아온다. 실질적인 검증 로직은 validateRequest에서 처리하고 있다. 여기서는 토큰의 존재와 유효성을 검증하며, 그에 따라 boolean을 리턴한다.
중요한 것은 authService.verify 결과를 request.user에 주입하는 부분인데, 이를 통해 컨트롤러에서는 별도의 작업 없이 request로부터 작업을 요청한 유저에 대한 정보를 얻을 수 있다.
import { User } from '@prisma/client';
@Controller('report')
export class ReportController {
constructor(private reportService: ReportService) {}
@Delete(':reportId')
@UseGuards(AuthGuard)
async deleteReport(@Param('reportId') reportId: string, @Req() req: Request) {
const { id } = req.user as User;
const isOwner = await this.reportService.checkIsOwner(reportId, id);
if (!isOwner) {
throw new ForbiddenException('권한이 없습니다.');
} else {
return this.reportService.deleteReport(reportId);
}
}
}
passport와 유저 인증
@nestjs/passport 패키지를 사용하면 지금까지 만들었던 AuthGuard 커스텀 가드를 아주 손쉽게 만들 수 있다. 때문에 아주 세밀한 로직이 필요한 것이 아니라면, 일반적으로는 @nestjs/passport 패키지가 제공하는 커스텀 가드를 사용하게 된다.
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';
@Module({
controllers: [AuthController],
providers: [AuthService, AuthRepository, JwtStrategy],
imports: [
JwtModule.register({}),
PassportModule.register({ defaultStrategy: 'jwt' }),
],
exports: [JwtStrategy, PassportModule],
})
export class AuthModule {}
PassportModule 역시 JwtModule과 마찬가지로 동적 모듈이므로 register 메소드를 사용한다. 이때 제공하는 옵션 객체에 { defaultStrategy : 'jwt' }를 제공해주어야 한다.
그리고는 JwtStrategy를 providers에 넣어주고, JwtStrategy, PassportModule을 export 해주어야 한다. JwtStrategy는 JWT 토큰을 검증하는 로직을 구현하는 일종의 커스텀 가드 팩토리 클래스라고 할 수 있겠다. 이는 따로 파일을 만들어 사용해야 하지만, PassportStrategy(Strategy)를 상속받기에 큰 뼈대는 @nestjs/passport 패키지가 제공해준다고도 할 수 있겠다.
// jwt.strategy.ts
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
@Inject(jwtConfig.KEY) private jwt: ConfigType<typeof jwtConfig>,
private authRepository: AuthRepository,
) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(request) => {
if (request && request.cookies) return request.cookies['accessToken'];
else return '';
},
]),
secretOrKey: jwt.accessSecret,
});
}
async validate(payload): Promise<any> {
const user = await this.authRepository.findUserByEmail(payload.email);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
내 경우는 토큰이 쿠키에 있어서 jwtFromRequest가 위와 같은 형태를 갖게 되었지만, Authorization Bearer로 토큰을 보내는 경우라면 ExtractJwt.fromAuthHeaderAsBearerToken() 메소드를 사용해 훨씬 간결하게 작성할 수 있다.
쓸 것인가…? passport를?
여러가지 방법을 찾아보던 중 @nestjs/passport 패키지를 알게 되었지만, 아무래도 나는 커스텀 가드를 만들어 쓰는 편이 더 좋은 것 같다. 여기에는 몇 가지 이유가 있는데, 가장 큰 요인은 JwtStrategy 클래스 하나 만들자고 여러 패키지 (@nestjs/passport뿐만 아니라 passport-jwt도) 설치해야 하는 게 싫다. 누군가에게는 별 거 아닐 수 있겠지만 미니멀리스트 비스무리한 나에게는 꽤 중요한 사항이다.
게다가 커스텀 가드는 내가 필요한 방식으로 로직을 자유롭게 구성할 수 있다. 그러나 JwtStrategy는 하는 일에 비해 이것저것 뭐가 많은 것처럼 느껴진다.
따라서 이 글에서 passport에 대해 다뤘음에도 불구하고 나는 따로 커스텀 가드를 만들어서 사용할 생각이다.
블로그의 정보
Ayden's journal
Beard Weard Ayden