JWT와 OAuth 2.0: 인증 및 권한 부여

민정이등장·2024년 10월 28일
0
post-thumbnail

📌 OAuth(Open Authorization)란?

위키백과 ver )

인터넷 사용자들이 비밀번호를 직접 제공하지 않고 다른 웹사이트 상의 자신

들의 정보에 대해 웹사이트나 어플리케이션의 접근 **권한을 **부여할 수 있는 공

통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준이다.

즉, 서드파티 어플리케이션이 사용자의 계정에 접근할 수 있는 권한을 부여하
기 위한 프로토콜을 말한다.

1. OAuth (Open Authorization)과 토큰

1-1. OAuth (Open Authorization) 개요

  • OAuth는 인증과 권한 부여를 위한 프로토콜로써, 로그인 시 사용자의 정보 요청에 대한 인증과 권한 요청을 처리한다.
  • 사용자의 요청을 받아오는 방식은 카카오, 구글과 같은 소셜 로그인 방식을 참고한다.
  • OAuth는 1.0 버전과 2.0 버전이 있으며, 2.0 버전에서는 비밀키와 사용자 토큰을 사용하여 인증과 권한을 분리한다.
  • 2.0 버전에서는 액세스 토큰을 사용하여 요청에 대해 처리한다.
  • 1.0 버전은 복잡한 방식이었으나, 2.0 버전은 개발자 입장에서 구현이 간단해졌다.
  • OAuth 1.0
    • 서드파티 어플리케이션이 사용자의 데이터에 접근할 권한을 얻기 위해 서명된 요청을 사용한다.
    • 요청의 서명 부분은 어플리케이션의 비밀 키와 사용자의 토큰을 조합하여 생성되며, 이를 통해 보안이 유지된다.
  • OAuth 2.0
    • OAuth 2.0은 보다 간편하고 확장성이 있는 버전으로, 기본적으로 인증과 권한을 분리하여 다루는 것이 특징이다.
    • Access Token을 통해 권한을 부여하고, 사용자의 실제 비밀 정보를 공유하지 않는다.

1-2. OAuth 2.0 버전의 특징

  • OAuth 2.0 버전에서는 Bearer Token방식을 지원하며, 이는 소유권을 의미하는 Bearer Token을 사용한다.
  • OAuth에서 로그인 시, 발급된 Bearer Token을 소유권자의 Access Token으로 인증한다.
  • 발급된 Bearer Token은 소유권 확인을 위한 헤더 정보에 포함되며, 소유권자의 요청을 확인하고 권한을 부여한다.
  • OAuth는 사용자 식별과 데이터 서명을 위한 암호화 방식인 JWT와 밀접한 연관이 있다.
  • JWT를 Bearer Token으로 사용할 수 있으며, 클라이언트가 발급받은 JWT를 "Authorization" 헤더에 담아 요청을 보내면, 서버는 해당 JWT의 유효성을 검사하여 권한을 부여한다.
Bearer란? 웹 서비스에서 사용자 인증에 사용되는 인증 방식 중 하나

1-3. OAuth 2.0 버전의 구현

  • OAuth 2.0 버전의 구현을 위해서는 헤더, 페이로드, 시그니처라는 3가지 파트로 나뉜다.
  • 헤더는 타입과 알고리즘이 있으며, 알고리즘은 JWT의 알고리즘과 동일하다.
  • 페이로드는 필요한 데이터를 포함하며, 시그니처는 헤더와 페이로드에 붙어 비밀이 포함된 값을 제공한다.
  • OAuth는 이 3가지 파트에서 필요한 값을 추려내고, JWT를 생성하여 인증과 권한을 부여한다.
  • OAuth 2.0 버전은 소유권 확인을 위한 JWT를 발급받은 후, 이를 통해 인증과 권한을 부여한다.

JWT(JSON Web Token) 형식의 토큰

  • Bearer Token: "Bearer"는 이 토큰이 "소지자"에 의해 사용될 수 있음을 나타낸다. 즉, 이 토큰을 가진 사람이 해당 리소스에 접근할 수 있는 권한이 있다는 뜻이다.
  • eyJ~~: 이 부분은 실제 JWT의 내용이다. JWT는 Base64Url로 인코딩되어 있어서 "eyJ~~"와 같은 형식으로 표시된다. 실제로는 더 긴 문자열로, 디코딩하면 JSON 형태의 데이터를 볼 수 있다.
    (밑에서 자세히 설명)

📌 JWT(JSON Web Token)

🔗 JWT사이트

JWT(JSON Web Token)란?

  • 웹 애플리케이션에서 정보를 안전하게 전송하기 위한 간단한 방법이다.
  • 인증된 사용자를 식별하거나 데이터에 서명을 하여 변조를 방지하는데 사용한다.

JWT 구성

  • Header : 토큰의 타입과 해시 알고리즘 정보가 포함한다.
  • Payload : 실제 정보 데이터가 포함되며, 클레임(claim)이라 불리는
    키-값 쌍으로 이루어져 있다.
  • Signature : 토큰의 무결성을 검증하기 위한 서명 부분으로,
    Header와 Payload의 조합에 비밀 키를 사용해 생성한다.

  • JWT 구성 자세한 설명

    “alg” : “HS256”
    // JWT 서명 알고리즘, HMAC SHA-256을 사용하여 서명 생성.
    “typ” : “JWT”
    // 토큰 유형이 JWT 임을 명시.

    "sub" : “~”
    // "sub"(subject) 클레임으로, JWT의 주체를 나타냄. (사용자 ID)
    “name” : “~~”
    // "name" 클레임으로, 사용자의 이름을 나타냄.
    "iat" : ~~
    // iat"(Issued At) 클레임으로, JMT가 발급된 시간을 타임스탬프 형식으로 나타냄.

    HMACSHA256(...) :
    // HMAC SHA-256 알고리즘을 사용하여 서명 생성 과정
    base64UrlEncode(header) ~ “.”~(payload) :
    // 헤더와 페이로드를 Base64 URI 인코딩 한 후 결합

    your-256-bit-secret:
    // 서명 생성 시 사용되는 비밀 키. 서버에서만 알고 있어야 함.

JWT 작동방식

핵심

  • JWT는 서명(인증)을 목적으로 사용된다. JWT는 Base64로 인코딩되어 있지만, 이는 암호화가 아니라 단순히 인코딩이기 때문에 디버거를 사용하면 페이로드의 내용을 쉽게 복호화할 수 있다. 따라서 페이로드에는 비밀번호와 같은 민감한 정보를 포함하면 안 된다.
  • JWT의 주요 목적정보 보호가 아니라 위조 방지이다. 서명을 통해 데이터의 무결성을 보장하며, 비밀키가 노출되지 않는 한 토큰의 내용이 위조되더라도 서명을 통해 위조된 토큰을 걸러낼 수 있다.

JWT 주요 함수

  • sign : 서버측에서 JWT를 생성할 때 사용
    jwt.sign(payload, secretOrPrivateKey, [options, callback])

  • verify : 클라이언트나 서버에서 받은 JWT의 유효성을 검증할 때 사용
    jwt.verify(payload, secretOrPrivateKey, [options, callback])

EX )

// JWT 생성
const token = jwt.sign(payload, secretKey, { expiresIn: '1h' });

// JWT 검증
jwt.verify(token, secretKey, (err, decoded) => {
  if (err) {
    console.log('Token is invalid or expired');
  } else {
    console.log('Decoded payload:', decoded);
  }
});

JWT 사용하기

npm install jsonwebtoken
이 명령어로 jsonwebtoken을 설치한다.

const jwt = require("jsonwebtoken");
사용하고자 하는 jsonwebtoken을 불러온다.


index.ejs 파일

  • 사용자가 JWT 토큰을 통해 인증된 상태인지 확인하고, 그에 따라 로그인 링크 또는 환영 메시지를 보여준다.
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>JWT</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  </head>
  <body>
    <h1>JWT 실습</h1>
    <div id="info"></div>

    <script>
      (async function () {
        const token = localStorage.getItem("token");
        const info = document.querySelector("#info");

        let html;
        if (!token) {
          html = '<a href="/login">로그인</a>';
        } else {
          const { data } = await axios({
            method: "POST",
            url: "/verify",
            headers: {
              Authorization: `Bearer ${token}`,
            },
          });

          if (data.result) {
            html = `<p>${data.name}님 환영합니다!</p>
            <button>로그아웃</button>
            `;
          }
        }
        info.innerHTML = html;
      })();

      function logout() {
        localStorage.clear();
        document.location.reload();
      }
    </script>
  </body>
</html>
  • 로직:
    • 페이지 로드 시 localStorage에서 token을 가져온다.
    • 토큰이 없으면 로그인 링크를 표시한다.
    • 토큰이 있으면 서버에 /verify POST 요청을 보내 인증을 확인한다.
    • 인증이 성공하면 사용자 이름을 표시하고 로그아웃 버튼을 제공한다.
    • 로그아웃 버튼 클릭 시 localStorage를 초기화하고 페이지를 새로고침한다.

login.ejs

  • 사용자가 ID와 비밀번호를 입력하여 로그인할 수 있도록 한다.
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>login</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  </head>
  <body>
    <h1>로그인</h1>
    <form name="login-form">
      <input type="text" id="id" placeholder="ID" /><br />
      <input type="password" id="pw" placeholder="PW" /><br />
      <button type="button" onclick="login()">로그인</button>
    </form>

    <script>
      async function login() {
        const form = document.forms["login-form"];
        const { data } = await axios({
          method: "POST",
          url: "/login",
          data: {
            id: form.id.value,
            pw: form.pw.value,
          },
        });

        if (data.result) {
          localStorage.setItem("token", data.token);
          document.location.href = "/";
        } else {
          alert(data.message);
          document.location.reload();
        }
      }
    </script>
  </body>
</html>
  • 로그인 폼을 제공하며, 사용자가 ID와 비밀번호를 입력한다.
  • 로그인 버튼 클릭 시 login 함수를 호출하여 서버에 /login POST 요청을 보낸다.
  • 서버 응답이 성공적이면 localStorage에 JWT를 저장하고 홈 페이지로 리디렉션한다.
  • 실패할 경우 알림을 보여주고 페이지를 새로 고친다.

index.js 파일

  • Express.js를 사용하여 서버를 설정하고 JWT 기반의 로그인 및 인증을 처리한다.
const express = require("express");
const jwt = require("jsonwebtoken");
const app = express();
const PORT = 8000;
const SECRET = "9PBYbnIhfXEVQdeXrvPWrX6ydDAJkIqV";

const userInfo = { id: "banana", pw: "1234", name: "홍길동", age: 20 };

app.set("view engine", "ejs");
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.get("/", (req, res) => {
  res.render("index");
});

app.get("/login", (req, res) => {
  res.render("login");
});

app.post("/login", (req, res) => {
  try {
    const { id, pw } = req.body;
    const { id: uId, pw: uPw } = userInfo;

    if (id === uId && pw === uPw) {
      // 토큰 생성
      const token = jwt.sign({ id }, SECRET);
      console.log(token);
      res.json({ result: true, token });
    } else {
      res.json({ result: false, message: "로그인 정보가 올바르지 않습니다." });
    }
  } catch (error) {
    console.log(error);
  }
});

app.post("/verify", (req, res) => {
  if (req.headers.authorization) {
    const headers = req.headers.authorization;
    console.log(headers);
    const [bearer, token] = headers.split(" "); // ['Bearer', 'token']

    try {
      const result = jwt.verify(token, SECRET);
      console.log(result); // { id: 'banana', iat: 1730078415 }

      if (result.id === userInfo.id) {
        res.json({ result: true, name: userInfo.name });
      } else {
        return res.status(403).json({ result: false, message: "검증 실패" });
      }
    } catch (error) {
      console.log(error);
      return res.status(403).json({ result: false, message: "검증 실패" });
    }
  } else {
    res.redirect("/login");
  }
});

app.listen(PORT, () => {
  console.log(`http://localhost:${PORT}`);
});
  • 기본적인 서버 설정과 EJS 뷰 엔진을 설정한다.
  • GET /GET /login 엔드포인트를 통해 각각 메인 페이지와 로그인 페이지를 렌더링한다.
  • POST /login 엔드포인트는 사용자가 제공한 ID와 비밀번호를 확인하여 JWT를 생성한다.
    • 로그인 정보가 맞으면 JWT를 발급하고, 맞지 않으면 오류 메시지를 반환한다.
  • POST /verify 엔드포인트는 클라이언트가 보내온 JWT를 검증한다.
    • JWT가 유효하면 사용자 정보를 반환하고, 유효하지 않으면 에러를 반환한다.

📖 위 코드 노트필기



🔍 공부하면서 어려웠던 부분

토큰 분리: authorization 헤더의 값을 공백으로 나누어 bearer와 token 두 개의 변수에 할당한다. bearer는 "Bearer" 문자열이고, token은 실제 JWT이다.

headers 변수: 이 변수는 req.headers.authorization의 값이다.

일반적으로 JWT를 포함하는 Authorization 헤더는 다음과 같은 형식이다 :

Authorization: Bearer <token>

Bearer 9PBYbnIhfXEVQdeXrvPWrX6ydDAJkIqV

split(" ") 메서드: split(" ")는 문자열을 공백(" ")을 기준으로 나누는 메서드이다.

위를 기준으로 설명하면 :

  • headers.split(" ")를 실행하면, ["Bearer", "9PBYbnIhfXEVQdeXrvPWrX6ydDAJkIqV"]와 같은 배열이 생성된다.

비구조화 할당:

  • const [bearer, token]는 비구조화 할당 구문이다. 이 구문을 사용하면 배열의 각 요소를 별도의 변수에 쉽게 할당할 수 있다.
  • 위에서, 첫 번째 요소 "Bearer"는 bearer 변수에, 두 번째 요소인 JWT 토큰은 token 변수에 할당된다.

⬇️ 위 코드를 실행 했을때 ⬇️

profile
킵고잉~

0개의 댓글