안녕하세요 건브로입니다.
오늘은 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도 안 좋다.
그나마 좋은 방법은 이 분의 블로그를 참고하면 된다.
🍪 프론트에서 안전하게 로그인 처리하기 (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 |