2024년 9월 25일
: 지난 시간에 이어서, validate를 유효성 검사 다음에 넣었을 때, 계속 sending request 되는 것을 확인했다.
// channels.js
const express = require("express");
const router = express.Router();
const conn = require("../mariadb.js");
const { body, param, validationResult } = require("express-validator");
router.use(express.json());
function validate (req, res) {
const err = validationResult(req);
if (!err.isEmpty()) {
return res.status(400).json(err.array());
}
}
router
.route("/")
// 채널 전체 조회
.get(
[
body("userId").notEmpty().isInt().withMessage("userId는 숫자이어야 해요")
],
(req, res) => {
validate(req, res);
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);
}
});
}
)
POSTMAN) Get + localhost:7777/channels + {”userId” : 2}
잘 나오는 것을 알 수 있다.
그럼 함수 형태가 아니라 모듈 형태로 전달하면 왜 오류가 날까 ???
: 우리는 함수 모듈로 만들어서 편리하게 사용하려고 따로 뺐는데, 에러가 그냥 없는채로 끝나버리는 걸로 router가 생각한다. (= 따로 뺀 모듈이 밖에서 오류를 발생하지 않으니깐 다음으로 무엇을 할지 몰라서 계속 샌딩됨!)
따라서, next()를 사용해야함!
: const validate = (req, res, next) => {} 를 매개변수로 넣어, 모듈로 만든 함수에서 next()를 사용해줘야함
// channels.js
const express = require("express");
const router = express.Router();
const conn = require("../mariadb.js");
const { body, param, validationResult } = require("express-validator");
router.use(express.json());
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) => {
validate(req, res);
const { userId } = req.body;
let sql = `SELECT * FROM channels WHERE user_id = ?`;
conn.query(sql, userId, function (err, results) {
if (err) {
// users에 없는 userId 넣으면
console.log(err);
return res.status(400).end();
}
if (results.length) {
res.status(200).json(results);
} else {
notFoundChannel(res);
}
});
}
)
POSTMAN) GET + localhost:7777/chanels + {”userId” : 2}
channels의 모든 API validate 모듈로 넣어주기
그런데, 클린 코드 관점에서 조건문이 부정문을 인식하기 어렵다
if (!err.isEmpty()) {
return res.status(400).json(err.array());
} else {
return next();
}
따라서, 코드를 바꿔주면
if (err.isEmpty()) {
return next();
} else {
return res.status(400).json(err.array());
}
: 우리는 '로그인 세션이 만료되었습니다' 라는 안내를 웹상에서 종종 볼 수 있다.
이 말은 무슨 말일까..?
: 그럼 인증(로그인)을 매번 해야 한다면? (상품 구매할 때마다 로그인 하거나 장바구니에 상품 담을 때마다 로그인 하면 너무 힘듦)
=> 따라서 '세션'을 사용함
세션 : (로그인이 되어 있는) 상태
쿠키 : 웹에서 서버와 클라이언트가 주고받는 데이터 중 하나
세션 : 쿠키의 단점을 보완해서 나온 해결 방안
보안에 취약한 쿠키의 단점을 보완하고, 서버 저장 공간을 쓰는 세션의 단점을 보완하기 위해서 나온 친구 : JWT (하지만 여전히 완벽하진 않음)
Json Web Token
Json 형태의 데이터를 Web에서 보안을 꽁꽁 싸매서 안전하게 전하는 Token
(= JSON 형태의 데이터를 안전하게 전송하기 위한 (웹에서 사용하는) 토큰)
cf) 토큰 : 입장 가능한 유저를 보여주기 위한 <인증용> / 관리자 권한 & 일반 유저 권한 <인가용>
토큰의 유무를 통해 인증을 할 수 있음
결국, 토큰을 가진 사용자가 증명을 하기 위한 수단
장점
: jwt.io 홈페이지를 가면 자세한 설명 볼 수 있음(특징, 구조)
https://jwt.io/
: 그럼 우리는 JWT로 어떻게 인증, 인가 할 수 있을까?
-> 로그인 하면, 아래 처럼 인증/인가 절차를 가짐
간단히 설명하면, 클라이언트는 POST API로 body에 username, password를 서버에게 요청한다. 서버는 내부 로직을 확인하고 JWT를 발행해준다. 이때, 로그인한 시점이 JWT 발행 시점이 된다. 그리고 서버는 다시 클라이언트에게 JWT를 동봉해서 주면서 다음에 어떤 요청을 할때 이 JWT를 들고 오면 서명이 맞는지만 확인해서 들여보내준다고 해준다. (인증)
그리고 클라이언트가 다른 요청(장바구니 담기, 결제 등등)을 하면 다시 로그인 하지 않고, JWT만 보여주면 서버는 JWT의 서명을 확인하고 요청들을 유효시간 내에서 허락해준다(인가)
// jwt-demo.js
const jwt = require('jsonwebtoken'); // jwt 모듈 소환
const token = jwt.sign({ foo: 'bar' }, 'shhhh');
// token 생성 = jwt 서명 (페이로드, 나만의 암호키) + SAH256(알고리즘)
// token = jwt.sign({ foo : 'bar' }, privateKey, {algorithm : 'RS256'});
// token 발행 끝
console.log(token);
검증 성공하면, 페이로드 값 확인 할 수 있음
// jwt-demo.js
const jwt = require('jsonwebtoken'); // jwt 모듈 소환
const token = jwt.sign({ foo: 'bar' }, 'shhhh');
// token 생성 = jwt 서명 (페이로드, 나만의 암호키) + SAH256(알고리즘)
// token = jwt.sign({ foo : 'bar' }, privateKey, {algorithm : 'RS256'});
// token 발행 끝
console.log(token);
// 검증
// 만약 검증에 성공하면, 페이로드 값을 확인할 수 있음!
const decoded = jwt.verify(token. 'shhhh');
console.log(decoded);
*iat(issued at) : 토큰이 발생된 시간(초형태)_발행 시간에 따라서 토큰의 형태 달라짐! → 똑같은 내용의 토큰을 발행해도 헤더값은 같아도 페이로드, 서명값은 다름!
: private key가 너무 잘보임(나중에 Github에 배포하면 다 보일거야….) → 암호키를 .env에 넣어야함
일단, private key를 문자열로 계속 써주면 오류가 발생할 확률이 크므로 변수에 넣어서 사용
// jwt-demo.js
const jwt = require('jsonwebtoken');
const privateKey = 'shhhh';
const token = jwt.sign({ foo: 'bar' }, privateKey);
console.log(token);
const decoded = jwt.verify(token. privateKey);
console.log(decoded);
그런데, private key는 여전히 코드에 나타는건 같음
이 때 사용하는 것이 .env!
private key를 우리의 프로젝트 어딘가에 숨겨놓고 외부에서 볼 수 없게 할 수 있음
// .env
PRIVATE_KEY = 'shhhh'; # JWT 암호 키
// jwt-demo.js
const jwt = require('jsonwebtoken');
const dotenv = require('dotenv');
dotenv.config(); // dotenv로 설정사용 하겠다
const token = jwt.sign({ foo: 'bar' }, process.env.PRIVATE_KEY);
console.log(token);
const decoded = jwt.verify(token. process.env.PRIVATE_KEY);
console.log(decoded);
env 파일을 만들고 나중에 깃허브와 연동할때, gitignore에 올려서 무시하는 방법으로 파일을 별도로 뺄 수 있음!!
: 클라이언트가 로그인 요청하면 서버는 JWT 토큰을 1)발행해서 클라이언트에게 2)준다 까지 구현해보자
npm i jsonwebtoken
npm i dotenv
모듈 소환하고,
youtube의 users 로그인 로직에서 jwt, dotenv를 구현해보자
일단, token 확인해보기
// .env
PRIVATE_KEY = "secret"
// users.js
const express = require("express");
const router = express.Router();
const conn = require("../mariadb.js");
const { body, param, validationResult } = require("express-validator");
// jwt 모듈
const jwt = require('jsonwebtoken');
// dotevn 모듈
const dotenv = require('dotenv');
dotenv.config();
router.use(express.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()
.isString()
.isEmail()
.withMessage("이메일을 확인해주세요"),
body("password")
.notEmpty()
.isString()
.withMessage("비밀 번호를 확인해주세요"),
validate,
],
function (req, res, next) {
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();
}
let loginUser = results[0];
if (loginUser && loginUser.password == password) {
// token 발행
const token = jwt.sign({
email : loginUser.email,
name: loginUser.name
}, procoess.env.PRIVATE_KEY); // 토큰 동봉, 보내는 준비 완료
res.status(200).json({
message: `${loginUser.name}님 로그인되었어요`,
token : token
// 토큰을 postman으로 확인해보자
});
} else {
res.status(400).json({
message: `이메일 또는 비밀번호가 틀렸습니다`,
});
}
});
}
);
원래는 express가 응답시[response를 줄 때], (서버가 클라이언트에게 토큰을 줄 때), 암호화된 token은 쿠키(cookie)에 담아 보냄
(클라이언트가 서버에게 다른 요청을 할 때 header에 토큰을 담아서 요청하듯이)
그런데, 쿠키를 이용하려면 모듈을 추가해야 하므로 일단, body에 잘 날아가는지 확인해보자.
POSTMAN) POST + localhost:7777/login + {”email” : “ko@email.com”, “password” : 1111}
message와 token이 같이 body에 날아오는 것 확인 (프론트엔드가 원하는 message와 token이 같이 응답됨)→ 우리는 쿠키에 담을 것이므로, body가 아닌 coockies에 알맞게 들어오는지 확인할 것임
: 쿠키에 JWT를 담아서 보내려고 함!
: 쿠키를 사용할 수 있는 모듈 사용하기
// users.js
... 생략 ...
if (loginUser && loginUser.password == password) {
// token 발행
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}님 로그인되었어요`,
});
} else {
res.status(400).json({
message: `이메일 또는 비밀번호가 틀렸습니다`,
});
}
... 생략 ..
POSTMAN) POST + localhost:7777/login + {”email” : “ko@email.com”, “password” : "1111"}
그리고 예외처리 상태코드(status code)를 바꿔줘야함 → ‘인증못해줘‘ 라는 것을 알려줘야함 → 403코드 사용
// users.js
... 생략 ...
} else {
res.status(403).json({
message: `이메일 또는 비밀번호가 틀렸습니다`,
});
}
});
}
);
쿠키 안을 보면,
을 확인할 수 있다.
// users.js
... 생략 ...
let loginUser = results[0];
if (loginUser && loginUser.password == password) {
// token 발행
const token = jwt.sign(
{
email: loginUser.email,
name: loginUser.name,
},
process.env.PRIVATE_KEY
);
res.cookie("token", token, { httpOnly: true });
res.status(200).json({
message: `${loginUser.name}님 로그인되었어요`,
});
} else {
res.status(403).json({
message: `이메일 또는 비밀번호가 틀렸습니다`,
});
}
... 생략 ...
POSTMAN) POST + localhost:7777/login + {”email” : “ko@email.com”, “password” : "1111"}
: 서버가 JWT(토큰)를 쿠키에 동봉해서 클라이언트에게 전달함 -> 클라이언트는 JWT(토큰)이 언제까지 유효한지 알 수 없음
평생 토큰이 만료되지 않으면 평생 로그인 되어있음 → 보안취약 → 유효기간 설정해야함
// users.js
... 생략 ...
let loginUser = results[0];
if (loginUser && loginUser.password == password) {
// token 발행
const token = jwt.sign(
{
email: loginUser.email,
name: loginUser.name,
},
process.env.PRIVATE_KEY,
{
exprieIn : '30m', // 유효기간 실정(m : minute, h: hour 단위로 설정)
issuer: 'hongbi' // 토큰 발행한 사람
}
);
res.cookie("token", token, { httpOnly: true });
console.log(token); // 콘솔창에서 보여주자
res.status(200).json({
message: `${loginUser.name}님 로그인되었어요`,
});
} else {
res.status(403).json({
message: `이메일 또는 비밀번호가 틀렸습니다`,
});
}
... 생략 ..
POSTMAN) POST + localhost:7777/login + {”email” : “ko@email.com”, “password” : "1111"}
-> 라우터를 이용해 페이지를 분기하면, 해당 페이지의 코드에 라우터가 먼저 보여야함
: 그런데 모든 코드가 한꺼번에 다 뭉쳐있음(Validate, api요청 등등) → 모듈화해서 사용해야 로직을 편하게 볼 수 있음
-> 각각의 콜백함수들도 복잡해보임
-> 예외처리에 대한 내용을 if, else문 밖에 안해봐서 실제로 해볼 내용 고민해봐야함
-> 전체 조회 쿼리에서, 예를 들면 100개의 채널을 만들면 100개의 채널을 다 보여줄 건지 고민해봐야함 (ex) 카카오 쇼핑에서는 제한적으로 보여줌. 20-30개씩 → database paging)
-> 코드 정돈이 안되어 있어서 고민해보는 시간을 가져보자
🍎🍏 오늘의 느낀점
: 와우,, 오늘은 정말 많은 것들을 실습해본 느낌이다. 쿠키, 세션, JWT에 대해서 새롭게 배워서 그동안 '로그인 세션이 만료되었습니다'하는 팝업을 항상 어떻게 왜 이게 나오는지 의문이었는데 오늘 수업을 통해서 알게 되어서 좋다:) 그리고 validate 유효성 검사를 쉽게 라이브러리를 이용해서 쓰는 것을 보고, 앞으로 어떤 프로젝트를 할 때 내가 유용하게 쓸 수 있는 라이브러리를 먼저 검색해보고 잘 이용해 볼수 있는 방법은 없을까 하는 생각도 가져야겠다. 이상 끝,!