Post

데브코스 TIL - Day 27

23년 12월 25일 강의를 들은 내용을 기록한 글입니다.

📒인증, 인가

인증 (Authentication)

인증은 신원을 확인하는 프로세스입니다.
비밀번호, 하드웨어 토큰, 기타 여러 방법을 사용해야 합니다.

인가 (Authorization)

인가는 인증 이후의 프로세스입니다. 인증된 사용자가 어떠한 자원에 접근할 수 있는지를 확인하는 절차가 바로 인가이다.

비교

  • 사이트에 가입된 사용자라는 것을 증명하는 것은 인증
  • 인증 후에 페이지 접근 권한 여부를 확인하는 것은 인가

Stateless

📒 쿠키, 세션, JWT

로그인을 유지시킬 때 사용하는 방법들

1. 쿠키

HTTP 쿠키란 서버에서 사용자 브라우저로 전송하는 작은 데이터를 뜻한다.
브라우저는 서버에서 받은 데이터(Cookie)를 저장해 놓았다가 동일한 서버로 재요청 시 제공받았던 데이터를 함께 전송한다. 이를 통해 HTTP의 stateless를 보완해 HTTP 통신에서도 상태 정보를 보존할 수 있게 됩니다

■ 쿠키를 사용한 로그인 기능

사용자가 로그인할 때
서버는 ID와 PW를 Cookie에 담아 응답하고,
이후 요청부터는 브라우저가 ID/PW를 Cookie에 담아 함께 보내기 때문에
,
인증/인가를 위해 매번 아이디와 비밀번호를 입력하지 않아도 되게 된다.

■ 장점

  • 기존 로그인을 위한 정보를 사용하기 때문에 인증/인가를 위한 추가적인 데이터 저장이 필요 없다.
  • 서버에 정보를 저장하지 않기때문에 서버 저장 공간이 필요 없고, Stateless하다고 할 수 있다.

■ 단점

  • 사용자의 주요 정보를 매번 요청에 담아야 하기에 보안이 취약하다.

2. 세션

https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQFFUP%2FbtrNdTUj8TF%2FOyw4ar5S7aDFaFCKTaIyc1%2Fimg.png

쿠키의 취약한 보안을 해결하기 위해 사용하는 것이 세션 Session은 고객의 주요 정보가 아닌, 단지 고객을 식별할 수 있는 값 생성해 Cookie로 주고받는다.

■ 세션를 사용한 로그인 기능

A 사용자가 ID/PW를 통해 로그인을 했다면
A사용자를 식별할 수 있는 값를 생성해 Cookie로 브라우저에 심고 매번 요청 때마다 생성한 값을 통해 인증/인가를 자동으로 진행한다.
이 때 생성되는 사용자 식별 값을 Session ID라 한다.

■ 장점

  • 쿠키보다 보안이 비교적 좋다.

■ 단점

  • 사용자를 식별할 수 있는 값을 생성하고 서버에 저장해두어야 한다.
  • 서버 저장 공간이 필요하다.
  • Stateless하다고 표현할 수 없다. (정보를 저장하기 때문에)

token?

사용자를 인증할 수 있는 정보가 숨겨진 암호화된 Access Token을 발행하고, 인증이 필요할 때마다 서버에 Token과 함께 요청을 보낸다. 서버는 저장된 데이터가 아닌 Token 해독을 통해 Token 속에서 사용자를 식별할 수 있는 정보들을 알아내고 이를 바탕으로 인증/인가가 자동으로 진행한다.

3. JWT (Json Web Token)

JSON 형태의 데이터를 안정하게 전송하기 위한 토큰

jwt 공식 문서

■ JWT 구조

https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMNg1I%2FbtrNhSfsmXs%2FtMK1UkKEq3BPAZ96kF6k7k%2Fimg.png

  • Header: 토큰을 암호화하는데 사용한 알고리즘, 토큰 형태
  • PAYLOAD: 사용자 정보
    • sub: 토큰 제목
    • iat: 언제 발급되었는지
  • SIGNATURE: 암호화를 위한 데이터,
    • MY_SECRET_KEY : 서버만 알고 있고, 서버와 비교하여 유효성 검사할 데이터

■ 토큰 사용한 로그인 기능

loginflow

서비스에 로그인 요청을 하면 서비스는 클라이언트에게 토큰을 발급하고, 전달한다.
클라이언트는 발급받은 토큰을 보관하고 있다가 토큰을 사용해 인증/인가를 요청한다.

■ 토큰 사용한 로그인 기능 - 실제 서비스

loginflow2

■ 장점

  • 암호화된 토큰이기 때문에 보안에 강하다.
  • Stateless하다. (서바가 상태를 저장하지 않음)
  • 서버에 부담을 줄여줄 수 있다.

실습

#JWT 구현해보기

npm - jsonwebtoken

#설치 명령어

npm i jsonwebtoken

#모듈 불러오기

  • sign: token 발행
  • sign(페이로드, 암호키) + SHA256
1
2
3
4
5
const jwt = require("jsonwebtoken");
const token = jwt.sign({ foo: "bar" }, "shhhhh");

console.log(token);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE3MDM0NjkyOTh9.aK-o_-G_0fYbnI4G5rbB9IZ5m6lp1etGSNK1LU8E_88

#token 검증하기

  • verify: token 복호화
  • verify(token, 암호키)
1
2
3
4
5
6
const jwt = require("jsonwebtoken");
const token = jwt.sign({ foo: "bar" }, "shhhhh");

const decoded = jwt.verify(token, "shhhhh");
console.log(decoded);
// { foo: 'bar', iat: 1703469696 }

#.env 파일을 만들고, 환경 변수 숨기기

1. dotenv 패키지 설치

npm i dotenv

2. .env 파일 만들고, 환경 변수 저장하기

1
2
3
4
# 디렉토리
├── .env
├── .gitignore
└── jwt-demo.js
  • .env : 환경 변수를 관리하기 위한 파일
  • .gitignore : github 업로드시 무시할 파일들을 설정하는 파일
1
2
// .env
const

3. 환경변수를 사용할 파일에서 dotenv 모듈을 가져오고, process.env를 사용해서 환경 변수 가져오기

1
2
3
4
5
6
7
8
9
10
11
12
13
// jwt-demo.js

const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");

dotenv.config();

const PRIVATE_KEY = process.env.PRIVATE_KEY;
const token = jwt.sign({ foo: "bar" }, PRIVATE_KEY);

const decoded = jwt.verify(token, PRIVATE_KEY);

console.log(decoded);

#실습 - 유튜브에 jwt 적용하기

■ token cookie에 담아서 보내기

1
res.cookie("token", token);

※ 나중에 req에서 cookie를 확인할 때 parser를 사용해야 함!

■ httpOnly 속성 변경하기

1
res.cookie("token", token, { httpOnly: true });

■ token 유효기간 설정하기

  • expiresIn
1
2
3
4
const token = jwt.sign({ email, name: matchedEmail.name }, TOKEN_PRIVATE_KEY, {
  expiresIn: "1h",
  issuer: "hyemin"
});

결과 유효기간

전체 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
const express = require("express");
const router = express.Router();
const conn = require("../mysql");
const { body, validationResult } = require("express-validator");

const jwt = require("jsonwebtoken");
const dotenv = require("dotenv");

dotenv.config();

const TOKEN_PRIVATE_KEY = process.env.PRIVATE_KEY;

router.use(express.json());

const validationBodyEmail = body("email")
  .notEmpty()
  .isEmail()
  .withMessage("이메일 형식이 아닙니다.");
const validationBodyName = body("name")
  .notEmpty()
  .isString()
  .withMessage("유저 이름은 문자여야합니다.");
const validationBodyContact = body("contact")
  .notEmpty()
  .isString()
  .withMessage("전화번호 확인해주세요");
const validationBodyPassword = body("pwd")
  .notEmpty()
  .isString()
  .withMessage("비밀번호 확인해주세요");

const validate = (req, res, next) => {
  const err = validationResult(req);
  if (err.isEmpty()) return next();
  return res.status(400).send(err.array());
};

const checkSqlError = (err, res) => {
  if (err) {
    console.log(err);
    return res.status(400).end();
  }
};

router.post(
  "/join",
  [
    validationBodyEmail,
    validationBodyName,
    validationBodyPassword,
    validationBodyContact,
    validate
  ],
  (req, res, next) => {
    const { email, pwd, name, contact } = req.body;
    const sql =
      "INSERT INTO users (email,name,password,contact) VALUES (?,?,?,?)";
    const values = [email, name, pwd, contact];

    conn.query(sql, values, (err, results, fields) => {
      checkSqlError(err, res);
      res.status(201).send({ message: `${name}님, 환영합니다.` });
    });
  }
);
router.post(
  "/login",
  [validationBodyEmail, validationBodyPassword, validate],
  (req, res, next) => {
    const { email, pwd } = req.body;
    const sql = "SELECT * FROM users WHERE email = ?";
    conn.query(sql, email, (err, results) => {
      checkSqlError(err, res);

      const [matchedEmail] = results;

      if (matchedEmail) {
        if (matchedEmail.password === pwd) {
          const token = jwt.sign(
            { email, name: matchedEmail.name },
            TOKEN_PRIVATE_KEY,
            {
              expiresIn: "1h",
              issuer: "hyemin"
            }
          );

          res.cookie("token", token, {
            httpOnly: true
          });
          console.log(token);

          res
            .status(200)
            .send({ message: `${matchedEmail.name}님, 환영합니다.` });
        } else {
          res.status(403).send({ message: "비밀번호가 일치하지 않습니다." });
        }
      } else {
        res.status(403).send({ message: "일치하는 아이디가 없습니다." });
      }
    });
  }
);

router
  .route("/users")
  .get([validationBodyEmail, validate], (req, res) => {
    const { email } = req.body;
    const sql = "SELECT * FROM users WHERE email = (?)";

    conn.query(sql, [email], (err, results) => {
      checkSqlError(err, res);

      if (results.length > 0) {
        res.status(200).send(results[0]);
      } else {
        res.status(404).send({ message: "일치하는 아이디가 없습니다." });
      }
    });
  })
  .delete([validationBodyEmail, validate], (req, res) => {
    const { email } = req.body;
    const sql = "DELETE FROM users WHERE email = ? ";

    conn.query(sql, [email], (err, results) => {
      checkSqlError(err, res);

      if (results.affectedRows > 0) {
        res.status(200).send(results);
      } else {
        res.status(400).end();
      }
    });
  });

module.exports = router;

참고 사이트

지마켓 기술 블로그 - 인증/인가는 어디에 어떻게 구현해야 할까?

This post is licensed under CC BY 4.0 by the author.