관심있는 언어들/Node.js

[Node.js] 초보자의 JWT 정리

건브로 2021. 2. 26. 01:08

안녕하세요 건브로입니다.

오늘은 JWT에 대해서 정리 해보겠습니다.

 

일단, 정리하기 전에 이 글은 정리한 만큼 저의 생각도 포함되어 있습니다.

그리고 구글링으로 찾아봐도 잘 없는 mysql + express + react를

제가 직접 부딪히면서 만들어본 경험을 바탕으로 글을 쓰겠습니다.

 

※주의:

1.밑에 나오는 코드는 새로 쓴 코드가 아닌 제가 만들다가 적용한 코드이므로

부분적으로만 참고하기를 바랍니다.

2. 클라이언트와 서버를 클라이언트 쪽 package.json에서 proxy를 적용해서

클라이언트와 서버가 통신이 가능하다는 전제로 보셔야 합니다.

3. mysql table 만드는 부분은 생략했으며, 쿼리를 이용해서 데이터를 꺼내거나 데이터를

넣는 것만 설명이 있으니 주의 바랍니다.

 

"proxy": "http://localhost:5000/"

 


아마 다들 쉽게 쉽게 JWT를 배우고 싶었을거다.

근데 웬열?

구글링으로 찾아봐도 타입스크립트를 곁들인 express와 mongoDB, mongoDB와 express 등등을

봤을 거다. 나 같은 경우는 mongoDB도 잘 모르고, typescript도 아직 배우질 않아서

잘 모른다. 대부분 sql을 초반에 배웠을 것 같고, mysql이 친근할거라고 생각한다.

 

1. JWT는 무엇인가?

 

이 부분은 간단하게 jwt를 정리한 부분이므로 알고 있다면 넘어가도된다.

JSON Web Token (JWT) 은 웹표준 (RFC 7519) 으로서 두 개체에서 JSON 객체를 사용하여 가볍고 자가수용적인 (self-contained) 방식으로 정보를 안전성 있게 전달해줍니다.
출처: velopert.com/2389
 

[JWT] JSON Web Token 소개 및 구조 | VELOPERT.LOG

지난 포스트에서는 토큰 기반 인증 시스템의 기본적인 개념에 대하여 알아보았습니다. 이 포스트를 읽기 전에, 토큰 기반 인증 시스템에 대해서 잘 모르시는 분들은 지난 포스트를 꼭 읽어주세

velopert.com

 

JWT에는 헤더(header), 정보(payload), 서명(signature)의 정보가 들어간다.

 

header에는 typ, alg라는 2가지 정보가 담겨져 있다.

 

payload에는 토큰에 담을 정보가 들어가 있다.

payload에 클레임이라는 한 조각의 정보가 여러 개가 들어 갈 수 있다.

그리고 클레임의 종류는 세 가지이다.

 

1) registered claim: 토큰에 대한 정보를 담기 위해 이름이 이미 정해진 클레임이다.

2) public claim: 충돌이 방지된 이름을 가지고 있어야하는 클레임이다. URI 형식을 사용해서 충돌 방지한다.

3) private claim: 서버와 클라이언트 협의 하에 사용되는 클레임 이름이다. 이름이 중복되어 충돌 가능성이 있다.

 

서명에는 헤더의 인코딩 값 + 정보의 인코딩 값+ 주어진 비밀키를 해쉬하여 생성된다.

 

2. 코드(Client)

1) Login.js 핵심

axios로 보낼 state는 userId와 userPassword이다.

const enterId = () => {
	const {userId, userPassword} = this.state;
    axios.post("/api/login", {
    	userId,
        userPassword
    })
    .then((response)=>{
    	console.log(response.data);
    })
    .catch((error)=> {
    	console.log(error);
    });
}

2) Login.js 전체 코드

import React from "react";
import axios from "axios";
import "bootstrap/dist/css/bootstrap.min.css";
import { Form, Button } from "react-bootstrap";
import "./Login.css";
import Dialog from "@material-ui/core/Dialog";

class Login extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      userId: "",
      userPassword: "",
      open: false,
    };
  }
  handleId = (e) => {
    this.setState({
      userId: e.target.value,
    });
  };

  handlePassword = (e) => {
    this.setState({
      userPassword: e.target.value,
    });
  };

  handleSubmit = (e) => {
    e.preventDefault();
    this.enterId();
  };

  enterId() {
    const { userId, userPassword } = this.state;
    axios
      .post("/api/login", {
        userId,
        userPassword,
      })
      .then((response) => {
        console.log(response.data);
      })
      .catch((error) => {
        console.log(error);
      });
  }

  handleClickOpen = () => {
    this.setState({
      open: true,
    });
  };

  handleClose = () => {
    this.setState({
      userId: "",
      userPassword: "",
      open: false,
    });
  };

  render() {
    const formStyle = {
      margin: 50,
    };
    const buttonStyle = {
      marginTop: 5,
    };
    const middleStyle = {
      display: "flex",
      justifyContent: "center",
    };
    return (
      <Dialog
        open={this.props.islogin}
        aria-labelledby="simple-modal-title"
        aria-describedby="simple-modal-description"
      >
        <Form onSubmit={this.handleSubmit} style={formStyle}>
          <Form.Group controlId="formBasicEmail">
            <Form.Label style={middleStyle}>아이디</Form.Label>
            <Form.Control
              type="text"
              placeholder="아이디"
              value={this.state.userId}
              onChange={this.handleId}
            />
          </Form.Group>
          <Form.Group controlId="formBasicPassword">
            <Form.Label style={middleStyle}>비밀번호</Form.Label>
            <Form.Control
              type="password"
              placeholder="비밀번호"
              value={this.state.userPassword}
              onChange={this.handlePassword}
            />
          </Form.Group>
          <Form.Group controlId="formBasicCheckbox">
            <Form.Check type="checkbox" label="로그인 상태유지" />
          </Form.Group>
          <Button variant="success" type="submit" style={buttonStyle} block>
            로그인
          </Button>
          <Button
            variant="danger"
            style={buttonStyle}
            onClick={this.handleClose}
            block
          >
            나가기
          </Button>
        </Form>
      </Dialog>
    );
  }
}
export default Login;

 

3. 코드(Server)

1) server.js 핵심

▶jwt를 적용하기 위해서는 어떤 모듈이 필요한가?

const express = require("express");
const bodyParser = require("body-parser");
const jwt = require("jsonwebtoken");
require("dotenv").config();
const cookie = require('cookie');

 

fs 모듈은 핵심은 아니다.

왜냐하면 database.json을 만들어서 나의 데이터베이스 정보들을 넣었기 때문이다.

아래의 코드 처럼 사용해서 해도된다!

대신 연습용으로만 이렇게 만들어서 하기.

왜냐하면 보안적으로 좋지는 않기 때문이다.

const connection = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: 'root',
  port: '3306',
  database: '데이터베이스 이름',
});

 

 "require("dotenv").config();"는 무엇인가?

 

일단 이 모듈을 다운 받으려면 npm install dotenv를 입력하면된다.

이 모듈은 .env 안에 있는 값을 읽게 도와주는 모듈이다.

 

require("dotenv").config();

const YOUR_SECRET_KEY = process.env.SECRET_KEY;

.env 파일 안에는 

SECRET_KEY=mySuperSecretKey 이 값을 넣으면 된다.

 

▶ 쿠키 모듈과 jwt 모듈의 사용법은?

 

jwt는 무조건 사용해야하는 모듈이며, 만약 쿠키에 토큰을 저장할 거라면

쿠키 모듈을 사용하는 게 좋다.

다만 쿠키에 넣어서 하는 것은 보안상 좋지 않다.

localstorage도 안 좋다.

 

그나마 좋은 방법은 이 분의 블로그를 참고하면 된다.

velog.io/@yaytomato/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0

 

🍪 프론트에서 안전하게 로그인 처리하기 (ft. React)

localStorage냐 쿠키냐 그것이 문제로다

velog.io

보안에 대해서 정말 잘 나와 있으니  보안에 민감한 사람들은 참고하자!

 

아래의 코드가 핵심 중 핵심이다.

app.post("/api/login", (req, res) => {
  let isUser = false;
  const { userId, userPassword } = req.body;
  var cookies = cookie.parse(req.headers.cookie);
  console.log(cookies.user);

  const sql = "SELECT userId, userPassword FROM MEMBER";
  connection.query(sql, (err, rows, fields) => {
    if (err) {
      console.log(err);
    } else {
      console.log(rows);
      rows.forEach((info) => {
        if (info.userId === userId && info.userPassword === userPassword) {
          isUser = true;
        } else {
          return;
        }
      });
      if (isUser) {
        const YOUR_SECRET_KEY = process.env.SECRET_KEY;
        const accessToken = jwt.sign(
          {
            userId,
          },
          YOUR_SECRET_KEY,
          {
            expiresIn: "1h",
          }
        );
        res.cookie("user", accessToken);
        res.status(201).json({
          result: "ok",
          accessToken,
        });
      } else {
        res.status(400).json({ error: 'invalid user' });
      }
    }
  });
});

 

cookie.parse()를 이용하면 쿠키를 객체 타입으로 만들 수 있다.

쿠키를 가져올 때 문자열로 만들어져 있기 때문에 반드시 객체로 바꿔줘서 가져올 쿠키만 고르면된다.

 

더보기

나 같은 경우는 쿠키를 클라이언트로부터 가져오는 방법을 몰랐다.

근데, "console.log(req);"를 입력하니까 많은 정보들이 나왔다.

그냥 삽질을 계속하다 보니 찾았으며,

콘솔에 찍어서 데이터를 확인하는 게 최고의 디버깅 방법이다.

forEach를 사용하여 사용자의 로그인 정보가 맞는지 확인하여,

맞다면 isUser는 true로 바뀐다.

 

const accessToken = jwt.sign(
          {
            userId,
          },
          YOUR_SECRET_KEY,
          {
            expiresIn: "1h",
          }
        );

 

이 부분은 jwt를 생성하는 코드이다.

첫 번째 인자로는 보낼 정보를 보내면되고,

두 번째 인자로는 비밀키를 넣으면 된다.

 

그리고 마지막 인자로는 저렇게 옵션을 넣어도 되며, 콜백 함수를 넣어도 된다.

그리고 payload에 userPassword를 안 넣은 이유는 jwt 내용을 해독해주는 사이트에서

내용을 볼 수 있기 때문에 보안상 누가 알아도 되는 정보를 넣어줘야 한다.

민감한 정보는 자기만 알자

jwt.sign(payload, secretOrPrivateKey, [options, callback])

출처: www.npmjs.com/package/jsonwebtoken#usage
 

jsonwebtoken

JSON Web Token implementation (symmetric and asymmetric)

www.npmjs.com

 

2) server.js 전체 코드

router를 쓰지고 않고 하나의 파일에 몽땅 넣었는데, 초보자 입장(나의 입장😊)에서는

라우트라는 폴더를 이용해서 연결하는 것보다는 직관적으로 보여주는 게 낫다고 싶어서

다 넣었다.

const fs = require("fs"); //database.json으로 부터 데이터 베이스 환경설정 정보를 읽어야한다.
const express = require("express");
const bodyParser = require("body-parser");
const jwt = require("jsonwebtoken");
require("dotenv").config();
const cookie = require('cookie');
const app = express();
const port = process.env.PORT || 5000; //5000포트

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const data = fs.readFileSync("./database.json");
const conf = JSON.parse(data);
const mysql = require("mysql");

const connection = mysql.createConnection({
  host: conf.host,
  user: conf.user,
  password: conf.password,
  port: conf.port,
  database: conf.database,
});
connection.connect();

app.post("/api/member", (req, res) => {
  const { userName, userId, userPassword, birthday, gender, eMail } = req.body;
  console.log("name :", userName);
  console.log("name :", userId);
  console.log("name :", userPassword);
  console.log("name :", birthday);
  console.log("name :", gender);
  console.log("name :", eMail);

  const sql =
    "INSERT INTO MEMBER(userName,userId, userPassword,birthday,gender, eMail) VALUES(?,?,?,?,?,?)";
  const params = [userName, userId, userPassword, birthday, gender, eMail];
  connection.query(sql, params, (err, rows, fields) => {
    if (err) {
      console.log(err);
    } else {
      console.log(rows);
    }
  });
});

app.post("/api/login", (req, res) => {
  let isUser = false;
  const { userId, userPassword } = req.body;
  // console.log("name :", userId);
  // console.log("name :", userPassword);
  // console.log(req.headers.cookie);
  var cookies = cookie.parse(req.headers.cookie);
  console.log(cookies.user);

  const sql = "SELECT userId, userPassword FROM MEMBER";
  connection.query(sql, (err, rows, fields) => {
    if (err) {
      console.log(err);
    } else {
      console.log(rows);
      rows.forEach((info) => {
        if (info.userId === userId && info.userPassword === userPassword) {
          isUser = true;
        } else {
          return;
        }
      });
      if (isUser) {
        const YOUR_SECRET_KEY = process.env.SECRET_KEY;
        const accessToken = jwt.sign(
          {
            userId,
          },
          YOUR_SECRET_KEY,
          {
            expiresIn: "1h",
          }
        );
        res.cookie("user", accessToken);
        res.status(201).json({
          result: "ok",
          accessToken,
        });
      } else {
        res.status(400).json({ error: 'invalid user' });
      }
    }
  });
});

app.get("/api/member/id", (req, res) => {
  const sql = "SELECT userId FROM MEMBER";
  connection.query(sql, (err, rows, fields) => {
    if (err) {
      console.log(err);
    } else {
      console.log(rows);
      res.send(rows);
    }
  });
});
app.listen(port, () => console.log(`Listening on port ${port}`));

 

여기까지!

 

'관심있는 언어들 > Node.js' 카테고리의 다른 글

[Node.js] REPL 사용 및 모듈 만들기  (0) 2021.07.27
[Node.js] 이벤트 루프  (0) 2021.07.22
[Node.js] 쿠키  (0) 2021.02.14