Intro
Documentation | NestJS - A progressive Node.js framework
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea
docs.nestjs.com
공식문서 최고!
평소에 그냥 jwt web token을 사용해서 인증을 진행했는데 nestjs 의 jwt service를 사용해서 만들어보고 싶어서 해봄
이왕 포스팅 하는김에 JWT와 토큰인증 대해서도 자세히 써보도록 하겠다.
JWT를 사용하는 이유
예전에는 stateful(서버에서 세션에 대한 상태를 알고있음)한 세션 인증 방식으로 사용자 식별을 진행했는데 이렇게되면 요청마다 서버와 통신이 필요해 트래픽이 증가한다.
이 문제를 해결하기 위해 state-less(서버와 클라이언트간의 통신 시 항상 사용자의 정보를 갖고있지 않는 것)인 JWT 가 등장했다.
JWT 안에 회원 정보같은 필요한 정보를 넣어두어 매 요청마다 서버와 통신할 필요가 없게 해준다.
JWT 구조
JSON Web Token의 약자로, 인증 정보를 안전하게 전달하기 위한 표준 방법
JWT는 세 부분으로 구성된다.
- Header - 토큰의 유형과 서명 알고리즘 등의 메타데이터를 포함
- Payload - 보낼 데이터(사용자 정보 등)을 포함
- Signuture - 헤더와 페이로드를 인코딩한 후, 비밀 키를 사용하여 생성된 서명
.
JWT.IO
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
jwt.io
해당 사이트에서 아래 사진과 같은 JWT 정보를 확인할 수 있다.

Encoded에 있는 알수없는 문자열이 JWT 이다. 아주 직관적이게 빨강, 보라, 파랑 색구분으로 JWT가 어떻게 이루어져있는지 볼 수 있다.
각 부분은 .(dot) 으로 구분된다.
JWT는 암호화된 문자열이 아닌 그냥 base64로 인코딩된 문자열이다.

base64 decode 해보면 바로 알 수 있다.
그러면 암호화는 어디 쓰이냐 할 수 있는데 전체 JWT를 다시 보면 아래처럼 이루어져있다. 즉 secret key로 서명(signiture)을 생성하기 위해 사용된다.
[Header].[PAYLOAD].[HMACSHA256으로 HEADER + PAYLOAD + secret key 를 암호화한 값]
📌HS256
Header에 alg : HS256 을 보면 해당 토큰은 HS256으로 인코딩되었음을 알 수 있다. 잠시 HS256에 대해 보면 아래와 같다.
- HMAC algorithm + SHA256 해시 알고리즘
- 입력 데이터와 비밀 키를 이용하여 서명하는 해시 기반 대칭키 알고리즘 (암호화, 복호화 할 때 키가 동일)
- 원본 메시지와 공유된 메시지를 비교하여 변조 여부를 확인
- 원본 메시지가 변하면 그 해시값도 변하는 해싱(Hashing)의 특징을 활용하며 메시지의 변조 여부를 확인하여 무결성 및 기밀성을 제공
JWT 로그인 흐름
간단히 도식도로 표현해보면 아래와 같다.

Authentication(인증) - 로그인
Authorization(인가) - 한번 인증을 받은 사용자가 로그인 되어있음을 확인하는것. 특정 리소스에 접근할 수 있는지 권한 확인
1. client에서 로그인 요청을 서버에 보냄
2. 3. 4. server에서 로그인 확인 (Authentication) 후 성공하면 JWT 생성하고 client에 전달
5. 6 client는 이후 서버로 보내는 요청에 JWT를 header에 포함하여 api call
7. 8 server는 client로부터 받은 JWT를 검증하여 유효성을 확인하고, 클라이언트의 요청에 대한 인가를 처리함
Nest.js로 JWT 로그인 구현
1. login 코드 구현
auth.controller.ts
@Controller('auth')
export class AuthController {
constructor(
private authService: AuthService,
) {}
@Post('login')
async login(@Body() body: LoginBody): Promise<LoginResponse> {
return this.authService.login(body);
}
}
auth.service.ts
export class AuthService {
constructor(
private userRepository: UserRepository,
) {}
async login(@Body() body: LoginBody): Promise<LoginResponse> {
const user = await this.userRepository.findOneBy({
userId: body.userId,
});
if (!user) throw new NotFoundException('유저를 찾을 수 없습니다');
// TODO - hash
if (body.password != user.password) {
throw new BadRequestException('잘못된 비밀번호입니다');
}
const payload = {
userId: user.userId,
userName: user.userName,
};
// TODO - generate JWT and return JWT
}
}
지금은 예시로 입력받은 password와 db에 저장된 user의 password를 직접 비교했지만 원래는 암호화 해서 저장해야한다. 나는 argon2 사용!
우선 login의 service 코드만 작성해보았다. 이제 db에 user가 있다면 (Authentication) JWT를 생성하고 user에게 return해주면 된다.
2. JWT 토큰 발급
우선 nestjs에서 제공하는 jwt 유틸리티 패키지를 설치한다.
$ npm install --save @nestjs/jwt
깔끔한 모듈화를 위해
auth.service.ts의 authService에 JwtService 메소드를 넣어준다.
그리고 JWT를 발급해준다.
export class AuthService {
constructor(
private userRepository: UserRepository,
private jwtService: JwtService,
) {}
async login(@Body() body: LoginBody): Promise<LoginResponse> {
const user = await this.userRepository.findOneBy({
userId: body.userId,
});
if (!user) throw new NotFoundException('유저를 찾을 수 없습니다');
// TODO - hash
if (body.password != user.password) {
throw new BadRequestException('잘못된 비밀번호입니다');
}
const payload = {
userId: user.userId,
userName: user.userName,
};
const accessToken = await this.jwtService.signAsync(payload);
return {
accessToken,
};
}
}
이제 JWT 서명을 위해 secret key를 넣어준다.
난 env 에서 가져오도록 했다.
이 키는 외부에 절대 공개되어서는 안된다!
export const jwtConstants = {
secret: configService.get('JWT_SECRET'),
};
그리고 이 secert key를 auth.module.ts에 아래 코드와 같이 업데이트 해준다.
@Module({
imports: [
UserModule,
JwtModule.register({
global: true,
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
controllers: [AuthController],
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}
이제 login 요청을 보내면 accessToken을 발급해서 return해주는것을 볼 수 있다.

3. Authentication Guard 구현
이제 사용자가 방금 발급받은 accessToken을 사용해 적합한 권한으로 api를 요청하도록 할것이다.
JWT가 요청 header에 함께 있어야만 api 요청 결과를 얻을 수 있도록 endpoint를 보호한다.
auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { jwtConstants } from './auth.util';
@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're 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;
}
}
authControlle에 사용자의 profile을 가져오는 endpoint를 작성해준다.
내 프로필은 나만 봐야하므로 접근권한이 있어야만 볼 수 있다.
즉 UseGuards를 사용해 위에 작성해둔 AuthGuard로 GET /profile을 보호해준다.
auth.controller.ts
@Controller('auth')
export class AuthController {
constructor(
private userRepository: UserRepository,
private authService: AuthService,
) {}
@Post('login')
async login(@Body() body: LoginBody): Promise<LoginResponse> {
return this.authService.login(body);
}
// accessToken이 있어야만 접근할 수 있도록 보호
@UseGuards(AuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}
우선 profile에 그냥 요청을 해보자.

권한이 없다고 한다. accessToken 없이 요청했으니 당연한 결과다. AuthGuard가 잘 작동함을 알 수 있다.
자 이제 Header에 로그인했을 때 받은 acessToken을 함께 주어 요청을 보내준다.

jwt 토큰은 단순 문자열. 공개되어도 상관없다. 다만 secret key가 털리면 암호화를 풀 수 있어져서 위험해진다. 그니까 secret key 관리 잘 하자!
로그인해서 받은 토큰을 Baerer에 담아서 보내주면

이렇게 자원에 접근이 가능해진다.
자 그럼 이제 JWT 인증 구현 완료!!
3. Global Guard로 변경
내가 만들고 싶은 서비스의 경우 대부분의 endpoint가 로그인 후에만 접근 가능하기 때문에 기본적으로 모든 endpoint가 보호되도록 Global Guard를 등록하고 로그인 없이 접근 가능한 endpoint에만 데코레이터를 주어 public으로 풀어주려한다.
auth.module.ts의 providers에 글로벌 가드를 등록한다.
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
],
이제 AuthGuard는 모든 엔드포인트에 자동으로 바인딩된다.
이제 로그인 없어도 사용가능한 public endpoint를 위한 데코레이터를 만들어보자.
public.decorator.ts 파일을 만들어준다.
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
메타데이터 key IS_PUBLIC_KEY와 새로 만든 데코레이터 Public을 export 해준다.
이제 우리는 @Public 데코레이터를 사용할 수 있다.
auth.controller.ts
@Public()
@Post('login')
async login(@Body() body: LoginBody): Promise<LoginResponse> {
return this.authService.login(body);
}
로그인은 누구나 시도할 수 있는 public endpoint 이므로 @Public() 데코레이터를 붙여준다.
마지막으로 AuthGuard에서 isPublic일 때 자원에 대한 접근을 풀어준다.
auth.guard.ts
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) {
// 💡 See this condition
return true;
}
// 이후 코드 이전과 동일
}
}
완성! 이제 진짜 끝!
💻전체 코드
GitHub - HaeSeon/wanna-travel-api
Contribute to HaeSeon/wanna-travel-api development by creating an account on GitHub.
github.com
'💛Backend' 카테고리의 다른 글
REST API에 대하여 (feat. 로이 필딩 논문) (1) | 2023.07.19 |
---|---|
Nest.js에서 TypeORM 0.3 migtation 하기 (1) | 2023.06.25 |
OAuth 2.0이란? 동작 방식은? (0) | 2023.01.25 |
Intro
Documentation | NestJS - A progressive Node.js framework
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea
docs.nestjs.com
공식문서 최고!
평소에 그냥 jwt web token을 사용해서 인증을 진행했는데 nestjs 의 jwt service를 사용해서 만들어보고 싶어서 해봄
이왕 포스팅 하는김에 JWT와 토큰인증 대해서도 자세히 써보도록 하겠다.
JWT를 사용하는 이유
예전에는 stateful(서버에서 세션에 대한 상태를 알고있음)한 세션 인증 방식으로 사용자 식별을 진행했는데 이렇게되면 요청마다 서버와 통신이 필요해 트래픽이 증가한다.
이 문제를 해결하기 위해 state-less(서버와 클라이언트간의 통신 시 항상 사용자의 정보를 갖고있지 않는 것)인 JWT 가 등장했다.
JWT 안에 회원 정보같은 필요한 정보를 넣어두어 매 요청마다 서버와 통신할 필요가 없게 해준다.
JWT 구조
JSON Web Token의 약자로, 인증 정보를 안전하게 전달하기 위한 표준 방법
JWT는 세 부분으로 구성된다.
- Header - 토큰의 유형과 서명 알고리즘 등의 메타데이터를 포함
- Payload - 보낼 데이터(사용자 정보 등)을 포함
- Signuture - 헤더와 페이로드를 인코딩한 후, 비밀 키를 사용하여 생성된 서명
.
JWT.IO
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
jwt.io
해당 사이트에서 아래 사진과 같은 JWT 정보를 확인할 수 있다.

Encoded에 있는 알수없는 문자열이 JWT 이다. 아주 직관적이게 빨강, 보라, 파랑 색구분으로 JWT가 어떻게 이루어져있는지 볼 수 있다.
각 부분은 .(dot) 으로 구분된다.
JWT는 암호화된 문자열이 아닌 그냥 base64로 인코딩된 문자열이다.

base64 decode 해보면 바로 알 수 있다.
그러면 암호화는 어디 쓰이냐 할 수 있는데 전체 JWT를 다시 보면 아래처럼 이루어져있다. 즉 secret key로 서명(signiture)을 생성하기 위해 사용된다.
[Header].[PAYLOAD].[HMACSHA256으로 HEADER + PAYLOAD + secret key 를 암호화한 값]
📌HS256
Header에 alg : HS256 을 보면 해당 토큰은 HS256으로 인코딩되었음을 알 수 있다. 잠시 HS256에 대해 보면 아래와 같다.
- HMAC algorithm + SHA256 해시 알고리즘
- 입력 데이터와 비밀 키를 이용하여 서명하는 해시 기반 대칭키 알고리즘 (암호화, 복호화 할 때 키가 동일)
- 원본 메시지와 공유된 메시지를 비교하여 변조 여부를 확인
- 원본 메시지가 변하면 그 해시값도 변하는 해싱(Hashing)의 특징을 활용하며 메시지의 변조 여부를 확인하여 무결성 및 기밀성을 제공
JWT 로그인 흐름
간단히 도식도로 표현해보면 아래와 같다.

Authentication(인증) - 로그인
Authorization(인가) - 한번 인증을 받은 사용자가 로그인 되어있음을 확인하는것. 특정 리소스에 접근할 수 있는지 권한 확인
1. client에서 로그인 요청을 서버에 보냄
2. 3. 4. server에서 로그인 확인 (Authentication) 후 성공하면 JWT 생성하고 client에 전달
5. 6 client는 이후 서버로 보내는 요청에 JWT를 header에 포함하여 api call
7. 8 server는 client로부터 받은 JWT를 검증하여 유효성을 확인하고, 클라이언트의 요청에 대한 인가를 처리함
Nest.js로 JWT 로그인 구현
1. login 코드 구현
auth.controller.ts
@Controller('auth')
export class AuthController {
constructor(
private authService: AuthService,
) {}
@Post('login')
async login(@Body() body: LoginBody): Promise<LoginResponse> {
return this.authService.login(body);
}
}
auth.service.ts
export class AuthService {
constructor(
private userRepository: UserRepository,
) {}
async login(@Body() body: LoginBody): Promise<LoginResponse> {
const user = await this.userRepository.findOneBy({
userId: body.userId,
});
if (!user) throw new NotFoundException('유저를 찾을 수 없습니다');
// TODO - hash
if (body.password != user.password) {
throw new BadRequestException('잘못된 비밀번호입니다');
}
const payload = {
userId: user.userId,
userName: user.userName,
};
// TODO - generate JWT and return JWT
}
}
지금은 예시로 입력받은 password와 db에 저장된 user의 password를 직접 비교했지만 원래는 암호화 해서 저장해야한다. 나는 argon2 사용!
우선 login의 service 코드만 작성해보았다. 이제 db에 user가 있다면 (Authentication) JWT를 생성하고 user에게 return해주면 된다.
2. JWT 토큰 발급
우선 nestjs에서 제공하는 jwt 유틸리티 패키지를 설치한다.
$ npm install --save @nestjs/jwt
깔끔한 모듈화를 위해
auth.service.ts의 authService에 JwtService 메소드를 넣어준다.
그리고 JWT를 발급해준다.
export class AuthService {
constructor(
private userRepository: UserRepository,
private jwtService: JwtService,
) {}
async login(@Body() body: LoginBody): Promise<LoginResponse> {
const user = await this.userRepository.findOneBy({
userId: body.userId,
});
if (!user) throw new NotFoundException('유저를 찾을 수 없습니다');
// TODO - hash
if (body.password != user.password) {
throw new BadRequestException('잘못된 비밀번호입니다');
}
const payload = {
userId: user.userId,
userName: user.userName,
};
const accessToken = await this.jwtService.signAsync(payload);
return {
accessToken,
};
}
}
이제 JWT 서명을 위해 secret key를 넣어준다.
난 env 에서 가져오도록 했다.
이 키는 외부에 절대 공개되어서는 안된다!
export const jwtConstants = {
secret: configService.get('JWT_SECRET'),
};
그리고 이 secert key를 auth.module.ts에 아래 코드와 같이 업데이트 해준다.
@Module({
imports: [
UserModule,
JwtModule.register({
global: true,
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
controllers: [AuthController],
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}
이제 login 요청을 보내면 accessToken을 발급해서 return해주는것을 볼 수 있다.

3. Authentication Guard 구현
이제 사용자가 방금 발급받은 accessToken을 사용해 적합한 권한으로 api를 요청하도록 할것이다.
JWT가 요청 header에 함께 있어야만 api 요청 결과를 얻을 수 있도록 endpoint를 보호한다.
auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { jwtConstants } from './auth.util';
@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're 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;
}
}
authControlle에 사용자의 profile을 가져오는 endpoint를 작성해준다.
내 프로필은 나만 봐야하므로 접근권한이 있어야만 볼 수 있다.
즉 UseGuards를 사용해 위에 작성해둔 AuthGuard로 GET /profile을 보호해준다.
auth.controller.ts
@Controller('auth')
export class AuthController {
constructor(
private userRepository: UserRepository,
private authService: AuthService,
) {}
@Post('login')
async login(@Body() body: LoginBody): Promise<LoginResponse> {
return this.authService.login(body);
}
// accessToken이 있어야만 접근할 수 있도록 보호
@UseGuards(AuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}
우선 profile에 그냥 요청을 해보자.

권한이 없다고 한다. accessToken 없이 요청했으니 당연한 결과다. AuthGuard가 잘 작동함을 알 수 있다.
자 이제 Header에 로그인했을 때 받은 acessToken을 함께 주어 요청을 보내준다.

jwt 토큰은 단순 문자열. 공개되어도 상관없다. 다만 secret key가 털리면 암호화를 풀 수 있어져서 위험해진다. 그니까 secret key 관리 잘 하자!
로그인해서 받은 토큰을 Baerer에 담아서 보내주면

이렇게 자원에 접근이 가능해진다.
자 그럼 이제 JWT 인증 구현 완료!!
3. Global Guard로 변경
내가 만들고 싶은 서비스의 경우 대부분의 endpoint가 로그인 후에만 접근 가능하기 때문에 기본적으로 모든 endpoint가 보호되도록 Global Guard를 등록하고 로그인 없이 접근 가능한 endpoint에만 데코레이터를 주어 public으로 풀어주려한다.
auth.module.ts의 providers에 글로벌 가드를 등록한다.
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
],
이제 AuthGuard는 모든 엔드포인트에 자동으로 바인딩된다.
이제 로그인 없어도 사용가능한 public endpoint를 위한 데코레이터를 만들어보자.
public.decorator.ts 파일을 만들어준다.
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
메타데이터 key IS_PUBLIC_KEY와 새로 만든 데코레이터 Public을 export 해준다.
이제 우리는 @Public 데코레이터를 사용할 수 있다.
auth.controller.ts
@Public()
@Post('login')
async login(@Body() body: LoginBody): Promise<LoginResponse> {
return this.authService.login(body);
}
로그인은 누구나 시도할 수 있는 public endpoint 이므로 @Public() 데코레이터를 붙여준다.
마지막으로 AuthGuard에서 isPublic일 때 자원에 대한 접근을 풀어준다.
auth.guard.ts
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) {
// 💡 See this condition
return true;
}
// 이후 코드 이전과 동일
}
}
완성! 이제 진짜 끝!
💻전체 코드
GitHub - HaeSeon/wanna-travel-api
Contribute to HaeSeon/wanna-travel-api development by creating an account on GitHub.
github.com
'💛Backend' 카테고리의 다른 글
REST API에 대하여 (feat. 로이 필딩 논문) (1) | 2023.07.19 |
---|---|
Nest.js에서 TypeORM 0.3 migtation 하기 (1) | 2023.06.25 |
OAuth 2.0이란? 동작 방식은? (0) | 2023.01.25 |