YS's develop story

(RP2기 5주차 과제 - part 2) JWT를 사용하여 회원용 API 만들기 (Node js) 본문

기타/라이징프로그래머 2기

(RP2기 5주차 과제 - part 2) JWT를 사용하여 회원용 API 만들기 (Node js)

Yusang 2021. 2. 4. 08:02

2번째 과제 : JWT를 사용하여 회원용 API 만들기

 

로그인 유지 방식 3가지(수업 내용 정리)

 

1. 쿠키, 세션

“자유이용권을 주는 것.”

맨 처음 클라이언트가 로그인을 할 경우, 서버에서 자유이용권 (토큰, 세션 ID)를 발급한다.

이후에 클라이언트가 회원용 API를 호출할 경우 http 헤더에 이 세션 ID가 담겨서 자유롭게 사용할 수 있다.

 

세션이란 서버 입장에서의 스토리지 이름이다.

쿠키라는 것은 클라이언트 스토리지 이름이다.

 

장점: 구현이 간단하다.

단점: 세션 ID가 노출되면 보안에 매우 취약하다.

 

2.OAuth

“Big-3 티켓을 주는 것” 특정 행위에 대해서만 권한을 주는 것

이 역시 로그인에 성공하면 서버에서 토큰을 보낸다.

 

단, 그전에 퍼미션 리스트도 함께 보낸다. 즉, 허용된 권한만 사용할 수 있게 하는 로그인 유지 방식이다.”

 

“카카오톡 게임을 할 때 어떤 정보를 제공할지 체크하는 것이 OAuth 방식 로그인의 대표적인 예시이다.”

 

리프레시 토큰: 한 달가량 유효 리프레시 토큰의 사용을 최소화하여 보안성을 높인다.

액세스 토큰: 몇 분 단위로 갱신 리프레시 토큰이 있어야 액세스 토큰을 발급받을 수 있다.

이 액세스 토큰으로 회원용 API에 접근할 수 있다.

 

장점: 보안성이 우수

단점: 과정이 복잡한 만큼 리소스가 많이 필요

 

3.JWT (Json Web Token)

“매번 놀이기구를 탈 때마다 권한을 검사하는 것”

“소프트 스퀘어드에서 주로 사용하는 방식”

 

클라이언트가 ID, PWD를 매번 서버에 보낸다. 서버는 이 정보를 64진법 인코딩하여 토큰으로 만들어

클라이언트에 보낸다.

 

“인코딩 된 정보는 JWT의 페이로드에 해당하는 부분이다. 헤더에는 인코딩 방식 등의 메타데이터가 담겨있다.”

 

시그니처: 헤더와 페이로드를 암호화 한 값.

  • ID, PWD 검증 유효한 토큰인지 시그니처 비교

장점: 리소스가 적게 필요하다.

단점: 보안에 취약하다. HTTPS이 꼭 요구됨

 

클라이언트 입장에서 어떻게 로그인을 유지시킬까?

“모든 플랫폼 (AOS, iOS, Web)은 각자의 로컬 스토리지가 있다!”

 

로그인 토큰을 로컬 스토리에 저장한다.

자동 로그인을 할 경우 이 토큰을 서버에 보내는 방식으로 토큰을 유지한다.

 

API 설계 부분

회원가입 API

exports.signUp = async function (req, res) {
    const {
        userEmail, userPassword,checkUserPassword,userName
    } = req.body;

    //if (!isEmail(userEmail))
    // return res.json({isSuccess: false, code: 300, message: "이메일 형식이 올바르지 않습니다."});

    if (!userEmail) return res.json({isSuccess: false, code: 301, message: "이메일을 입력해주세요."});

    if (userEmail.length > 30) return res.json({
        isSuccess: false,
        code: 302,
        message: "이메일은 30자리 미만으로 입력해주세요."
    });

    if (!regexEmail.test(userEmail)) return res.json({isSuccess: false, code: 303, message: "이메일을 형식을 정확하게 입력해주세요."});

    if (!userPassword) return res.json({isSuccess: false, code: 304, message: "비밀번호를 입력 해주세요."});

    if (userPassword.length < 8 || userPassword.length > 20) return res.json({
        isSuccess: false,
        code: 305,
        message: "비밀번호는 8~20자리로 입력해주세요."
    });

    if (!checkUserPassword) return res.json({isSuccess: false, code: 306, message: "비밀번호 확인을 입력해 주세요."});

    if (userPassword != checkUserPassword ) return res.json({isSuccess: false, code: 307, message: "비밀번호와 비밀번호 확인이 다릅니다."});

    if (!userName) return res.json({isSuccess: false, code: 308, message: "닉네임을 입력 해주세요."});

    if (userName.length > 10) return res.json({
        isSuccess: false,
        code: 309,
        message: "닉네임은 10자리 이하로 입력해주세요."
    });

    if (!/^([a-zA-Z0-9ㄱ-ㅎ|ㅏ-ㅣ|가-힣]).{1,10}$/.test(userName))
    return res.json({
      isSuccess: false,
      code: 310,
      message: "닉네임은 한글, 영문, 숫자만 입력가능하고 2자 이상이어야 합니다.",
    });

        try {
            // 이메일 중복 확인
            const emailRows = await userDao.userEmailCheck(userEmail);
            if (emailRows.length > 0) {

                return res.json({
                    isSuccess: false,
                    code: 311,
                    message: "중복된 이메일입니다."
                });
            }

            // 닉네임 중복 확인
            const nicknameRows = await userDao.userNicknameCheck(userName);
            if (nicknameRows.length > 0) {
                return res.json({
                    isSuccess: false,
                    code: 312,
                    message: "중복된 닉네임입니다."
                });
            }

            // TRANSACTION : advanced
           // await connection.beginTransaction(); // START TRANSACTION
            const hashedPassword = await crypto.createHash('sha512').update(userPassword).digest('hex');
            const insertUserInfoParams = [userEmail, hashedPassword,  userName];
            
            const insertUserRows = await userDao.insertUserInfo(insertUserInfoParams);

          //  await connection.commit(); // COMMIT
           // connection.release();
            return res.json({
                isSuccess: true,
                code: 200,
                message: "회원가입 성공"
            });
        } catch (err) {
           // await connection.rollback(); // ROLLBACK
           // connection.release();
            logger.error(`App - SignUp Query error\n: ${err.message}`);
            return res.status(500).send(`Error: ${err.message}`);
        }
};

 

사용자로부터 입력받는 비밀번호는 해시함수를 통해서 DB에 알 수 없게 암호 형식으로 저장해야 합니다.

 

사용자로부터 입력받는 이메일, 닉네임 등등은

개발자가 원하는 값만 입력될 수 있도록 정규식을 사용해야 합니다.

 

JS 정규식 모음 ) 전화번호, 닉네임, URL, 휴대폰 번호, 이메일 체크 정규식

 

JS 정규식 모음 ) 전화번호, 닉네임, URL, 휴대폰 번호,이메일 체크 정규식

DATA 정규식 0000-00-00 if(!/^(19|20)\d{2}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[0-1])$/.test(reservationDate)) return res.json({isSuccess: false, code: 301, message: "올바른 날짜가 아닙니다. 유효한 날..

yusang.tistory.com

 

JWT 토큰 생성

로그인 페이지 API

userController.js

 

userRoute.js

 

userDao.js

 

위의 로그인 페이지를 통해 로그인을 하게 되면 아래와 같이 토큰을 생성합니다.

이 토큰은 userController.js에서 표시한 노란색 네모 박스의 정보를 모두 가지고 있습니다.

 

실제로 그런지 아래 사이트에서 확인해 봅시다.

https://jwt.io/

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

위의 결과와 같이 이는 정보가 인코딩 된 형식이므로

이 토큰에는 암호와 같은 중요한 정보가 들어가서는 절대 안 됩니다!

 

 

발급받은 이 토큰을 검증하기 위해서는 아래와 같이 Headers에 토큰 값을 넣어 주면 됩니다.

 

이 토큰을 이용하여 로그인 중인 사용자만이 사용할 수 있는 사용자 API를 생성할 수 있습니다.

예를 들면 아래와 같습니다.

 

토큰으로부터 userId값을 받아와서 사용자 개인정보를 볼 수 있는 API를 설계할 수 있는 것입니다.

 

userController.js

 

userDao.js

 

토큰에는 아래와 같이 userId, userEmail, userName, userLatitue, userHardness가 저장되어 있습니다.

 

Postman에서의 실행 결과.

JWT 토큰을 활용하여 리뷰의 수정권한 체크

사용자가 작성한 리뷰를 삭제하거나 수정하려면 그 사용자에게 해당 리뷰를 수정할 수 있는

권한이 있는지 확인을 해야합니다.

내가 작성한 리뷰를 다른사람이 수정하거나 삭제하면 안 되기 때문이죠.

JWT토큰을 활용하여서 특정행동에 권한을 확인할 수 있습니다.

 

pathvariable로 reviewId를 넘겨받은 후

그 리뷰에 저장되어 있는 userId와 토큰에 저장된 userId가 같은지 확인을 진행합니다.

같다면 그 유저가 작성한 리뷰가 맞기 때문에 수정할 수 있고

그렇지 않다면 그 유저가 작성한 리뷰가 아니기 때문에 권한을 부여하지 않습니다.

 

userController.js

userDao.js

 

 

이렇게 JWT를 사용하여 사용자 전용 API를 여러 개 설계하는 것이 이번 주 과제의 목표입니다!!

 

 

저번 주 과제에 이어서 추가한 api를 api명세서에 추가하여 업데이트하는 중입니다!

docs.google.com/spreadsheets/d/1Zw9Q9GtI5gqAeZHfsJmILvcVwINKMHejNJ7pWRJeDVY/edit?usp=sharing

 

RP2기 api 명세서

개요 Index,Method,URI,Description,명세서,서버 반영 여부 1,GET,/main,메인조회,OK,Y 2,GET,/café,카페검색,OK,Y 3,GET,/cafe/:cafeid,카페상세조회,OK,Y 4,GET,/cafe/:cafeid/review,카페리뷰상세조회,OK,Y 5,POST,/cafe/:cafeid/review

docs.google.com

 

그럼 이번 주 과제의 기록은 여기까지!!

 

Comments