그저 내가 되었고
항해99) 4주차:: 주특기 숙련; 📚미들웨어 본문
01. 미들웨어
- 역할: 웹 서버의 요청/응답에 대해 공통적으로 관리(e.g. 모든 요청에 대해서 로그를 남겨 확인, 승인된 사용자만 API를 접근 등)
- 쓰임:
- 미들웨어는 내가 만드려는 기능에 다양한 방식으로 사용 가능
- 관리 측면에서도 많은 이점이 있음
- 이미 다양한 미들웨어가 존재함
- 기본 미들웨어: https://expressjs.com/ko/4x/api.html
- 예시:
express.js의👇🏻
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
- urlencoded: form-urlencoded 라는 규격의 body 데이터를 손쉽게 코드에서 사용할 수 있게 도와주는 미들웨어
- json: JSON 이라는 규격의 body 데이터를 손쉽게 코드에서 사용할 수 있게 도와주는 미들웨어
Express.js의 미들웨어가 실행되는 경우👇🏻
- app.use(’/api’, Middleware) : api로 시작하는 요청에서 미들웨어를 실행
- app.post(’/api’, Middleware) : api로 시작하는 POST 요청에서 미들웨어를 실행
- app.use(Middleware) : 모든 요청에서 미들웨어가 실행됨
- 작성법:
app.use((req, res, next) => {
// 필요한 코드
});
** req: 요청에 대한 정보가 담겨있는 객체
- HTTP Headers, Query Parameters, URL 등 브라우저가 서버로 보내는 정보들이 담겨있음
** res: 응답을 위한 기능이 제공됩니다.
- 어떤 HTTP Status Code로 응답 할지, 어떤 데이터 형식으로 응답 할지, 헤더는 어떤 값을 넣어 응답 할지 다양한 기능을 제공
** next: 다음 스택으로 정의된 미들웨어를 호
여러개의 미들웨어가 겹치는 경우, 이는 첫번째 미들웨어부터 순차적으로 진입. 중간에 미들웨어에 next()가 실행되지 않으면 다음 미들웨어는 실행되지 않으니 꼭 next 붙이긔
- router vs middleware
: Router와 미들웨어는 서로 다른 방식처럼 보이지만
: Router는 미들웨어 기반으로 구현된 객체이므로
: 결국 둘은 동일한 방식으로 작동. 라우터 역시 일종의 미들웨어라고 보면 됨.
02. 로그인 기능 구현
0) 프로젝트 폴더 열고 기본적으로 필요한 모듈 설치
npm init
npm i express mongoose jsonwebtoken -S
1) 기능 구현 전에 뭐가 필요할지 스케치
- JWT 토큰 만들기 위한 라이브러리 하나가 필요
- 회원권 구매와, 놀이공원 입장, 내 정보 조회를 위한 기능 필요
- 회원가입 API
- 로그인 API
- 내 정보 조회 API
- 로그인 확인에 해당하는 기능은 미들웨어로 구현해 여러 라우터에서 공용으로 사용하도록 함
2) User 모델 구현
- mongoose로 데이터를 저장하려면 우선 모델부터 구현해야 함.
app.js
// app.js
const express = require("express");
const mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/shopping-demo", {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error:"));
const app = express();
const router = express.Router();
app.use("/api", express.urlencoded({ extended: false }), router);
app.use(express.static("assets"));
app.listen(8080, () => {
console.log("서버가 요청을 받을 준비가 됐어요");
});
models/user.js
// models/user.js
const mongoose = require("mongoose");
const UserSchema = new mongoose.Schema({
email: String,
nickname: String,
password: String,
});
UserSchema.virtual("userId").get(function () {
return this._id.toHexString();
});
UserSchema.set("toJSON", {
virtuals: true,
});
module.exports = mongoose.model("User", UserSchema);
03. 회원가입 API 구현
// app.js
const User = require("./models/user");
// 회원가입 API
router.post("/users", async (req, res) => {
const { email, nickname, password, confirmPassword } = req.body;
if (password !== confirmPassword) {
res.status(400).send({
errorMessage: "패스워드가 패스워드 확인란과 다릅니다.",
});
return;
}
// email or nickname이 동일한게 이미 있는지 확인하기 위해 가져온다.
const existsUsers = await User.findOne({
$or: [{ email }, { nickname }],
});
if (existsUsers) {
// NOTE: 보안을 위해 인증 메세지는 자세히 설명하지 않는것을 원칙으로 한다: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#authentication-responses
res.status(400).send({
errorMessage: "이메일 또는 닉네임이 이미 사용중입니다.",
});
return;
}
const user = new User({ email, nickname, password });
await user.save();
res.status(201).send({});
});
04. 로그인 API 구현
- how to?
+ 이메일, 패스워드 값을 입력받아 데이터베이스에 있는 정보 중 이메일 및 패스워드가 일치하면 로그인이 성공했다고 판단
+ 로그인에 성공하면 JWT 토큰 구성 예시👇🏻와 같은 방법으로 데이터를 반환하면 됨
const token = jwt.sign({ userId: user.userId }, "customized-secret-key");
+ 이런👇🏻식으로 응답값 받아서 프론트에서 사용하도록.. 예시
res.send({
token: "JWT로 만들어진 토큰을 반환하게 해보세요!"
});
- 로그인 API 구현 예시
// app.js
const jwt = require("jsonwebtoken");
router.post("/auth", async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
// NOTE: 인증 메세지는 자세히 설명하지 않는것을 원칙으로 한다: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#authentication-responses
if (!user || password !== user.password) {
res.status(400).send({
errorMessage: "이메일 또는 패스워드가 틀렸습니다.",
});
return;
}
res.send({
token: jwt.sign({ userId: user.nickname }, "시크릿키"),
});
});
여기서.. 👇🏻얘의 의미는? 토큰 만들고, token이라는 key로 반환하는 것.
res.send({
token: jwt.sign({ userId: user.nickname }, "시크릿키"),
});
const jwt로 jsonwebtoken 모듈 불러온 후,
token이라는 변수에다가 jwt.sign(사인 해야 토큰 만들 수 있는 것)에
userId라는 키라는 가진 곳에 user의 nickname을 담아서 보냄.
05. 사용자 인증 미들웨어 구현(auth-middleware)
1) how to?
- 헤더에서 Authorization: Bearer JWT토큰내용 이렇게, 그러니까 Authorization이라는 키로 Bearer JWT 토큰내용이란 밸류를 보내고 있음. 그러니까 이걸
const {authorization} = req.headers;
이렇게 받아서(headers 안에 header가 포함됨)(여기서 console.log(authorization) 찍으면 "Bearer 토큰내용" 이렇게 잘 찍혀 나옴)
- 여기서 토큰만 받아오려면?!(공백을 기준으로 잘라서 배열로 변환. 즉, ["Bearer", "토큰내용"] 이렇게 자른 후, authType과 authToken으로 distructuring해서 사용해주면 됨~!)
const [authType, authToken] = (authorization || "").split(" ");
- 일단, tokenType이 Bearer일 때만 진행할 것. 아니면 아예 return으로 탈출해서 밑부분 코드로 진행 안되게끔 막아주는 것 까지~!
if (!authToken || authType !== "Bearer") {
res.status(401).send({
errorMessage: "로그인이 필요합니다.",
});
return;
}
- 이제 jwt 모듈 불러오기
const jwt = require("jsonwebtoken");
- jwt 불러왔으니까,, 사용자 검증하기
try {
const decoded = jwt.verify(authToken, "시크릿키"); //검증용. 헤더로 입력받은 tokenValue가 authToken
} catch (err) {
res.status(401).send({
errorMessage: "로그인이 필요한 기능입니다.",
});
}
👆🏻이거를 👇🏻이렇게. 달라진건 두번째줄 하나... decoded에 들어있는 value는 아까 로그인하면서 토큰에 담아 보내준(
{ userId: user.nickname }) user.nickname임.
try {
const { userId } = jwt.verify(authToken, "시크릿키");
} catch (err) {
res.status(401).send({
errorMessage: "로그인이 필요한 기능입니다.",
});
}
- 이렇게 검증하라고 로그인단에서 정보 받았으면, 일단 저 사용자가 있는지 체크해봐야됨.
const User = require("../models/user");
...
const user = await User.findOne({ nickname, password });
- 일치하는거 찾았으면 locals라고 유틸리티하게 사용할 수 있는 공간에 사용자 정보를 담아서 넘겨줄 것.
res.locals.user = user;
- 마지막에 next까지 꼭 빼주기
next();
- auth-middleware.js
const jwt = require("jsonwebtoken");
const User = require("../models/user");
module.exports = async (req, res, next) => {
const {authorization} = req.headers;
// console.log(authorization)
const [authType, authToken] = (authorization || "").split(" ");
//console.log(authToken)
if (!authToken || authType !== "Bearer") {
res.status(401).send({
errorMessage: "로그인이 필요합니다.",
});
return;
}
try {
const { userId } = jwt.verify(authToken, "시크릿키"); //{ userId } 까면 nickname이라는 사용자 정보 들었고, 걸로 인증은 끄으읏
const user = await User.findOne( { nickname: userId } ); //💛💛사용자 1인의 데이터 전체
res.locals.user = user; // 바로 위에서 찾은 사용자 1명을 res~변수에 다 보관
// User.findOne({ nickname: userId }).then((user) => {
// res.locals.user = user; //토큰의 userId로 해당 사용자가 맞는지 확인함. 이미 db에서 사용자 정보 가져온 것. 그러므로 이 미들웨어를 사용하는 라우터에서는 굳이 db에서 사용자 정보를 꺼내오지 않아도 되도록 express가 제공하는 안전한 변수(res.locals.user)에 담아두고 언제나 꺼내서 사용할 수 있게 작성함. 이렇게 담아둔 값은 정상적으로 응답 값을 보내고 나면 소멸하므로 해당 데이터가 어딘가에 남아있을 걱정의 여지를 남겨두지 않게 됨.
next();
// });
} catch (err) {
res.status(401).send({
errorMessage: "로그인이 필요한 기능입니다.",
});
}
};
06. 내 정보 조회 API 구현
1) how to?
- 우선.. 얘는 사용자가 토큰을 Authorization이라는 헤더에 담아서 보내야 동작하는 API여야 함. 로그인을 해야 내 정보를 조회할 수 있으니까.
- res.locals.user에 인증을 다 해놓고 user 객체를 거기다가 담아둠. 그러니 거기에 정보가 들어있단건 이미 인증이 다 끝났단 것. authMiddleware라는 미들웨어를 꼭 핸들러 앞에 붙여줘야 res.locals에 정보 담겨있을 것.
2) 예시
routes/posts.js(실제로 인증 사용하는 API)
router.get("/posts/like", authMiddleware, async (req, res) => {
const { nickname } = res.locals.user; //💛💛💛💛💛
👆🏻꼭!!! 핸들러 앞에 authMiddleware를 붙여줘야 거기를 통과하면서 인증이 될 것
👆🏻nickname이라는 변수에 res.locals.user라는 객체 안의 nickname이라는 키가 구조 분해 할당이 됨. 여기에 사용자 정보가 담겨 있음.
+왜 nickname? 내가 authMiddleware만들 때 사용자 정보 verify 후 그 사용자 데이터 전체를 nickname으로 찾아서 res.locals.user에 담아 보냈슴👇🏻 그걸 구조분해할당 한닷.
try {
const { userId } = jwt.verify(authToken, "시크릿키");
const user = await User.findOne( { nickname: userId } ); //💛💛사용자 1인의 데이터 전체
res.locals.user = user;
👆🏻토큰 속에다가 userId와 user.nickname을 묶어서 보냈기 때문쓰
+ 그래서 여기서 console.log(res.locals.user) 찍어보면
'개발 > 항해99 9기' 카테고리의 다른 글
항해99) 5주차:: 주특기 심화; 📚Socket.io (0) | 2022.10.15 |
---|---|
항해99) 4주차:: 주특기 숙련; 개인과제 전체코드(JS)(ver.Sequelize) (0) | 2022.10.13 |
항해99) 4주차:: 주특기 숙련; 📚MySQL → Sequelize (0) | 2022.10.12 |
항해99) 4주차:: 주특기 숙련; 개인과제 전체코드(JS)(ver.MongoDB) (0) | 2022.10.11 |
항해99) 4주차:: 주특기 숙련; 📚쿠키 & 세션 & JWT (0) | 2022.10.11 |