JWT를 이용한 토큰발행 연습해보기

ssomae·2024년 9월 25일

DevCourse

목록 보기
21/29
post-thumbnail

next()

우선 Middle ware란?

req, res, 그리고 next 함수에 액세스 권한을 갖는 함수이다.

즉, Express내에서 우리가 작성하는 대부분의 코드는 미들웨어라고도 볼 수 있다.

next :

const express = require('express');
const app = express();
const port = 8080;

//GET http://localhost:8080/
app.get('/', (req, res, next) => {
  console.log("1")
  next()
})

app.get("/", (req, res, next) => {
  console.log("2")
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

결과 값은 다음과 같다.

//console.log()
Example app listening on port 8080
1
2
  • 이를 보아 알수있는 사실은 next()가 다음 함수, route를 실행시킨다는 사실이다.
  • 이를 통해 유효성 검사를 간편하게 해보자

channel.js

const validate = (req,res,next) => {
    const err = validationResult(req);
    if (!err.isEmpty()) {
        return res.status(400).json(err.array())
    } else {
        return next();
    }
}
router
    .route('/')
    .get(
        [
            body('userId').notEmpty().isInt().withMessage('userId는 숫자여야 합니다.'),
            validate
        ]
        , (req, res,next) => {//채널 전체 조회
            const { userId } = req.body;
            let sql = `SELECT * FROM channels WHERE user_id = ?`
            conn.query(sql, userId,
                function (err, results) {
                    if (err) {
                        console.log(err);
                        return res.status(400).end();
                    }
                    if (results.length)
                        res.status(200).json(results)
                    else
                        notFoundChannel(res);
                }
            )
    })
  • 이를 토대로 users.js 도 업데이트 해보자

users.js 업데이트

const express = require('express');
const router = express.Router();
const conn = require('../mariadb')
const { body,param, validationResult } = require('express-validator')
router.use(express.json()) // http 외 모듈 'json' 사용

const validate = (req,res,next) => {
    const err = validationResult(req);
    if (err.isEmpty()) {
        return next();
    } else {
          return res.status(400).json(err.array())
    }
}

// 로그인
router.post(
    '/login',
    [
        body('email').notEmpty().isEmail().withMessage('올바른 이메일 형식을 입력해주세요'),
        body('password').notEmpty().isString.withMessage('비밀번호 확인 필요'),
        validate
    ],
    function (req, res) {
        const { email, password } = req.body;
        let loginUser = {};
        let sql = `SELECT * FROM users WHERE email = ?`
        conn.query(sql,email,
            function (err, results) { 
                if (err) {
                        console.log(err);
                        return res.status(400).end();
                    }
                loginUser = results[0];

                if (loginUser && loginUser.password === password) { //email이 db에 저장된 회원인지 확인
                    res.status(200).json({
                        message: `${loginUser.name}님 로그인 되었습니다.`
                    })
                }
                else {
                    res.status(404).json({
                        message : "이메일 또는 비밀번호가 틀렸습니다"
                    })
                }
            }
        );  
})

// 회원 가입
router.post(
    '/join',
    [
        body('email').notEmpty().isEmail().withMessage('올바른 이메일 형식을 입력해주세요'),
        body('name').notEmpty().isString.withMessage('이름 확인 필요'),
        body('password').notEmpty().isString.withMessage('비밀번호 확인 필요'),
        body('contact').notEmpty().isString.withMessage('연락처 확인 필요'),
        validate
    ],
    function (req, res) {
      
    const { email, name, password, contact } = req.body;
    let sql = `INSERT INTO users (email, name, password, contact) VALUES (?, ?, ?, ?)`;
    let values = [email, name, password, contact]; 

    conn.query(sql,values,
        function (err, results, fields) {
            if (err) {
                console.log(err);
                return res.status(400).end();
            }
            res.status(201).json(results);
        }
    )
        
});

router
    .route('/users') 
    .get(//회원 개별 조회
        [
            body('email').notEmpty().isEmail().withMessage('올바른 이메일 형식을 입력해주세요'),
            validate
        ],
        function (req, res) {
            let { email } = req.body;
            let sql = `SELECT * FROM users WHERE email = ?`
            conn.query(sql,email,
                function (err, results, fields) {
                    if (err) {
                        console.log(err);
                        return res.status(400).end();
                    }
                    res.status(200).json(results)          
                }
            );   
            
    })
    .delete( //회원 탈퇴
        [
            body('email').notEmpty().isEmail().withMessage('올바른 이메일 형식을 입력해주세요'),
            validate
        ],
        function (req, res) {
            let { email } = req.body;
            let sql = `DELETE FROM users WHERE email = ?`
            conn.query(sql, email,
                function (err, results, fields) {
                   if (err) {
                        console.log(err);
                        return res.status(400).end();
                    }

                    if (results.affectedRows === 0) {
                        return res.status(400).end();
                    } else {
                        res.status(200).json(results)   
                    }
                }
            ); 
    })

module.exports = router // 모듈화 진행

로그인 과정 심화!

로그인(인증) 세션 만료

  • 다음과 같은 창을 인터넷하면서 자주 만나볼수 있다.
  • 오류가 아니라 사용자의 보안을 위해 시행하는 방법이다.

인증과 인가

인증 ( = 로그인)

  • Authentication
  • 쇼핑몰 상품 볼 때 ? X
  • 쇼핑몰 장바구니 담을 때? O
  • 쇼핑몰 상품 구매할 때? O
  • 로그인을 통해 해당 사이트에 가입된 유저라는걸 확인시키게함

인가

  • Authorization
    • 관리자 / 고객
    • 같은 사이트 내에 관리자 / 고객 에 따라 접근할 수 있는 페이지가 다르다.
  • 관리자든 고객이든 인증을 통해서 사이트에 가입된 사용자라는걸 증명하는것
  • 인증 후에 해당 페이지에 대한 접근 권한이 있는지 확인하는 과정이 인가

쿠키와 세션

쿠키

  • 웹에서 서버와 클라이언트가 주고받는 데이터 중 하나
  • 정확히는 생성은 웹 서버가 해서, 웹 브라우저에게 주면, 브라우저가 자기 메모리에 저장해두고,
    다음에 같은 웹서버 방문할 때 쿠키 들고 요청하러 간다.
  • 장점 : 서버가 저장하지 않는다 → 서버 저장 공간, Stateless → RESTful
  • 단점 : 보안이 취약하다.

세션

  • 쿠키의 보안적인 단점을 보완하기 위한 방안이다.
  • 쿠키에 중요한 정보를 담지 말고, 중요한 정보는 서버에 저장해두고
    그 정보가 어딨는지 주소만 적어서 쿠키에 담는다.
  • 쿠키에 넣어서 보내기엔 중요한 내용은 서버가 가진 세션에 넣어 두고
    세션 ID만 쿠키에 넣어서 통신한다.
  • 장점 : 보안이 비교적 좋다
  • 단점 : 서버가 저장하기 때문에 서버 저장 공간 낭비, Stateless X

JWT (JSON Web Token)

개념

  • JSON 형태의 데이터를 안전하게 전송하기 위한 (웹에서 사용하는) 토큰
  • 토큰을 가진 사용자가 증명을 하기 위한 수단
  • 토큰 : (인증용) 입장 가능한 유저 / (인가용) 관리자 권환 & 일반 유저 권한

장점

  • 보안에 강하다 → 암호화가 되어 있다.
  • Stateless하다. (HTTP 특징을 잘 따랐다) ← 서버가 상태를 저장하지 않는다.
  • 서버 부담을 줄여줄 수 있다.
    • 토큰을 발행하는 서버를 따로 만들어줄 수도 있다.

JWT의 구조

image.png

  • 헤더에는 어떤 알고리즘과 타입을 사용하는지 명시되어 있다.
  • 페이로드에는 데이터에 대한 값에 대한 설명이 명시되어 있다.
  • 시그니처에는 헤더의 인코딩값과, 정보의 인코딩값을 합친후 주어진 비밀키로 해쉬를 하여 생성합니다.
    • 만약 페이로드 값이 바뀌면, 서명값 통째로 바뀌기 때문에 우리는 JWT를 믿고 쓸 수 있다.

JWT로 인증/인가 하는 절차

1. 로그인 요청 (인증)

  • 사용자가 로그인 폼을 통해 사용자 이름과 비밀번호 같은 자격 증명을 서버에 보냅니다.

2. 서버에서 자격 증명 확인

  • 서버는 이 자격 증명이 올바른지 데이터베이스 등에서 확인한 후, 유효하면 JWT를 발급합니다. 이 JWT는 사용자에 대한 정보를 포함하고, 서명되어 있어 변조할 수 없습니다.

3. JWT 반환

  • 서버는 이 JWT를 클라이언트(웹 브라우저 등)에 반환합니다. 클라이언트는 이 JWT를 저장하고 이후의 요청에서 인증을 위해 사용합니다. 보통 JWT는 로컬 스토리지나 쿠키에 저장됩니다.

4. 인가 요청

  • 사용자는 서버에 다시 요청을 보낼 때, JWT를 HTTP 헤더에 포함시켜 함께 보냅니다. 예를 들어, Authorization: Bearer 와 같은 형식으로 헤더에 포함합니다.

5. 서버에서 JWT 확인

  • 서버는 클라이언트가 보낸 JWT를 확인합니다. 이때 서버는 JWT가 유효한지, 만료되지 않았는지, 서명이 변조되지 않았는지를 검사합니다.

6. 인가 처리

  • JWT가 유효하면 서버는 요청한 리소스에 대한 접근을 허용하거나, 필요한 데이터를 반환합니다. 이때 JWT에 포함된 정보를 바탕으로 사용자가 접근할 수 있는 리소스인지 확인하는 절차를 인가라고 합니다.

7. JWT 만료 및 갱신

  • JWT는 유효 기간이 있어서, 만료되면 클라이언트는 다시 로그인하거나, 갱신(refresh token) 요청을 통해 새로운 JWT를 받아야 합니다.

요약

  1. 사용자 로그인 → 서버에서 JWT 발급

  2. 클라이언트는 JWT를 저장하고, 이후 요청에 JWT를 포함

  3. 서버는 JWT를 확인하고, 유효하면 요청 처리

image.png

JWT 실습해보기

npm install jsonwebtoken
var jwt = require('jsonwebtoken');
var token = jwt.sign({ foo: 'bar' }, 'shhhhh');
//token 생성 = jwt 서명을 했다. (페이로드, 나만의 암호키) + SHA256

console.log(token);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE3MjcyNTk2NzR9.bMtomWZJnk2UjLV4KSIN-sIM8pp4nl1-MyoDafoOMf4

// 검증
// 만약 검증에 성공하면, 페이로드 값을 확인할 수 있음
let decoded = jwt.verify(token, 'shhhhh');
console.log(decoded);
// { foo: 'bar', iat: 1727259674 }

암호키를 안전하게 사용하는 방법

  • shhhhh 암호키를 안전하게 사용해보자
  1. 변수에 저장하기
var jwt = require('jsonwebtoken');
var privateKey = 'shhhhh';
var token = jwt.sign({ foo: 'bar' }, privateKey);
//token 생성 = jwt 서명을 했다. (페이로드, 나만의 암호키) + SHA256

console.log(token);

// 검증
// 만약 검증에 성공하면, 페이로드 값을 확인할 수 있음
let decoded = jwt.verify(token, privateKey);
console.log(decoded);
  1. .env (environment 환경 변수 설정 값) 이용하기
    • 개발을 하다가 포트넘버, 데이터베이스 계정, 암호 키 … 등등 외부에 유출되면 안되는 중요한 환경 변수
      들을 따로 관리하기 위한 파일
      - cf . 깃허브에 올라가면 안되는 값
    • 파일 확장자가 .env
      • dotenv 모듈을 사용해서 쓰자
    • npm을 통해 dotenv를 설치후 패키지 최상위 경로에 .env 파일을 생성한 후 저장할 데이터들을 작성한다.
var jwt = require('jsonwebtoken');

require('dotenv').config({ path: './.env' });
console.log('Private Key:', process.env.PRIVATE_KEY);
var token = jwt.sign({ foo: 'bar' }, process.env.PRIVATE_KEY);
//token 생성 = jwt 서명을 했다. (페이로드, 나만의 암호키) + SHA256

console.log(token);

// 검증
// 만약 검증에 성공하면, 페이로드 값을 확인할 수 있음
let decoded = jwt.verify(token, process.env.PRIVATE_KEY);
console.log(decoded);

직접 사용해보기

const express = require('express');
const router = express.Router();
const conn = require('../mariadb');
const { body, param, validationResult } = require('express-validator');

//jwt, dotenv 모듈설정
const jwt = require('jsonwebtoken');
const dotenv = require('dotenv')
dotenv.config();

router.use(express.json()); // http 외 모듈 'json' 사용

const validate = (req,res,next) => {
    const err = validationResult(req);
    if (err.isEmpty()) {
        return next();
    } else {
          return res.status(400).json(err.array())
    }
}

// 로그인
router.post(
    '/login',
    [
        body('email').notEmpty().isEmail().withMessage('올바른 이메일 형식을 입력해주세요'),
        body('password').notEmpty().isString().withMessage('비밀번호 확인 필요'),
        validate
    ],
    function (req, res) {
        const { email, password } = req.body;
        let loginUser = {};
        let sql = `SELECT * FROM users WHERE email = ?`
        conn.query(sql,email,
            function (err, results) { 
                if (err) {
                        console.log(err);
                        return res.status(400).end();
                    }
                loginUser = results[0];

                if (loginUser && loginUser.password === password) { //email이 db에 저장된 회원인지 확인

                    //토큰 발행
                    const token = jwt.sign({
                        email: loginUser.email,
                        name: loginUser.name
                    }, process.env.PRIVATE_KEY);
                    
                    res.cookie("token", token);
                    
                    res.status(200).json({
                        message: `${loginUser.name}님 로그인 되었습니다.`,
                        token: token
                    })
                }
                else {
                    res.status(404).json({
                        message : "이메일 또는 비밀번호가 틀렸습니다"
                    })
                }
            }
        );  
})
  • 토큰을 쿠키에 담기위해서 *res*.cookie("token", token); 을 통해 쿠키를 만들수 있다.

Cookie의 속성

  1. HttpOnly
  • 설명: 쿠키에 HttpOnly 속성을 설정하면 JavaScript에서 해당 쿠키에 접근할 수 없게 만듭니다. 즉, 클라이언트 측 스크립트(XSS 공격 등)를 통해 쿠키가 탈취되는 것을 방지하는 역할을 합니다.
  • 주요 역할: XSS(크로스 사이트 스크립팅) 공격 방지
  • 사용 예시: 인증 토큰과 같이 민감한 정보를 저장할 때 사용
  1. Secure
  • 설명: Secure 속성이 설정된 쿠키는 HTTPS 연결에서만 전송됩니다. 즉, HTTP와 같은 비보안 연결에서는 쿠키가 전송되지 않아 네트워크 상에서의 쿠키 탈취를 방지합니다.
  • 주요 역할: 쿠키가 암호화된 HTTPS 연결에서만 전송되도록 하여 안전하게 보호
  • 사용 예시: HTTPS를 사용하는 경우 모든 중요한 쿠키에 Secure 속성을 설정하는 것이 권장됩니다.

다음과 같이 설정 변경이 가능하다.

res.cookie("token", token, {
    httpOnly:true
});

JWT의 유효기간 설정

다음과 같이 유효시간과 발행자를 설정해줄수 있다.

const token = jwt.sign({
    email: loginUser.email,
    name: loginUser.name
	}, process.env.PRIVATE_KEY, {
    expiresIn: '30m',
    issuer: "ssommae"
});
profile
성장해나갈 개발자

0개의 댓글