그저 내가 되었고

항해99) 4주차:: 주특기 숙련; 📚미들웨어 본문

개발/항해99 9기

항해99) 4주차:: 주특기 숙련; 📚미들웨어

hyuunii 2022. 10. 12. 08:15

01. 미들웨어

- 역할: 웹 서버의 요청/응답에 대해 공통적으로 관리(e.g. 모든 요청에 대해서 로그를 남겨 확인, 승인된 사용자만 API를 접근 등)

 

- 쓰임:

  • 미들웨어는 내가 만드려는 기능에 다양한 방식으로 사용 가능
  • 관리 측면에서도 많은 이점이 있음
  • 이미 다양한 미들웨어가 존재함

 

- 예시:

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) 찍어보면