JWT, bcrypt를 저번에 만들어 둔 X에 입혀보자.
git의 새로운 가지를 생성하여 일단 그곳에 저장해두었다.
다음은 커맨드 창에서 실행한 결과이다.
git branch
main
* session-auth // 원래는 이 가지였는데!
C:\Hsgo\X>git checkout main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
C:\Hsgo\X>git branch
* main // 저장소를 main으로 옮겼다!
session-auth
C:\Hsgo\X>git checkout -b jwt-auth
Switched to a new branch 'jwt-auth'
C:\Hsgo\X>git branch
* jwt-auth // 새로운 가지 생성! 새로운 저장소 생성!
main
session-auth
X에는 express-validator가 없어서 이것도 설치해주고...
npm i express-validator
민감한 정보를 보호하기 위해 .env 파일을 .gitignore에 추가하여 Git과 같은 버전 관리 시스템에 커밋되지 않도록 한다.
JWT_SECRET이나 데이터베이스 비밀번호와 같은 중요한 정보는 소스 코드에 하드코딩해서 저장하면 안 된다.
이 정보를 .env 파일에 저장함으로써 코드와 분리하고, 이를 외부에 노출하지 않도록 한다.
.envJWT_SECRET=abcdefg1234%^&*
JWT_EXPIRES_SEC=172800
BCRYPT_SALT_ROUNDS=10
HOST_PORT=9090
.gitignore# DB
.env
# node.js
node_modules/
package-lock.json
# MACOS
.DS_Store
설치
npm i dotenv
import dotenv from "dotenv";
dotenv.config();
function required(key, defaultValue = undefined) {
const value = process.env[key] || defaultValue;
if (value == null) {
throw new Error(`키 ${key}는 undefined!!`);
}
return value;
}
export const config = {
jwt: {
secretKey: required("JWT_SECRET"),
expiresInSec: parseInt(required("JWT_EXPIRES_SEC", 86400)), // 기본은 86400초(하루)
},
bcrypt: {
saltRounds: parseInt(required("BCRYPT_SALT_ROUNDS", 10)), // 기본은 10
},
host: {
port: parseInt(required("HOST_PORT", 9090)),
},
};
import express from "express";
import postsRouter from "./router/posts.mjs";
import authRouter from "./router/auth.mjs";
import { config } from "./config.mjs";
import cors from "cors"; // 추가
const app = express();
app.use(cors()); // 추가
app.use(express.json());
app.use("/posts", postsRouter);
app.use("/auth", authRouter);
app.use((req, res, next) => {
res.sendStatus(404);
});
app.listen(config.host.port);

파일 경로는 이렇게 되어 있다.
let users = [
{
id: "1",
userid: "apple",
password: "1111",
name: "김사과",
email: "apple@apple.com",
url: "https://randomuser.me/api/portraits/women/32.jpg",
},
{
id: "2",
userid: "banana",
password: "2222",
name: "반하나",
email: "banana@banana.com",
url: "https://randomuser.me/api/portraits/women/44.jpg",
},
{
id: "3",
userid: "orange",
password: "3333",
name: "오렌지",
email: "orange@orange.com",
url: "https://randomuser.me/api/portraits/men/11.jpg",
},
{
id: "4",
userid: "berry",
password: "4444",
name: "배애리",
email: "orange@orange.com",
url: "https://randomuser.me/api/portraits/women/52.jpg",
},
{
id: "5",
userid: "melon",
password: "5555",
name: "이메론",
email: "orange@orange.com",
url: "https://randomuser.me/api/portraits/men/29.jpg",
},
];
// 회원가입 사용자 추가
export async function createUser(userid, password, name, email) {
const user = {
id: Date.now().toString(),
userid,
password,
name,
email,
url: "https://randomuser.me/api/portraits/men/29.jpg",
};
users = [user, ...users];
return users;
}
// 로그인
export async function login(userid, password) {
const user = users.find(
(user) => user.userid === userid && user.password === password
);
return user;
}
export async function findByUserid(userid) {
return users.find((user) => user.userid === userid);
}
export async function findByid(id) {
return users.find((user) => user.id === id);
}
let posts = [
{
id: "1",
name: "김사과",
userid: "apple",
text: "Node.js 배우는 중인데 Express 진짜 편하다! :로켓:",
createdAt: Date.now().toString(),
url: "https://randomuser.me/api/portraits/women/32.jpg",
},
{
id: "2",
name: "반하나",
userid: "banana",
text: "오늘의 커피 :커피:️ + 코딩 = 최고의 조합!",
createdAt: Date.now().toString(),
url: "https://randomuser.me/api/portraits/women/44.jpg",
},
{
id: "3",
name: "오렌지",
userid: "orange",
text: "Elasticsearch 연동 완료! 실시간 검색 API 짜릿해 :돋보기:",
createdAt: Date.now().toString(),
url: "https://randomuser.me/api/portraits/men/11.jpg",
},
{
id: "4",
name: "배애리",
userid: "berry",
text: "JavaScript 비동기 너무 어렵다... Promises, async/await, 뭐가 뭔지 :울음:",
createdAt: Date.now().toString(),
url: "https://randomuser.me/api/portraits/women/52.jpg",
},
{
id: "5",
name: "이메론",
userid: "melon",
text: "새 프로젝트 시작! Express + MongoDB + EJS 조합 좋아요 :전구:",
createdAt: Date.now().toString(),
url: "https://randomuser.me/api/portraits/men/29.jpg",
},
];
// 모든 포스트를 리턴
export async function getAll() {
return posts;
}
// 사용자 아이디(userid)에 대한 포스트를 리턴
// 조건을 만족하는 모든 요소를 배열로 리턴
export async function getAllByUserid(userid) {
return posts.filter((post) => post.userid === userid);
}
// 글 번호(id)에 대한 포스트를 리턴
// 조건을 만족하는 첫 번째 요소 하나를 리턴
export async function getById(id) {
return posts.find((post) => post.id === id);
}
// 포스트 작성
export async function create(userid, name, text) {
const post = {
id: Date.now().toString(),
userid,
name,
text,
createdAt: Date.now().toString(),
};
posts = [post, ...posts];
return posts;
}
// 포스트 변경
export async function update(id, text) {
const post = posts.find((post) => post.id === id);
if (post) {
post.text = text;
}
return post;
}
// 포스트 삭제
export async function remove(id) {
posts = posts.filter((post) => post.id !== id);
}
import * as authRepository from "../data/auth.mjs";
import * as bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { config } from "../config.mjs";
const secretKey = config.jwt.secretKey;
const bcryptSaltRounds = config.bcrypt.saltRounds;
const jwtExpiresInDays = config.jwt.expiresInSec;
async function createJwtToken(id) {
return jwt.sign({ id }, secretKey, { expiresIn: jwtExpiresInDays });
}
// 회원가입을 진행하는 함수
export async function signup(req, res, next) {
const { userid, password, name, email } = req.body;
// 회원 중복 체크
const found = await authRepository.findByUserid(userid);
if (found) {
return res.status(409).json({ message: `${userid}이 이미 있습니다.` });
}
const hashed = bcrypt.hashSync(password, bcryptSaltRounds);
const users = await authRepository.createUser(userid, hashed, name, email);
const token = await createJwtToken(users.id);
console.log(token);
if (users) {
res.status(201).json({ token, userid });
}
}
// 로그인
export async function login(req, res, next) {
const { userid, password } = req.body;
const user = await authRepository.findByUserid(userid);
if (!user) {
res.status(401).json(`${userid} 아이디를 찾을 수 없음`);
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({ message: "아이디 또는 비밀번호 확인" });
}
const token = await createJwtToken(user.id);
res.status(200).json({ token, userid });
}
export async function verify(req, res, next) {
const id = req.id;
if (id) {
res.status(200).json(id);
} else {
res.status(401).json({ message: "사용자 인증 실패" });
}
}
export async function me(req, res, next) {
const user = await authRepository.findByid(req.id);
if (!user) {
return res.status(404).json({ message: "일치하는 사용자가 없음" });
}
res.status(200).json({ token: req.token, userid: user.userid });
}
import * as postRepository from "../data/post.mjs";
// 모든 포스트 / 해당 아이디에 대한 포스트를 가져오는 함수
export async function getPosts(req, res, next) {
const userid = req.query.userid;
const data = await (userid
? postRepository.getAllByUserid(userid)
: postRepository.getAll());
res.status(200).json(data);
}
// id를 받아 하나의 포스트를 가져오는 함수
export async function getPost(req, res, next) {
const id = req.params.id;
const post = await postRepository.getById(id);
if (post) {
res.status(200).json(post);
} else {
res.status(404).json({ message: `${id}의 포스트가 없습니다.` });
}
}
// 포스트를 생성하는 함수
export async function createPost(req, res, next) {
const { userid, name, text } = req.body;
const posts = await postRepository.create(userid, name, text);
res.status(201).json(posts);
}
// 포스트를 변경하는 함수
export async function updatePost(req, res, next) {
const id = req.params.id;
const text = req.body.text;
const post = await postRepository.update(id, text);
if (post) {
res.status(201).json(post);
} else {
res.status(404).json({ message: `${id}의 포스트가 없습니다.` });
}
}
// 포스트를 삭제하는 함수
export async function deletePost(req, res, next) {
const id = req.params.id;
await postRepository.remove(id);
res.sendStatus(204);
}
/*
Authorization
- 본인의 신원을 증명하는 과정
Authorization 헤더
- http 요청을 보낼 때 헤더(Headers)라는 곳에 "추가정보"를 담을 수 있음
- 인증정보를 담는 표준 위치가 Authorization 헤더임
Bearer
- Authorization에 실을 수 있는 방식(타입) 중 하나
- Bearer 토큰(token)을 가지고 있다는 것 자체로 인증함
Authorization: Bearer <토큰>
*/
import jwt from "jsonwebtoken";
import * as authRepository from "../data/auth.mjs";
import { config } from "../config.mjs";
const AUTH_ERROR = { message: "인증에러" };
export const isAuth = async (req, res, next) => {
const authHeader = req.get("Authorization");
console.log(authHeader);
if (!(authHeader && authHeader.startsWith("Bearer "))) {
console.log("헤더 에러");
return res.status(401).json(AUTH_ERROR);
}
// Bearer dafkjdkfj;lkdja;dkj;lkdj;k
const token = authHeader.split(" ")[1];
console.log(token);
jwt.verify(token, config.jwt.secretKey, async (error, decoded) => {
if (error) {
console.log("토큰 에러");
return res.status(401).json(AUTH_ERROR);
}
console.log(decoded.id);
const user = await authRepository.findByid(decoded.id);
if (!user) {
console.log("아이디 없음");
return res.status(401).json(AUTH_ERROR);
}
console.log("user.id: ", user.id);
console.log("user.userid: ", user.userid);
req.userid = user.userid;
next();
});
};
import { validationResult } from "express-validator";
export const validate = (req, res, next) => {
const errors = validationResult(req);
if (errors.isEmpty()) {
return next();
}
return res.status(400).json({ message: errors.array()[0].msg });
};
import express from "express";
import * as authController from "../controller/auth.mjs";
import { body } from "express-validator";
import { validate } from "../middleware/validator.mjs";
const router = express.Router();
const validateLogin = [
body("userid")
.trim()
.isLength({ min: 4 })
.withMessage("최소 4자 이상 입력")
.matches(/^[a-zA-Z0-9]*$/)
.withMessage("특수문자는 사용불가"),
body("password")
.trim()
.isLength({ min: 8 })
.withMessage("최소 8자 이상 입력"),
validate,
];
const validateSignup = [
...validateLogin,
body("name").trim().notEmpty().withMessage("name을 입력"),
body("email").trim().isEmail().withMessage("이메일 형식 확인"),
validate,
];
// 회원가입
// POST
// http://127.0.0.1:9090/auth/signup
router.post("/signup", validateSignup, authController.signup);
// 로그인
// POST
// http://127.0.0.1:9090/auth/login
router.post("/login", validateLogin, authController.login);
export default router;
import express from "express";
import * as postController from "../controller/post.mjs";
import { body } from "express-validator";
import { validate } from "../middleware/validator.mjs";
import { isAuth } from "../middleware/auth.mjs";
const router = express.Router();
const validatePost = [
body("text").trim().isLength({ min: 5 }).withMessage("최소 5자 이상 입력"),
validate,
];
// 모든 포스트 가져오기
// 해당 아이디에 대한 포스트 가져오기
// GET
// http://127.0.0.1:9090/posts/
// http://127.0.0.1:9090/posts?userid=apple
router.get("/", isAuth, postController.getPosts);
// 글번호에 대한 포스트 가져오기
// GET
// http://127.0.0.1:9090/posts/:id
router.get("/:id", isAuth, postController.getPost);
// 포스트 쓰기
// POST
// http://127.0.0.1:9090/posts
// json 형태로 입력 후 저장
router.post("/", validatePost, isAuth, postController.createPost);
// 포스트 수정하기
// PUT
// http://127.0.0.1:9090/posts/:id
// json 형태로 입력 후 저장
router.put("/:id", validatePost, isAuth, postController.updatePost);
// 포스트 삭제하기
// DELETE
// http://127.0.0.1:9090/posts/:id
router.delete("/:id", isAuth, postController.deletePost);
export default router;
Postman에서 결과를 확인해본다.




왼쪽 메뉴에서 Collections에 create new collection하여 폴더를 만들어두고, 저장해 둘 수 있다.





회원가입하고, 로그인 한 후
로그인 Token을 Token 칸에 입력한 후 저장한다.
그래야 포스트를 가져오고, 쓰고, 수정하고, 저장하고, 삭제할 수 있다.





깃허브에서 모든 가지 병합 후,
main으로 돌아오면, 다른 가지에서 작업했던 미들웨어(middleware) 폴더가 사라져있다.
풀(PULL)하면 병합 상태로 돌아온다.

