백엔드 심화 6-1 ~ 8-4(유효성 검사 적용, validate, next(), jwt, env)

Develop Kim·2024년 9월 25일

programmers

목록 보기
18/40

6 유효성 검사랑 친해지기

6-1 검사 미들웨어 분리

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)
      }
    }
  )  
})
  • 모듈이 잘 실행됨

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




6-2 next()

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(); // 다음 할 일(미들웨어, 함수 등)을 찾아가기 위한 코드
  }
}
  • 다음영상에서는 채널과 유저의 유효성 검사를 전부 추가하고 코드를 정리해보자



6-3 channels.js validate 정리

1️⃣ 채널에 validate 모듈을 모두 적용해주고 정리함

  • 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를 반환함



6-4 users.js validate 추가 + 전체 테스트

1️⃣ users.js에도 validate를 추가하자

  • validate와 sql 에러 발생시 종료되도록 추가함
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;



7 jwt(feat. 로그인 세션이 만료되었습니다.)

7-1 로그인 세션- 인증과 인가

1️⃣ "로그인(인증) 세션이 만료되었습니다"는 무엇일까?

  • 웹 사이트에서 해당 문구를 많이 볼 수 있다. 사용자를 위해 영원히 로그인되어있는 것을 방지하기 위해 로직을 짜두는 것임

2️⃣ 인증과 인가

  • 인증(Authentication)은 로그인과 같은 말

    • 쇼핑몰에서 제품을 조회할 때는 로그인이 필요 없음(공개정보)
    • 쇼핑몰에서 마이페이지에 접속하거나 구매할 때는 로그인이 필요함(비공개, 내 정보)
    • 인증은 "인증을 통해 사이트에 가입된 유저라는 것을 증명하는 것"
  • 인가(Authorization)은 접근에 대한 권한

    • 관리자는 상품 관리 페이지에 접근할 수 있으나 고객은 불가능함
    • 인가는 인증 후에 페이지별 접근 권한이 있는 지를 구분하는 것



7-2 쿠키 세션 차이

1️⃣ 세션은 무엇인가?

  • 상품 구매할 때마다, 페이지가 바뀔 때마다 로그인을 반복하면 유저는 화날 것이기에 로그인을 유지하는 것이 좋음
  • 세션은 로그인이 되어있는 상태를 말하는 것

2️⃣ 쿠키

  • 김송아 강사님의 쿠키를 참고해보면 쿠키의 역할은 안에 내용물을 담아 서버와 클라이언트가 서로 주고 받는 것
  1. 로그인하면 -> 서버가 쿠키를 구워줌
  2. 사용자 <-> 서버 간 쿠키를 핑퐁
  • 장점

    • 서버가 저장하지 않는 것이 장점
      • 서버 저장공간 절약
      • HTTP의 특징 중 하나로 서버가 상태를 저장하지 않는 것 이것을 stateless라고 하며 RESTful(HTTP 잘 지킴)이라고도 함
  • 단점

    • 보안에 취약함(누군가 쿠키를 중간에 가로챌 수 있음)

3️⃣ 세션

  • 쿠키의 단점을 보완하기 위해 나온 친구, 클라이언트와 서버가 정보를 핑퐁하지 않고 암호화된 것만 주고 받는 것
  1. 로그인하면 -> 서버가 금고를 만들어서, 정보 저장 후 그 금고 번호를 줌
  2. 사용자 <-> 서버가 금고 번호만 가지고 대화함
  • 장점

    • 보안이 더 강화됨
  • 단점

    • 서버가 저장을 한다는 것
      • 서버 저장공간 차지
      • Stateless하지 않음

4️⃣ jwt

  • 쿠키과 세션의 단점을 보완해서 나온 친구
  • 그치만, 완벽하지는 않아 사용자들이 많지는 않음



7-3 JWT 개념, 특징 12

1️⃣ jwt란?

  • JSON Web Token의 약자로 json데이터를 웹에서 토큰으로 안전하게 보낼 수 있는 것
    • 토큰은 인증과 권한을 대체함

2️⃣ 정리하자면

  • 개념
    • json형태의 데이터를 안전하게 전송하기 위한 (웹에서 사용하는)토큰
      = 토큰을 가진 사용자가 "증명"하기 위한 수단
    • cf. 토큰 : (인증용) 입장 가능 / (인가용) 권한 구분
  • 장점
    • 보안에 강하다 = "암호화"가 되어 있다
    • 서버가 상태를 저장하지 않는다 = stateless 하다!(HTTP 특징을 잘 따랐다)
    • 서버의 부담을 줄일 수 있다
    • cf. 토큰을 발행하는 서버를 따로 만들어 줄 수도 있다
  • 구조(feat. jwt.io)



7-4 JWT 구조(feat. jwt.io) 15

1️⃣ JWT의 구조를 공식 홈페이지에서 알아보자

  • 홈페이지에서 보면 토큰의 구조가 나와있는데 복잡한 내용으로 암호화되어있는 것을 알 수 있음
  • 왼쪽의 토큰을 풀면 오른쪽과 같은 내용이 됨(json형태)
  • 여기서 색깔별로 세가지 구조로 분리되어 있음
    • HEADER : 암호화할 때 사용된 알고리즘, 토큰 타입(jwt)
    • PAYLOAD : 사용자 정보(내 이름, 주소, 연락처 등...)
    • VERIFY SIGNATURE : 위 내용을 전부 나타낸다는 보증서(혹여나 해커가 중간에 페이로드 값을 바꿀 경우 서명값도 바뀌기 때문에 그 토큰은 쓰지 못하게 됨)




7-5 JWT 인증-인가 절차 12

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

  • 클라이언트와 서버 간 절차를 그림으로 보면
    • body값에 이름과 비밀번호를 주면 서버가 내부 로직을 확인하고 로그인을 성공시킴
    • 그리고 서버는 로그인을 당분간 유지하기 위해 jwt토큰을 발행하여 클라이언트에게 줌
    • 클라이언트는 서버에 다른 요청(페이지 이동 등)을 할 때 jwt를 들고다니게 됨
      • 이 때 클라이언트는 jwt를 body가 아닌 header에 들고 다님
    • 클라이언트가 서버에 다른 요청을 하면 서버가 토큰을 확인하고 ok를 보냄




7-6 JWT 구현해보기 17

1️⃣ jwt를 직접 발행해보자

  • npm에서 jwt 외부모듈을 설치해서 사용하자
  • 설치 후 코드를 작성하여 확인해보면 토큰이 바로 생성되는 것을 볼 수 있고
var jwt = require('jsonwebtoken'); // jwt 모듈을 불러옴
var token = jwt.sign({ foo: 'bar' }, 'shhhhh'); // 서명을 가져와서 생성함 = 토큰을 생성함
// 서명메소드의 매개변수에 들어가는 것은 나만의 암호키, (페이로드 + 나만의 암호키) + SHA256(알고리즘)

console.log(token)

  • 나온 결과를 확인해보니 잘 맞는 것을 확인할 수 있다. 여기서 나만의 암호키 shhhhhh를 넣으니 시그니처 인증도 함께 확인이 가능하다

  • 페이로드 값을 확인할 수 있는 코드도 작성해보면 잘 출력되는 것을 확인할 수 있다
// 검증
// 만약 검증에 성공하면 페이로드 값을 확인할 수 있음
var decoded = jwt.verify(token, 'shhhhh');

console.log(decoded);
  • 여기서 iat는 토큰 발행시간을 초 형태로 나타낸 것이다.
  • 그리고 두번째 발행된 것을 확인해보면 페이로드와 시그니처가 다른 것을 확인할 수 있다.




7-7 .env

1️⃣ 코드를 깃허브에 올린다면...? 암호화키가 노출된다

  • 코드에서 나만의 암호를 문자열로 두는 것보다 조치를 해두는 것이 중요하다
  • privateKey 변수로 넣어줘볼까...? 큰 의미는 없는 것 같다
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를 만들어보자

  • .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);

  • env를 이용하여 포트넘버, 데이터베이스 내용 등 중요한 변수는 암호화할 수 있다.



8-1 youtube에 jwt 적용해보기

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 인증 거절 코드

  • 로그인을 실패하면 지금까지 404를 썼지만, 403이 더 잘 맞는 코드임
  • 403: 클라이언트가 거절당했을 때



1️⃣ cookie의 구성별 의미를 알아보자

  • 아래 사진을 보면 쿠키는 Value, Domain, Path, Expires, HttpOnly, Secure가 있다.
  • Secure란?
    • 지금까지 프로젝트는 HTTP를 http:/localhost:1234 로 사용했음
    • 근데 대부분의 웹사이트는 https:/ 를 사용한다.
    • 이때 사용되는 것이 Secure이며, 쉽게 암호화된 웹 사이트인 것이다.
  • HttpOnly란?
    • 프론트엔드가 아니라 API 호출만 받는 것
    • false이면 XSS 공격(프론트엔드 공격: 웹 브라우저로 js접근이 가능한 점을 활용해 공격) 대상이 될 수 있음

2️⃣ 쿠키 설정값을 변경하면?

  • 토큰값 뒤에 쿠키 설정을 수정해줄 수 있다.
res.cookie("token", token, { // 쿠키에 토큰이라는 상자를 만들어 토큰을 담아준다
    httpOnly : true // 쿠키 설정값을 변경
})




8-4 jwt 유효기간 설정

1️⃣ jwt 고도화를 위해 유효기간을 설정해보자

  • 토큰의 유효기간을 설정하고 확인해보면
// 토큰 발급
const token = jwt.sign({
    email : loginUser.email,
    name : loginUser.name
    }, process.env.PRIVATE_KEY, {
        expiresIn : '30m', // 토큰의 유효기간 부여
        issuer : "kim" // 토큰의 발행자 부여
    })

  • 조건이 페이로드에 잘 들어간 것을 확인할 수 있다.



profile
김개발의 개발여정

0개의 댓글