본 캠프 19강 TIL

정희준·2023년 4월 13일
0
post-thumbnail

📌 오늘의 내용

오늘은 JWT토큰을 활용한 로그인 API 인증 구현을 진행 해보았다
오늘의 내용에 대해 알아보자

토큰 기반 인증 시스템

토큰 기반 인증은 최근의 웹서비스에서 아주 많이 사용되고 있습니다. 토큰 기반 인증 시스템은 어떻게 작동되고, 무슨 장점을 가지고 있으며, 왜 나타나게 된 걸까요? 이를 이해하기 위해 먼저 기존의 서버 기반 시스템에 대해 알아보겠습니다.

서버 기반 인증 시스템

기존의 서버 기반 인증 시스템은 서버 측에서 유저들의 정보를 기억하고 있어야 했습니다. 따라서 여러 가지 문제점이 발생했습니다.

  • 서버가 유저의 인증 기록을 세션에 저장하는데, 로그인 유저가 많아지면 서버가 과부하됩니다.
  • 세션을 사용하면 분산된 시스템을 설계하고 서버를 확장하는 것이 어렵습니다.
  • 세션을 관리할 때 사용되는 쿠키는 여러 도메인에서 관리하는 것이 번거롭습니다.

이런 문제점들을 해결하기 위해 토큰 기반 인증 시스템을 사용하기 시작했습니다.

토큰 기반 인증 시스템의 장점

무상태(Stateless) & 확장성(Scalability)

Stateful Server의 경우 클라이언트에게 요청을 받을 때마다 상태를 유지하고 정보를 서비스 제공에 이용됩니다. 반면 Stateless Server에서는 상태 정보를 저장하지 않고서버는 클라이언트의 요청만으로 작업을 처리하며 세션을 사용하지 않습니다. 따라서 토큰을 사용하면 클라이언트와 서버의 연결고리가 없어 서버를 확장하기에 매우 적합한 환경을 제공합니다.

확장성(Extensibility)

서버를 확장시키는 것뿐 아니라 로그인 정보가 사용되는 분야를 확장할 수 있습니다. 우리가 Google 계정을 이용해 Notion, Slack 등을 이용하는 것처럼 토큰에 선택적인 권한을 부여해서 발급할 수 있습니다.

시스템 작동 원리

대략적인 토큰 기반 인증 시스템의 구현 방식은 다음과 같습니다.

  1. 유저가 아이디와 비밀번호로 로그인을 합니다.
  2. 서버 측에서 해당 계정 정보를 검증합니다.
  3. 계정 정보가 정확하다면, 서버 측에서 유저에게 signed 토큰(accesstoken)을 발급해 줍니다.
  4. 클라이언트 측에서 전달받은 토큰을 저장해두고, 서버에 요청을 할 때마다 해당 토큰을 함께 서버에 전달합니다.
  5. 서버는 토큰을 검증하고, 요청에 응답합니다.

💡 이러한 토큰 기반 인증 시스템의 구현체가 바로 Json Web Token 입니다.

Json Web Token

JWT는 웹 표준으로서 C, Java, Python, JS 등 대부분의 주류 프로그래밍 언어에서 지원됩니다. 또한 필요한 모든 정보를 자체적으로 가지고 있어 자가 수용적(Self-contained)이며 그렇기에 두 개체 사이에서 쉽게 전달될 수 있는 장점들을 가지고 있습니다.

JWT는 .으로 구분되는 Header, Payload, Signature의 3가지 문자열로 되어있습니다.

  • Header

Header는 토큰의 타입과 해싱 알고리즘이라는 두 가지 정보를 담고 있습니다

{"alg":"HS256","typ":"JWT"}

보통 해싱 알고리즘은 HS256을 사용하지만 HS512을 이용해 토큰을 더 길게 만들수 있습니다.

  • Payload

Payload에는 토큰에 담을 정보가 들어가며, 담는 정보의 한 조각은 name/value의 한 쌍으로 이루어진 Claim이라고 부릅니다. Claim은 Registered, Public, Private의 세 분류로 나누어져 있으며 Registered Claim은 토큰 발급자, 토큰 제목, 토큰 만료시간, 토큰 발급 시간 등 토큰에 대한 정보를 담기 위해 이미 이름이 정해진 Claim 입니다.

  • Signature

JWT의 마지막 부분은 서명으로, Header의 인코딩 값과 Payload의 인코딩 값을 합친 후 주어진 비밀키로 해싱하여 생성합니다


로그인 API 실습

위 토큰 기반 인증 시스템의 개념을 토대로 로그인 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정보"}

profile
같이 일하고 싶은 사람이 되어보자! 다시 시작하는 개발 블로그.

0개의 댓글