nestjs 공식문서 Authentication 파트를 요약 정리한 내용입니다(https://docs.nestjs.com/security/authentication)
인증과 인가 차이
어플리케이션은 사용자의 권한을 확인하기 위해 1)인증(authentication)
과 2)인가(authorization)
작업을 한다. 1)인증은 유저가 자신이 서비스를 사용할 수 있음을 증명하는 것이고 2)인가는 인증된 유저가 특정 기능에 대한 사용 권한이 있는지 판별하는 것이다.
authentication 모듈 생성
$ nest g module auth
$ nest g controller auth
$ nest g service auth
유저 관련 작업 수행을 캡슐화시키기 위해 UsersService
도 생성해주자.
$ nest g module users
$ nest g service users
유저 서비스 모듈에 findOne 로직과 하드코딩 간이 DB를 만들어주자.
// users.service.ts
import { Injectable } from '@nestjs/common';
// This should be a real class/interface representing a user entity
export type User = any;
/**
* hard-coded DB
* - you can replace this with ORM later
*/
@Injectable()
export class UsersService {
private readonly users = [
{
userId: 1,
username: 'john',
password: 'changeme',
},
{
userId: 2,
username: 'maria',
password: 'guess',
},
];
async findOne(username: string): Promise<User | undefined> {
return this.users.find((user) => user.username === username);
}
}
UsersService를 외부에서 사용하기 위한 유저 모듈에 서비스 추가
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
@Module({
providers: [UsersService],
exports: [UsersService], // enable visible outside
})
export class UsersModule {}
'Sign in' 엔드포인트 구현
// auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersService } from 'src/users/users.service';
@Injectable()
export class AuthService {
// 외부 유저서비스 주입
constructor(private usersService: UsersService) {}
async signIn(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user?.password !== pass) {
throw new UnauthorizedException();
}
const { password, ...result } = user;
// TODO : Generate JWT and return it here instead of user obj
return result;
}
}
Auth 모듈 업데이트
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';
@Module({
imports: [UsersModule],
providers: [AuthService],
controllers: [AuthController],
})
export class AuthModule {}
auth컨트롤러에 signIn() 추가
- 여기서 유저 정보가 맞으면 토큰을 줘야 하는 지점이다.
// auth.controller.ts
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@HttpCode(HttpStatus.OK)
@Post('login')
signIn(@Body() signInDto: Record<string, any>) {
return this.authService.signIn(signInDto.username, signInDto.password);
}
}
- Record<> 대신 DTO 클래스 사용해도 된다.
## JWT token
requirements about JWT:
- 유저가 username, password을 정확히 넣으면, JWT 토큰 주기
- JWT의 정상 여부에 따라 방어해주는 API 라우트 만들기
$ npm install --save @nestjs/jwt
```ts
// auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from 'src/users/users.service';
@Injectable()
export class AuthService {
// 외부 유저서비스 주입
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async signIn(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user?.password !== pass) {
throw new UnauthorizedException();
}
const payload = { sub: user.userId, username: user.username };
return {
access_token: await this.jwtService.signAsync(payload)
}
}
}
JWT생성한 다음 access_token라는 객체에 넣어 반환했다. signAsync()
는 JWT 생성하는 함수다.
시크릿 키를 저장하는 파일을 만들자.
// ./auth/constants.ts
export const jwtConstants = {
secret:
'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.',
};
auth 모듈에 jwt 모듈을 import 하고, auth 모듈을 export해준다.
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
@Module({
imports: [
UsersModule,
JwtModule.register({
global: true,
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}
JWT는 global로 등록해주었기에 어플리케이션 내 어디서든 Jwt모듈 import하지 않고 바로 사용할 수 있다.
authentication 가드 구현
AuthGuard
를 통해 JWT 여부에 따라 특정 엔드포인트들을 방어하는 방법을 알아보자.
// auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { Request } from 'express';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: jwtConstants.secret,
});
// we are assigning the payload to the request object here
// so that we can access it in our route handlers
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
AuthGuard를 profile 라우터에 추가해준다.
// auth.controller.ts
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Request,
UseGuards,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthGuard } from './auth.guard';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@HttpCode(HttpStatus.OK)
@Post('login')
signIn(@Body() signInDto: Record<string, any>) {
return this.authService.signIn(signInDto.username, signInDto.password);
}
@UseGuards(AuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}
$ # GET /profile
$ curl http://localhost:3000/auth/profile
{"statusCode":401,"message":"Unauthorized"}
$ # POST /auth/login
$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."}
$ # GET /profile using access_token returned from previous step as bearer code
$ curl http://localhost:3000/auth/profile -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."
{"sub":1,"username":"john","iat":...,"exp":...}
beare 토큰 없이 profile로 요청하면 unauthorized 401 에러가 발생하지만, 로그인한 다음 받은 JWT를 베어러 토큰에 추가하여 다시 요청하면 정상적으로 데이터를 반환해준다.
가드를 글로벌로 적용하기
위에서는 auth/profile 엔드포인트에만 가드를 적용했다면, 글로벌로 모든 엔드포인트에 적용할 수도 있다.
// auth.module.ts
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth.guard';
@Module({
imports: [
UsersModule,
JwtModule.register({
global: true,
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [
AuthService,
// global guard 등록
{
provide: APP_GUARD,
useClass: AuthGuard,
},
],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}
providers에 APP_GUARD를 AuthGuard 클래스로 사용한다고 등록하면, 자동으로 모든 엔드포인트에 AuthGuard가 적용된다.
SetMetadata 데코레이터 팩토리 함수를 이용하여 커스텀 데코레이터를 만들자.
// ./auth/decorators/public.decorator.ts
import { SetMetadata } from "@nestjs/common";
// metadata key
export const IS_PUBLIC_KEY = 'isPublic';
// new decorator called 'Public'
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// result -> we will get @Public() decorator
isPublic
메타데이터가 있을 경우 AuthGuard가 리턴하도록 해주자.
// auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { Request } from 'express';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from './decorators/public.decorator';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService, private reflector: Reflector) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: jwtConstants.secret,
});
// we are assigning the payload to the request object here
// so that we can access it in our route handlers
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
'Research > Nest.js' 카테고리의 다른 글
nestjs_에러해결_DTO validation 작동이 되지 않는 문제 (0) | 2023.04.26 |
---|---|
nestjs_실패로그_프로젝트 docker 환경변수 설정 문제 (1) | 2023.04.26 |
nestjs_error_html formData를 axios로 서버에 전송하기 (0) | 2023.04.17 |
nestjs_RDS Postgres 인스턴스 생성 및 nestjs 모듈 설치 (0) | 2023.04.15 |
nestjs_view 엔진 적용하기(hbs) (0) | 2023.04.13 |
댓글