[프로그래머스] 백엔드심화(6)

Lina Hongbi Ko·2024년 9월 25일
1

Programmers_BootCamp

목록 보기
25/76
post-thumbnail

2024년 9월 25일

✏️ next()

: 지난 시간에 이어서, validate를 유효성 검사 다음에 넣었을 때, 계속 sending request 되는 것을 확인했다.

  • 이 문제는 validate 코드 탓 이었다..
  • 일단 모듈화 하지 않고, 그냥 함수 호출을 하면 잘 되는지 확인해보자
// 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.js validate 정리

  • 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());
}
  • 모든 API validate 추가하고 필요 없는 부분 삭제하기

✏️ users.js validate 추가

  • validate의 매개변수 req, res, next는 express가 넣어주는 것임 (api요청할 때 req, res를 express가 넣어주는 것과 같은 원리)

✏️ 로그인 세션

: 우리는 '로그인 세션이 만료되었습니다' 라는 안내를 웹상에서 종종 볼 수 있다.
이 말은 무슨 말일까..?

  • 인증(=로그인) : Authentication -> 가입된 유저 확인할 때
    • 쇼핑몰 상품 볼 때? X → 공개적인 데이터를 볼때는 로그인 안해도 됨
    • 쇼핑몰 장바구니 담을 때 O
    • 쇼핑몰 상품 구매 O
    • 마이페이지 O
      ....
    • 보안이 중요함
  • 인가(=허가) : Authorization
    • 같은 사이트내에 관리자 / 고객 에 따라 접근할 수 있는 페이지 다름
  • 관리자든 고객이든 인증을 통해서 사이트에 가입된 사용자라는 걸 증명 → 인증
  • 인증 후, 페이지 접근 권한이 있는지 허가 → 인가

✏️ 쿠키 vs 세션

: 그럼 인증(로그인)을 매번 해야 한다면? (상품 구매할 때마다 로그인 하거나 장바구니에 상품 담을 때마다 로그인 하면 너무 힘듦)
=> 따라서 '세션'을 사용함

  • 세션 : (로그인이 되어 있는) 상태

  • 쿠키 : 웹에서 서버와 클라이언트가 주고받는 데이터 중 하나

    • 쿠키 생성 : 로그인 → 서버가 쿠키를 구워줌(로그인한 내용을 쿠키 안에 넣고, 서버에게 다른 기능(장바구니, 물건구매, 마이페이지) 요청할 때, 이후에 로그인하는 시점이 필요하면 그 때 쿠키 보여주면 로그인한 적 있었다는거 인지하고 있겠다고 생각)
    • 생성은 웹 서버가 해서 웹 브라우저 주면, 브라우저가 자기 메모리에 저장해두고, 다음에 같은 웹서버 방문할 때 쿠키 들고 요청하러 감
    • 사용자는 쿠키 사용 ↔ 서버는 쿠키를 확인 핑퐁
    • 장점 : 서버가 무언가를 저장하지 않음(누가 로그인 했는지 기억하지 않음 / 사용자가 매번 가지고 오는 쿠키를 보기만 함)(쿠키만 구워줌) → 서버의 저장 공간 아낄 수 있음 & Stateless(상태를 저장하지 않음) ⇒ RESTfull
    • 단점 : 매번 쿠키를 사용자가 서버가 핑퐁하니까 누군가 훔쳐갈 수 있음 → 보안상 취약
  • 세션 : 쿠키의 단점을 보완해서 나온 해결 방안

    • 로그인 → 서버가 금고를 만들어서, 정보 저장 → 금고 번호를 줌
    • 사용자 ↔ 서버가 번호만 가지고 대화
    • 쿠키에 중요한 정보를 담지 말고, 중요한 정보는 서버에 저장해두고 그 정보가 어딨는지 주소만 적어서 쿠키에 담음
    • 쿠키에 넣어서 보내기엔 너무 중요한 내용은 서버가 가진 금고(Session)에 넣어 두고, 그 금고 번호(Session ID)만 쿠키에 넣어서 통신
    • 사용자는 session id 사용 ↔ 서버는 쿠키에 session id만 확인 핑퐁
    • 장점 : 보안이 비교적 좋음
    • 단점 : 서버가 저장 → 서버 저장 공간을 씀, Stateless X (상태를 저장함)
  • 보안에 취약한 쿠키의 단점을 보완하고, 서버 저장 공간을 쓰는 세션의 단점을 보완하기 위해서 나온 친구 : JWT (하지만 여전히 완벽하진 않음)

✏️ JWT

  • Json Web Token

  • Json 형태의 데이터를 Web에서 보안을 꽁꽁 싸매서 안전하게 전하는 Token
    (= JSON 형태의 데이터를 안전하게 전송하기 위한 (웹에서 사용하는) 토큰)
    cf) 토큰 : 입장 가능한 유저를 보여주기 위한 <인증용> / 관리자 권한 & 일반 유저 권한 <인가용>

  • 토큰의 유무를 통해 인증을 할 수 있음

    • 토큰의 종류에 따라 인증의 방식도 다름(권한) ex)관리자, 유저
  • 결국, 토큰을 가진 사용자가 증명을 하기 위한 수단

  • 장점

    • 보안에 강함 = 암호화 되어 있음
    • 서버가 상태를 저장하지 않음 = Stateless = HTTP 특징을 잘 따름
    • 서버의 부담 줄여 줄 수 있고, 토큰을 발행 하는 서버를 따로 만들어 줄수도 있음

💡 JWT 구조

: jwt.io 홈페이지를 가면 자세한 설명 볼 수 있음(특징, 구조)
https://jwt.io/

  • Debugger -> JWT를 풀어주는 친구
    • JWT는 암호화 되었기 때문에 debugger로 해석할 수 있음
    • encoded된 것을 header:algorithm & token type, payload:data, verify signature 으로 decode 할 수 있음 (크게 3개의 구조)
      • header : 알고리즘(암호화하는 대표적인 알고리즘들(ex길찾기 알고리즘))과 타입
      • payload : 정보들 (나는 누구, 언제 로그인했고, 언제까지 로그인 되어있을 수 있고,, 등등이 담김)
      • verify signature : header와 padyload를 읽고, 총체적으로 보여줌. 데이터들을 보증함
  • 모든 사람들이 같은 알고리즘을 알고 있어서 알고리즘을 풀어서 해킹하려고 하지만 그럴 수 없다
    • 서버가 데이터만 바뀐 것을 보는것이 아니라 verify signature까지 보고 판단
    • 데이터가 바뀌면 서명값도 바뀌기 때문에 서버가 자기가 서명해준 서명값이 아니라서 받아주지 않는다
    • 따라서, 아무리 같은 알고리즘과 나쁜 데이터를 끼워도, 서명값이 바뀌기 때문에 우리는 안심하고 쓸 수 있다.
  • 자세히 보자.
    • header : 토큰을 암호화하는데 사용한 알고리즘, 토큰의 형태(jwt)
    • payload : 사용자 정보(이름, 주소, 핸드폰, …. 비밀번호X)
    • verify signature(서명) : 만약, 페이로드 값이 바뀌면 이 서명값이 통째로 바뀌기 때문에 우리는 JWT를 믿고 쓸 수 있음

💡 JWT 인증, 인가

: 그럼 우리는 JWT로 어떻게 인증, 인가 할 수 있을까?
-> 로그인 하면, 아래 처럼 인증/인가 절차를 가짐

간단히 설명하면, 클라이언트는 POST API로 body에 username, password를 서버에게 요청한다. 서버는 내부 로직을 확인하고 JWT를 발행해준다. 이때, 로그인한 시점이 JWT 발행 시점이 된다. 그리고 서버는 다시 클라이언트에게 JWT를 동봉해서 주면서 다음에 어떤 요청을 할때 이 JWT를 들고 오면 서명이 맞는지만 확인해서 들여보내준다고 해준다. (인증)

그리고 클라이언트가 다른 요청(장바구니 담기, 결제 등등)을 하면 다시 로그인 하지 않고, JWT만 보여주면 서버는 JWT의 서명을 확인하고 요청들을 유효시간 내에서 허락해준다(인가)

💡 JWT 구현

  • JWT 직접 발행 해보기
  • 설치 : npm i jsonwebtoken
// 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) : 토큰이 발생된 시간(초형태)_발행 시간에 따라서 토큰의 형태 달라짐! → 똑같은 내용의 토큰을 발행해도 헤더값은 같아도 페이로드, 서명값은 다름!

✏️ .env

: 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 : environment / 환경 변수 ‘설정 값’
    • .env 파일에 설정값을 넣어 두면 깔끔하고 보안에 좋음
    • 개념 : 개발을 하다가 포트넘버, 데이터베이스 계정, 암호키, … 등등 외부에 유출되면 안되는 중요한 환경 변수들을 따로 관리하기 위한 파일
      cf) 깃허브에 올라가면 안되는 값
    • 파일 확장자 : .env
    • dotenv 모듈을 사용해서 관리하면 편리함
      • 설치 : npm i dotenv
    • .env 파일은 환경 변수 파일 → 프로젝트 최상위 패키지에 존재해야함
      • 주석을 달땐 # 사용
// .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에 올려서 무시하는 방법으로 파일을 별도로 뺄 수 있음!!

✏️ youtube에 jwt 적용

: 클라이언트가 로그인 요청하면 서버는 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에 알맞게 들어오는지 확인할 것임

✏️ cookie

: 쿠키에 JWT를 담아서 보내려고 함!

: 쿠키를 사용할 수 있는 모듈 사용하기

  • npm i cookie
  • 설치를 안하고 사용해도 되지만, request에서 쿠키의 내용을 끊어서 확인 해야 하므로 parser을 할 때 필요하기 때문에 나중에 설치 해야함
  • 우리는 현재 response에 쿠키를 사용할 예정이므로 일단 express의 cookie()를 사용하자
    => 쿠키 안에서 또 다른 상자를 마련해서 담아준다 ⇒ token이라는 상자자 “token”을 마련해서 여기에 token을 담아줌
// 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코드 사용

  • 403 코드 : 클라이언트가 접근 거절(forbidden from accessing) 당함 (인증(authorize)할 수 없다)
// users.js

... 생략 ...
      } else {
        res.status(403).json({
          message: `이메일 또는 비밀번호가 틀렸습니다`,
        });
      }
    });
  }
);

쿠키 안을 보면,

을 확인할 수 있다.

  • Secure : https 환경에서 보낼 건지, http 환경에서 보낼건지
  • HttpOnly(= 프론트엔드가 아니라 API 호출만 허락?) : fale이면 XSS 공격 (프론트엔드 공격 : 웹브라우저 js 접근 공격)받을 수 있음 → true로 바꾸자
// 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(토큰)를 쿠키에 동봉해서 클라이언트에게 전달함 -> 클라이언트는 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 유효성 검사를 쉽게 라이브러리를 이용해서 쓰는 것을 보고, 앞으로 어떤 프로젝트를 할 때 내가 유용하게 쓸 수 있는 라이브러리를 먼저 검색해보고 잘 이용해 볼수 있는 방법은 없을까 하는 생각도 가져야겠다. 이상 끝,!

profile
프론트엔드개발자가 되고 싶어서 열심히 땅굴 파는 자

0개의 댓글