오늘은 JWT토큰을 활용한 로그인 API 인증 구현을 진행 해보았다
오늘의 내용에 대해 알아보자
토큰 기반 인증은 최근의 웹서비스에서 아주 많이 사용되고 있습니다. 토큰 기반 인증 시스템은 어떻게 작동되고, 무슨 장점을 가지고 있으며, 왜 나타나게 된 걸까요? 이를 이해하기 위해 먼저 기존의 서버 기반 시스템에 대해 알아보겠습니다.
기존의 서버 기반 인증 시스템은 서버 측에서 유저들의 정보를 기억하고 있어야 했습니다. 따라서 여러 가지 문제점이 발생했습니다.
이런 문제점들을 해결하기 위해 토큰 기반 인증 시스템을 사용하기 시작했습니다.
무상태(Stateless) & 확장성(Scalability)
Stateful Server의 경우 클라이언트에게 요청을 받을 때마다 상태를 유지하고 정보를 서비스 제공에 이용됩니다. 반면 Stateless Server에서는 상태 정보를 저장하지 않고, 서버는 클라이언트의 요청만으로 작업을 처리하며 세션을 사용하지 않습니다. 따라서 토큰을 사용하면 클라이언트와 서버의 연결고리가 없어 서버를 확장하기에 매우 적합한 환경을 제공합니다.
확장성(Extensibility)
서버를 확장시키는 것뿐 아니라 로그인 정보가 사용되는 분야를 확장할 수 있습니다. 우리가 Google 계정을 이용해 Notion, Slack 등을 이용하는 것처럼 토큰에 선택적인 권한을 부여해서 발급할 수 있습니다.
시스템 작동 원리
대략적인 토큰 기반 인증 시스템의 구현 방식은 다음과 같습니다.
💡 이러한 토큰 기반 인증 시스템의 구현체가 바로 Json Web Token 입니다.
JWT는 웹 표준으로서 C, Java, Python, JS 등 대부분의 주류 프로그래밍 언어에서 지원됩니다. 또한 필요한 모든 정보를 자체적으로 가지고 있어 자가 수용적(Self-contained)이며 그렇기에 두 개체 사이에서 쉽게 전달될 수 있는 장점들을 가지고 있습니다.
JWT는 .
으로 구분되는 Header, Payload, Signature의 3가지 문자열로 되어있습니다.
Header는 토큰의 타입과 해싱 알고리즘이라는 두 가지 정보를 담고 있습니다
{"alg":"HS256","typ":"JWT"}
보통 해싱 알고리즘은 HS256
을 사용하지만 HS512
을 이용해 토큰을 더 길게 만들수 있습니다.
Payload에는 토큰에 담을 정보가 들어가며, 담는 정보의 한 조각은 name/value의 한 쌍으로 이루어진 Claim이라고 부릅니다. Claim은 Registered, Public, Private의 세 분류로 나누어져 있으며 Registered Claim은 토큰 발급자, 토큰 제목, 토큰 만료시간, 토큰 발급 시간 등 토큰에 대한 정보를 담기 위해 이미 이름이 정해진 Claim 입니다.
JWT의 마지막 부분은 서명으로, Header의 인코딩 값과 Payload의 인코딩 값을 합친 후 주어진 비밀키로 해싱하여 생성합니다
위 토큰 기반 인증 시스템의 개념을 토대로 로그인 API 실습을 해보자!
두 과정을 실습 해볼 것 이다!
authentication(인증) : 로그인을 하는 것(로그인을해서 토큰을 받아오는 과정)
authorization(인가) : 로그인한 후, 로그인이 필요한 서비스들을 사용할 때 해당 유저임을 확인하는 것(리소스에 접근할 수 있도록 토큰을 확인하는 과정)
authentication(인증)
// 서비스
import { Injectable, UnprocessableEntityException } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';
import { JwtService } from '@nestjs/jwt';
import {
IAuthServiceGetAccessToken,
IAuthServiceLogin,
} from './interfaces/auth-service.interface';
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService, //
private readonly jwtService: JwtService,
) {}
async login({ user_email, user_pwd }: IAuthServiceLogin): Promise<string> {
const user = await this.usersService.findOneByEmail({ user_email });
if (!user) throw new UnprocessableEntityException('이메일이 없습니다.');
const isAuth = await bcrypt.compare(user_pwd, user.user_pwd);
if (!isAuth)
throw new UnprocessableEntityException('비밀번호가 일치하지 않습니다.');
return this.getAccessToken({ user });
}
getAccessToken({ user }: IAuthServiceGetAccessToken): string {
return this.jwtService.sign(
{ id: user.user_id },
{ secret: '나의비밀번호', expiresIn: '1h' },
);
}
}
위 소스는 서비스 파일이며
auth관련 모듈,리졸버를 만든 후 실제 로직이 담긴 서비스 파일이다
로그인 시 받아온 정보를 데이터 검증을 통해 에러처리를 하고
패스워드의 경우 해쉬된 정보 끼리 비교를 통해 데이터 검증을 진행했다
데이터 검증을 통과 시 JWT토큰을 생성해주는 라이브러리인 jwt서비스에 토큰 옵션을 담아 보내준다!
그 후 인가를 위해 Backend는 JWT를 받고 Guard를 통해 JWT Strategy를 실행하고, Secret Key를 통해 JWT를 Decoding 합니다. JWT를 복호화 한 후에 원하는 API의 Business Logic이 수행된 후, Response 됩니다.
// 가드
import { ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';
export class GqlAuthAccessGuard extends AuthGuard('access') {
getRequest(context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
return gqlContext.getContext().req;
}
}
// Strategy
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
export class JwtAccessStrategy extends PassportStrategy(Strategy, 'access') {
constructor() {
super({
// jwtFromRequest: (req) => {
// const temp = req.headers.Autorization; // Bearer asdasd
// const accessToken = temp.toLowerCase().replace('bearer ', '');
// return accessToken;
// },
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: '나의비밀번호',
});
}
validate(payload) {
// 성공시 로직 처리
console.log(payload); // {id: adasdasdqwdqw(유저ID)}
return {
id: payload.id,
};
}
}
프론트가 없기 때문에 플레이그라운드에서 HTTP HEADER 부분에 데이터를 넣어 주어야 한다!
{"Authorization":"Bearer accesstoken정보"}