1️⃣ 유효성 검사를 미들웨어로 분리해보자
const validate = (req, res) => { // 변수에 담으면 함수를 모듈처럼 사용이 가능하다
const err = validationResult(req) // 만약 에러가 있다면
if (!err.isEmpty()){ // 에러를 반환하도록
return res.status(400).json(err.array()) // return을 써주면 함수가 곧바로 종료됨
}
}
.get(
[body('userId').notEmpty().isInt().withMessage('숫자 입력 필요'), // 유효성 검사는 사용자의 요청에 대해서 구분이 가능
validate, // 배열에 유효성 검사 대상 뿐 아니라 모듈도 넣을 수 있음
// 콜백함수 전에 유효성 검사 실행과 유효성 검사에 대한 에러가 난다면 모듈 작동이 되도록 함
], (req, res) => {
let {userId} = req.body
let sql = 'SELECT * FROM channels WHERE user_id = ?'
conn.query(sql, userId,
function(err, results) {
if (err) {
console.log(err)
res.status(400).end()
}
if (results.length) {
res.status(200).json(results) // 쿼리 결과를 클라이언트에 응답
} else {
notFoundChannel(res)
}
}
)
})
모듈이 잘 실행됨

근데 정상적인 데이터를 보냈을 때는 무한로딩이 됨, 벨리데이트 모듈을 실행하고 콜백함수로 못 넘어가는 중 미들웨어로 선언하였기 때문에 발생하는 것임

1️⃣ validate가 함수로는 잘 작동할까?
function validate (req, res) { // 변수에 담으면 함수를 모듈처럼 사용이 가능하다
const err = validationResult(req) // 만약 에러가 있다면
if (!err.isEmpty()){ // 에러를 반환하도록
return res.status(400).json(err.array()) // return을 써주면 함수가 곧바로 종료됨
}
}
.get(
[body('userId').notEmpty().isInt().withMessage('숫자 입력 필요') // 유효성 검사는 사용자의 요청에 대해서 구분이 가능
], (req, res) => {
validate(req,res)
let {userId} = req.body
let sql = 'SELECT * FROM channels WHERE user_id = ?'
conn.query(sql, userId,
function(err, results) {
if (err) {
console.log(err)
res.status(400).end()
}
if (results.length) {
res.status(200).json(results) // 쿼리 결과를 클라이언트에 응답
} else {
notFoundChannel(res)
}
}
)
})

2️⃣ 그렇다면 next()를 이용하자
이런 경우는 에러가 안날 때 다음 콜백함수로 넘겨주기 위해 next를 사용함

next는 이렇게 사용한다.
const validate = (req, res, next) => { // 변수에 담으면 함수를 모듈처럼 사용이 가능하다
const err = validationResult(req) // 만약 에러가 있다면
if (!err.isEmpty()){ // 에러를 반환하도록
return res.status(400).json(err.array()) // return을 써주면 함수가 곧바로 종료됨
} else {
return next(); // 다음 할 일(미들웨어, 함수 등)을 찾아가기 위한 코드
}
}
1️⃣ 채널에 validate 모듈을 모두 적용해주고 정리함
const express = require('express')
// 모듈화를 위해 서버를 불러오는 건 app.js로 넘김
const router = express.Router() // express에 Router로 사용할 수 있도록 만들어 줌
// app이라는 서버에 직접 연결 해주던 것을 app,js에 넘기게 되어
// app이 사용된 모든 곳을 router로 변경 해줌
const conn = require('../mariadb'); // Promise 기반의 mariadb 연결 객체 가져옴
const {body, param, validationResult} = require('express-validator') // body 메소드를 불러와 벨리데이터 모듈을 넣어준다, 추가적으로 오류를 받아주는 리절트 변수도 넣어준다
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()) // return을 써주면 함수가 곧바로 종료됨
}
}
router
.route('/') // route로 URL 묶어주기
// 채널 개별 생성
.post(
[
body('userId').notEmpty().isInt().withMessage('숫자입력 필요'), // 바디 메소드에 검사할 데이터를 넣고 조건들을 부여함
body('name').notEmpty().isString().withMessage('문자입력 필요'),
validate
],
(req, res) => {
const {name, userId} = req.body
let sql = `INSERT INTO channels (name, user_id) VALUES (?, ?)`
let values = [name, userId]
conn.query(sql, values,
function(err, results) {
if (err)
{ console.log(err)
return res.status(400).end()
} // 에러가 걸리면 끝내도록 설정
res.status(200).json(results) // 쿼리 결과를 클라이언트에 응답
}
)
})
// 채널 전체 조회
.get(
[
body('userId').notEmpty().isInt().withMessage('숫자 입력 필요'), // 유효성 검사는 사용자의 요청에 대해서 구분이 가능
validate // 여기에 유효성 검사 대상 뿐 아니라 모듈도 넣을 수 있음, 콜백함수 전에 유효성 검사와 유효성 검사에 대한 에러가 난다면 모듈 작동이 되는 것
],
(req, res) => {
let {userId} = req.body
let sql = 'SELECT * FROM channels WHERE user_id = ?'
conn.query(sql, userId,
function(err, results) {
if (err) {
console.log(err)
res.status(400).end()
}
if (results.length) {
res.status(200).json(results) // 쿼리 결과를 클라이언트에 응답
} else {
notFoundChannel(res)
}
}
)
})
router
.route('/:id')
// 채널 개별 조회
.get(
[
param('id').notEmpty().withMessage('채널id 필요'), // param은 url에서 꺼내쓰는 것이기 때문에 body가 아닌 pram으로 지정
validate
],
(req, res) => {
const err = validationResult(req) // 만약 에러가 있다면
if (!err.isEmpty()){ // 에러를 반환하도록
return res.status(400).json(err.array()) // return을 써주면 함수가 곧바로 종료됨
}
let {id} = req.params // req.params 객체에서 URL 경로에 포함된 id 값을 추출하여 id 변수에 저장
id = parseInt(id) // URL에서 문자열로 추출된 id를 정수로 변환
let sql = 'SELECT * FROM channels WHERE id = ?'
conn.query(sql, id, // email 값을 바인딩
function(err, results) {
if (err) {
console.log(err)
res.status(400).end()
}
if (results.length) {
res.status(200).json(results); // 쿼리 결과를 클라이언트에 응답
} else {
notFoundChannel(res)
}
}
)
})
// 채널 개별 수정
.put(
[
param('id').notEmpty().withMessage('채널id 필요'), // param은 url에서 꺼내쓰는 것이기 때문에 body가 아닌 pram으로 지정
body('name').notEmpty().isString().withMessage('채널명 오류'),
validate
],
(req, res) => {
let {id} = req.params
id = parseInt(id)
let {name} = req.body
let sql = 'UPDATE channels SET name = ? WHERE id = ?'
let values = [name, id]
conn.query(sql, values,
function(err, results) {
if (err) {
console.log(err)
res.status(400).end()
}
if (results.affectedRows == 0) {
return res.status(400).end() // 쿼리가 실행되었지만 변경된 행이 없을 경우 400 에러 응답
} else {
res.status(200).json(results) // 쿼리 실행으로 행이 변경된 경우 성공적으로 응답
}
}
)
})
// 채널 개별 삭제
.delete(
[
param('id').notEmpty().withMessage('채널id 필요'), // param은 url에서 꺼내쓰는 것이기 때문에 body가 아닌 pram으로 지정
validate
],
(req, res) => {
let {id} = req.params
id = parseInt(id)
let sql = 'DELETE FROM channels WHERE id = ?'
conn.query(sql, id, // email 값을 바인딩
function(err, results) {
if (err) {
console.log(err)
res.status(400).end()
}
if (results.affectedRows == 0) {
return res.status(400).end() // 쿼리가 실행되었지만 변경된 행이 없을 경우 400 에러 응답
} else {
res.status(200).json(results) // 쿼리 실행으로 행이 변경된 경우 성공적으로 응답
}
}
)
})
module.exports = router // 모듈화를 해주기 위해 router를 반환함
1️⃣ users.js에도 validate를 추가하자
const express = require("express");
// 모듈화를 위해 서버를 불러오는 건 app.js로 넘김
const router = express.Router(); // express에 Router로 사용할 수 있도록 만들어 줌
// app이라는 서버에 직접 연결 해주던 것을 app,js에 넘기게 되어
// app이 사용된 모든 곳을 router로 변경 해줌
const conn = require("../mariadb"); // Promise 기반의 mariadb 연결 객체 가져옴
const { body, param, validationResult } = require("express-validator"); // body 메소드를 불러와 벨리데이터 모듈을 넣어준다, 추가적으로 오류를 받아주는 리절트 변수도 넣어준다
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()); // return을 써주면 함수가 곧바로 종료됨
}
}
// 로그인
router.post(
"/login", // 유효성 검사는 URL 뒤에 적어준다. 즉, 콜백함수 바로 앞에
[
body("email").notEmpty().isEmail().withMessage("이메일 입력 필요"), // 바디 안의 이메일에 대한 조건을 걸어줌
body("password").notEmpty().isString().withMessage("비밀번호 입력 필요"),
validate
],
(req, res) => {
// 먼저 email이 디비에 저장된 회원인지 확인해야
const { email, password } = req.body;
let sql = "SELECT * FROM users WHERE email = ?"; // sql을 변수로 지정하여 코드 오류도 줄이고 깔끔해짐
conn.query(sql, email, // email 값을 바인딩
function (err, results) { // 매개변수는 순서를 맞춰야 하므로 err는 남겨줌
if (err) {
console.log(err)
res.status(400).end()
}
let loginUser = results[0]; // 로그인 유저 객체에 results의 0번째 객체를 담아준다. 이전보다 코드가 쉬워짐
if (loginUser && loginUser.password == password) {
// results에 값이 있다면을 로그인유저가 있고 패스워드가 같으면으로 변경해줌
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("contect").notEmpty().isString().withMessage("연락처 입력 필요"),
validate
],
(req, res) => {
let sql = `INSERT INTO users (email, name, password, contect) VALUES (?, ?, ?, ?)`;
const { email, name, password, contect } = req.body;
conn.query(sql, [email, name, password, contect],
function (err, results) {
if (err) {
console.log(err)
res.status(400).end()
}
res.status(201).json(results); // 쿼리 결과를 클라이언트에 응답
}
)
})
router
.route("/users")
// 회원 개별 조회
.get(
[
body("email").notEmpty().isEmail().withMessage("이메일 입력 필요"),
validate
],
(req, res) => {
let { email } = req.body; // {}는 비구조화, id 값을 따로 빼서 사용
let sql = "SELECT * FROM users WHERE email = ?";
conn.query(sql, email, // email 값을 바인딩
function (err, results) {
if (err) {
console.log(err)
res.status(400).end()
}
res.status(200).json(results); // 쿼리 결과를 클라이언트에 응답
}
);
})
// 회원 개별 탈퇴
.delete(
[
body("email").notEmpty().isEmail().withMessage("이메일 입력 필요"),
validate
],
(req, res) => {
let { email } = req.body; // {}는 비구조화, id 값을 따로 빼서 사용
let sql = "DELETE FROM users WHERE email = ?";
conn.query(sql, email, // email 값을 바인딩
function (err, results) {
if (err) {
console.log(err)
res.status(400).end()
}
if (results.affectedRows == 0) {
return res.status(400).end() // 쿼리가 실행되었지만 변경된 행이 없을 경우 400 에러 응답
} else {
res.status(200).json(results) // 쿼리 실행으로 행이 변경된 경우 성공적으로 응답
}
}
)
})
module.exports = router;
2️⃣ channels.js
const express = require('express')
// 모듈화를 위해 서버를 불러오는 건 app.js로 넘김
const router = express.Router() // express에 Router로 사용할 수 있도록 만들어 줌
// app이라는 서버에 직접 연결 해주던 것을 app,js에 넘기게 되어
// app이 사용된 모든 곳을 router로 변경 해줌
const conn = require('../mariadb'); // Promise 기반의 mariadb 연결 객체 가져옴
const {body, param, validationResult} = require('express-validator') // body 메소드를 불러와 벨리데이터 모듈을 넣어준다, 추가적으로 오류를 받아주는 리절트 변수도 넣어준다
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()) // return을 써주면 함수가 곧바로 종료됨
}
}
router
.route('/') // route로 URL 묶어주기
// 채널 개별 생성
.post(
[
body('userId').notEmpty().isInt().withMessage('숫자입력 필요'), // 바디 메소드에 검사할 데이터를 넣고 조건들을 부여함
body('name').notEmpty().isString().withMessage('문자입력 필요'),
validate
],
(req, res) => {
const {name, userId} = req.body
let sql = `INSERT INTO channels (name, user_id) VALUES (?, ?)`
let values = [name, userId]
conn.query(sql, values,
function(err, results) {
if (err)
{ console.log(err)
return res.status(400).end()
} // 에러가 걸리면 끝내도록 설정
res.status(200).json(results) // 쿼리 결과를 클라이언트에 응답
}
)
})
// 채널 전체 조회
.get(
[
body('userId').notEmpty().isInt().withMessage('숫자 입력 필요'), // 유효성 검사는 사용자의 요청에 대해서 구분이 가능
validate // 여기에 유효성 검사 대상 뿐 아니라 모듈도 넣을 수 있음, 콜백함수 전에 유효성 검사와 유효성 검사에 대한 에러가 난다면 모듈 작동이 되는 것
],
(req, res) => {
let {userId} = req.body
let sql = 'SELECT * FROM channels WHERE user_id = ?'
conn.query(sql, userId,
function(err, results) {
if (err) {
console.log(err)
res.status(400).end()
}
if (results.length) {
res.status(200).json(results) // 쿼리 결과를 클라이언트에 응답
} else {
res.status(400).end()
}
}
)
})
router
.route('/:id')
// 채널 개별 조회
.get(
[
param('id').notEmpty().withMessage('채널id 필요'), // param은 url에서 꺼내쓰는 것이기 때문에 body가 아닌 pram으로 지정
validate
],
(req, res) => {
const err = validationResult(req) // 만약 에러가 있다면
if (!err.isEmpty()){ // 에러를 반환하도록
return res.status(400).json(err.array()) // return을 써주면 함수가 곧바로 종료됨
}
let {id} = req.params // req.params 객체에서 URL 경로에 포함된 id 값을 추출하여 id 변수에 저장
id = parseInt(id) // URL에서 문자열로 추출된 id를 정수로 변환
let sql = 'SELECT * FROM channels WHERE id = ?'
conn.query(sql, id, // email 값을 바인딩
function(err, results) {
if (err) {
console.log(err)
res.status(400).end()
}
if (results.length) {
res.status(200).json(results); // 쿼리 결과를 클라이언트에 응답
} else {
res.status(400).end()
}
}
)
})
// 채널 개별 수정
.put(
[
param('id').notEmpty().withMessage('채널id 필요'), // param은 url에서 꺼내쓰는 것이기 때문에 body가 아닌 pram으로 지정
body('name').notEmpty().isString().withMessage('채널명 오류'),
validate
],
(req, res) => {
let {id} = req.params
id = parseInt(id)
let {name} = req.body
let sql = 'UPDATE channels SET name = ? WHERE id = ?'
let values = [name, id]
conn.query(sql, values,
function(err, results) {
if (err) {
console.log(err)
res.status(400).end()
}
if (results.affectedRows == 0) {
return res.status(400).end() // 쿼리가 실행되었지만 변경된 행이 없을 경우 400 에러 응답
} else {
res.status(200).json(results) // 쿼리 실행으로 행이 변경된 경우 성공적으로 응답
}
}
)
})
// 채널 개별 삭제
.delete(
[
param('id').notEmpty().withMessage('채널id 필요'), // param은 url에서 꺼내쓰는 것이기 때문에 body가 아닌 pram으로 지정
validate
],
(req, res) => {
let {id} = req.params
id = parseInt(id)
let sql = 'DELETE FROM channels WHERE id = ?'
conn.query(sql, id, // email 값을 바인딩
function(err, results) {
if (err) {
console.log(err)
res.status(400).end()
}
if (results.affectedRows == 0) {
return res.status(400).end() // 쿼리가 실행되었지만 변경된 행이 없을 경우 400 에러 응답
} else {
res.status(200).json(results) // 쿼리 실행으로 행이 변경된 경우 성공적으로 응답
}
}
)
})
module.exports = router // 모듈화를 해주기 위해 router를 반환함
3️⃣ app.js
// 서버역할을 담당함
const express = require('express')
const app = express()
app.listen(7777)
const userRouter = require('./routes/users')// users 소환하기
app.use("/", userRouter) // app.use를 사용하여 매개변수에 내가 만든 모듈을 추가해줌
const channelRouter = require('./routes/channels')// channels 소환하기
app.use("/channels", channelRouter) //
4️⃣ mysql 모듈
const mysql = require('mysql2');
// MySQL에 비동기적으로 연결
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '1234',
database: 'Youtube', // 데이터베이스 이름을 'Youtube'로 설정
dateStrings: true, // 날짜를 문자열로 반환
});
// 연결을 Promise로 내보냄
module.exports = connection;
1️⃣ "로그인(인증) 세션이 만료되었습니다"는 무엇일까?
2️⃣ 인증과 인가
인증(Authentication)은 로그인과 같은 말
인가(Authorization)은 접근에 대한 권한
1️⃣ 세션은 무엇인가?
2️⃣ 쿠키

장점
단점
3️⃣ 세션
장점
단점
4️⃣ jwt
1️⃣ jwt란?
2️⃣ 정리하자면
1️⃣ JWT의 구조를 공식 홈페이지에서 알아보자

1️⃣ jwt의 인증-인가 절차를 알아보자

1️⃣ jwt를 직접 발행해보자
var jwt = require('jsonwebtoken'); // jwt 모듈을 불러옴
var token = jwt.sign({ foo: 'bar' }, 'shhhhh'); // 서명을 가져와서 생성함 = 토큰을 생성함
// 서명메소드의 매개변수에 들어가는 것은 나만의 암호키, (페이로드 + 나만의 암호키) + SHA256(알고리즘)
console.log(token)


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

1️⃣ 코드를 깃허브에 올린다면...? 암호화키가 노출된다
var jwt = require('jsonwebtoken'); // jwt 모듈을 불러옴
var privateKey = 'shhhhhh'
// 서명 = 토큰 발행
var token = jwt.sign({ foo: 'bar' }, privateKey); // 서명을 가져와서 생성함 = 토큰을 생성함
// 서명메소드의 매개변수에 들어가는 것은 나만의 암호키, (페이로드 + 나만의 암호키) + SHA256(알고리즘)
console.log(token)
// 검증
// 만약 검증에 성공하면 페이로드 값을 확인할 수 있음
var decoded = jwt.verify(token, privateKey);
console.log(decoded);
2️⃣ .env(environment : 환경 변수 '설정 값')를 이용해보자
개념 : 개발을 하다가 포트넘버, 데이터베이스 계정, 암호키 등 외부에 유출되면 안되는 중요한 환경 변수들을 따로 관리하기 위한 파일
파일 확장자가 '.env' 인 것
npm으로 설치할 수 있다
3️⃣ .env를 만들어보자
# 모두 대문자로 적는다
PRIVATE_KEY='shhhhhh' # jwt 암호키
var jwt = require('jsonwebtoken'); // jwt 모듈을 불러옴
var dotenv = require('dotenv'); // env 모듈을 불러옴
dotenv.config(); // dotenv 사용설정
// 서명 = 토큰 발행
var token = jwt.sign({ foo: 'bar' }, process.env.PRIVATE_KEY); // env에 저장된 변수를 사용
console.log(token)
var decoded = jwt.verify(token, process.env.PRIVATE_KEY);
console.log(decoded);

1️⃣ 만들어둔 미니 프로젝트에도 jwt를 적용해보자
// 로그인
router.post(
"/login",
[
body("email").notEmpty().isEmail().withMessage("이메일 입력 필요"),
body("password").notEmpty().isString().withMessage("비밀번호 입력 필요"),
validate
],
(req, res) => {
const { email, password } = req.body;
let sql = "SELECT * FROM users WHERE email = ?";
conn.query(sql, email,
function (err, results) {
if (err) {
console.log(err)
res.status(400).end()
}
let loginUser = results[0];
if (loginUser && loginUser.password == password) {
// 토큰 발급
const token = jwt.sign({
email : loginUser.email,
name : loginUser.name
}, process.env.PRIVATE_KEY);
res.status(200).json({
message: `${loginUser.name}님 로그인 되었습니다.`,
token : token // 토큰도 같이 출력
})
} else {
res.status(404).json({
message: `이메일 또는 비밀번호가 틀렸습니다.`,
})
}
}
)
}
)

1️⃣ 쿠키에 jwt를 담아보자
먼저 원활한 쿠키 사용을 위해 npm에서 쿠키 파셔를 설치해준다.
쿠키로 토큰값을 돌려주는 코드를 추가하면 결과가 잘 확인된다.
// 로그인
router.post(
"/login",
[
body("email").notEmpty().isEmail().withMessage("이메일 입력 필요"),
body("password").notEmpty().isString().withMessage("비밀번호 입력 필요"),
validate
],
(req, res) => {
const { email, password } = req.body;
let sql = "SELECT * FROM users WHERE email = ?";
conn.query(sql, email,
function (err, results) {
if (err) {
console.log(err)
res.status(400).end()
}
let loginUser = results[0];
if (loginUser && loginUser.password == password) {
// 토큰 발급
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(403).json({ // 클라이언트 거절이기 때문에 403으로 변경
message: `이메일 또는 비밀번호가 틀렸습니다.`,
})
}
}
)
}
)

2️⃣ status 인증 거절 코드
1️⃣ cookie의 구성별 의미를 알아보자

2️⃣ 쿠키 설정값을 변경하면?
res.cookie("token", token, { // 쿠키에 토큰이라는 상자를 만들어 토큰을 담아준다
httpOnly : true // 쿠키 설정값을 변경
})

1️⃣ jwt 고도화를 위해 유효기간을 설정해보자
// 토큰 발급
const token = jwt.sign({
email : loginUser.email,
name : loginUser.name
}, process.env.PRIVATE_KEY, {
expiresIn : '30m', // 토큰의 유효기간 부여
issuer : "kim" // 토큰의 발행자 부여
})
