[토이프로젝트] 2 편 : 로그인 모듈 설계 (Feat.JWT)

[토이프로젝트] 2 편 : 로그인 모듈 설계 (Feat.JWT)

안녕하세요? 정리하는 개발자 워니증입니다. 이번시간에는 지난 토이프로젝트에 대한 설계에 대하여 정리를 해볼까 합니다. 처음에는 로그인을 도입하고자 했을때, AWS의 서비스 중에서 Congnito를 활용하고 싶었습니다. 그런데 아직은 필자에게 와닿을 만큼 편리하게 사용할 방법을 찾지 못해서 다른방법을 찾아보게 되었습니다.

그러던중에 JWS(Json Web Token)을 알게 되었고, 이참에 이것을 한번 적용해서 로그인 모듈을 만들어보자라는 생각을 하게 되었습니다.

jwt는 사용자 정보를 json 객체에 담아 이를 암호화하고 해싱 작업을 거쳐 문자열 토큰을 생성하는 기술입니다. 클라이언트는 이 토큰을 HTTP Header에 추가하여 요청을 보냄으로써 사용자 인증을 얻게 됩니다.

지난 글은 아래를 참고 부탁드립니다.

1. jwt 란 무엇인가요?

JWT는 이름에서 알 수 있듯이 Json으로 된 Token을 사용합니다. 그런데 JWT는 토큰 자체가 의미를 갖는 Claim 기반의 토큰 방식입니다. Claim(권한)은 사용자에 대한 프로퍼티나 속성을 의미합니다.

client에 대한 인증(Authentication)과 인가(Authorization)를 위한 매커니즘이 필요한데,
JWT는 MSA의 인증,인가에 사용할 수 있는 서명된 JSON 이기 때문에 안성맞춤이다.

  • 토큰 기반의 인증 원리
  1. 사용자 로그인
  2. 서버에서 로그인 검증 절차를 완료 하고 완료되면, 토큰을 발행한다.
  3. 클라이언트측에서 서버로부터 전달받은 토큰을 저장하고, 서버에 요청한다.
  4. 서버에서는 전달된 토큰을 검증하여 결과를 리턴한다.

참고: JSON Web Token(JWT) 정리

2. jwt 구조

JWT는 헤더(header), 정보(payload), 서명(signature) 구조로 이루어져 있습니다.

  • header : 타입(JWT)과 알고리즘(BASE64 같은)을 담는다.
  • payload : 보통 유저정보(id같은)와 만료기간이 객체형으로 담긴다.
  • signature : header, payload를 인코딩 한 값을 합친뒤 SECRET_KEY로 해쉬한다.

JWT는 . 을 구분자로 3가지의 문자열로 되어있습니다. 구조는 다음과 같이 이루어져 있습니다.

참고 : JSON Web Token 소개 및 구조

아래의 공식 jwt 사이트에 들어가서 token을 입력하게 되면, 어떤 내용인지 보이게 됩니다. 제가 임시로 발급받은 코드로 테스트를 해보았습니다.

jwt 토큰자체는 3개의 내용으로 구분이 되어있고, 우측에 해당 내용이 정확하게 표기가 되었습니다.

3. 로그인 모듈 설계

필자가 구현하고자 하는 프로젝트의 로그인 모듈을 어떤식으로 설계할지에 대한 내용입니다. 어렸을적 로그인을 생각해보면 ID/PW만 입력하면 API 쪽에서 DB값 조회해서 매칭이 되면 정상 리턴 해주고 아니면, 에러를 응답하는 구조로 생각을 했엇는데요.

너무 허접해서… 조금더 설계답게 생각해보았습니다.

간단하게 대충대충 flow chart로 만들어봤습니다. Start – End사이에 어떤 흐름이 있는지만 기억하면 될 것 같습니다.

  • 로그인 시도
  • user 존재 여부 확인 -> 없으면 Error(로그인 페이지 전환)
  • password 매칭 여부 확인 -> 안되면 Error(로그인 페이지 전환)
  • 로그인이 정상적으로 되면, JWT 토큰 발급
  • local storage에 저장
  • Homepage 전환

참으로 간단하게 설계된 로그인 페이지 인것 같습니다. 이렇게 되고, 모든 API를 호출 할때마다 header에 token정보를 같이 전달해서 토근이 만료되거나 잘못된 정보면은 API를 호출하지 못하도록 설계 했습니다.

개별적으로 구성되어있는 Lambda 들이 동작을 할때, 제일 먼저 jwt token검증을 수행합니다. 그렇게 되고나서 정상적으로 인증절차를 거치면 그다음에 호출을 할 수 있습니다.

4. 구현

  • 로그인 Lambda
//모듈 선언.
const crypto = require('crypto')
const jwt = require('jsonwebtoken')
const doc = require('dynamodb-doc');
const dynamo = new doc.DynamoDB();

exports.handler = (event, context, callback) => {

    // 0. 변수선언
    const body = JSON.parse(event.body);
    const email = body.email;
    const password = body.password;

    const encrypted = crypto.createCipher('aes-256-cbc', 'secret');
    let encryptedpw = encrypted.update(password, 'utf8', 'base64'); 
    encryptedpw += encrypted.final('base64');

    //1. 유저 정보 조회
    const payload = {
        TableName: 'user',
        //ProjectionExpression: "#email, password",    // 1. select val, test, id
        FilterExpression: "#email=:email", // 3. where val between :start and :end
        ExpressionAttributeNames: {
            "#email": "email"
        },
        ExpressionAttributeValues: {
             ":email": email
        }
    }
    dynamo.scan(payload, (err, data) => {
        data = data.Items[0];
        if(data === undefined){
            //2 . 없으면 에러  
            callback(null, {
                    'statusCode': 500,
                    'headers': {'Content-Type': 'application/json; charset=UTF-8', 'access-control-allow-origin': '*'},
                    'body': '{"message" : "ID or PW mismatched"}'
            });
        }

        //3. 있으면 패스워드 체크
        else
        //3-1. 패스워드 일치하면 jwt 발급
        if(encryptedpw === data.password){
                const token = jwt.sign({
                    id: email,
                    name: data.name
                }, (process.env.JWT_SECRET || 'JWT_SECRET'), {
                    expiresIn: '30m',
                     issuer: 'get_token_controller',
                });
                callback(null, {
                    'statusCode': 200,
                    'headers': {'Content-Type': 'application/json; charset=UTF-8'},
                    'body': '{"user" : {"email":\"' +data.email +'\","username":\"'+data.name+'\","token" : ' + JSON.stringify(token) + '}}'
                });

        }else{
        //3-2. 패스워드 일치하지 않으면 에러 
            callback(null, {
                    'statusCode': 500,
                    'headers': {'Content-Type': 'application/json; charset=UTF-8', 'access-control-allow-origin': '*'},
                    'body': '{"message" : "ID or PW mismatched"}'
            });
        }
    });
};

위의 Flow chart의 로직 흐름대로 개발을 진행했습니다.

정확하게는 아래 보이시는 부분이 jwt 모듈에서 토큰을 발급하는 내용입니다.

const token = jwt.sign({
                    id: email,
                    name: data.name
                }, (process.env.JWT_SECRET || 'JWT_SECRET'), {
                    expiresIn: '30m',
                     issuer: 'get_token_controller',
                });

5. 마치며…

로그인 모듈을 설계하고 개발하는 가운데 확실히 기존의 쿠키나 세션방식보다는 여러가지 장점을 갖고 있다고 느껴졌습니다. 서버에서 더이상 세션을 저장하거나 메모리를 사용해서 기록을 해두지 않기때문에 확장성이 확실히 뛰어날 것입니다.

또한 쿠키방식은 request가 오가며 보안상에서 위변조가 가능했을텐데 이런 부분이 개선이 된것 같습니다. 물론 jwt도 가능하다고는 하지만 HMAC이라는 해싱 알고리즘을 써서 좀더 보안상에는 탁월하다고 들었습니다.

다음이시간에는 API들의 설계와 작성에 대해서 소개를 하고 최종적으로는 front-end 기술에 대해서 정리를 하고 마치는 식으로 포스팅을 연재하겠습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다