node.js : 토큰

코딩로그·2025년 3월 19일
0

Token

  • 출입증 역할을 하는 도구
  • 클라이언트가 소지하고 있음

JWT (JSON Web Token)

  • JSON 객체에 정보를 담고 이를 토큰으로 암호화

JWT의 구조

HeaderPayloadSignature
토큰의 한 종류 암호화 알고리즘유저 정보, 사용자 권한, 기타 필요한 정보Header + Payload를 base64로 인코딩한 값에 secret을 더해 암호화
* secret : 열쇠 역할을 하는 암호

JWT 검증

  1. Header와 Payload를 base64로 인코딩
  2. 인코딩한 Header, Payload에 Secret을 더해서 암호화
  3. 값이 Signature와 일치해야 유효한 토큰
  • base64 인코딩 방식은 원한다면 얼마든지 디코딩 가능 → payload에 민감한 정보 넣지 않기

JWT.IO : JSON Web Token 공식 사이트

  • Header
  • Payload
  • Signature

JWT 값이 바뀌면 유효하지 않은 값이 됨

위의 JWT 토큰을 가지고 실습

  • JWT 내용
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
    • Header
      eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    • Payload
      eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0
    • Signature
      KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30

토큰 디코딩하기

Base64 Decode and Encode - Online

  • 디코드 모드에서 디코딩 진행하기
    • 기존 코드
      eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0
    • 디코딩된 코드 : JSON 파일로 디코딩
      {"sub":"1234567890","name":"John Doe","admin":true,"iat":1516239022}

‼ base64 인코딩은 원한다면 얼마든지 디코딩이 가능하므로 payload에 민감한 정보 넣지 않기

토큰의 유효성 검증

  • 토큰의 유효성 === 사용자의 인증 상태
  • 클라이언트에서 인증 상태 보관

토큰의 종류

엑세스 토큰 (Access Token)

  • 권한 인증용

리프레시 토큰 (Refresh Token)

  • 엑세스 토큰 재발급용

토큰 인증의 흐름

token 실습

token을 쿠키로 저장할 때

  • 토큰을 사용하기 위한 패키지 설치
npm install jsonwebtoken
  • 변수에 할당
const jwt = require("jsonwebtoken");
  • secretKey 생성
const secretKey = "secret-key"
  • POST 요청
    • jwt.sign → 토큰 생성
      • json으로 관리되기 때문에 객체로 인자 전달
      • 첫 번째 인자 : payloda, 두 번째 인자 : secretKey, 세 번째 인자 : 토큰에 대한 옵션
        • 토큰 옵션 중 expiresIn : 토큰의 유효 기간 설정 (ms 단위, 1000 60 10 = 10분)
// POST 요청 (로그인 요청시 보내는 메소드)
app.post("/", (req, res) => {
  // 2️⃣. 요청 바디에서 전달받은 값을 구조분해 할당을 사용하여 관리
  const {userId, userPassword} = req.body;
  // 3️⃣. (find 메서드를 사용하여) users의 정보와 사용자가 입력한 정보를 비교하여 일치하는 회원이 존재하는지 확인하는 로직![](https://velog.velcdn.com/images/jukdanglife/post/d24d1818-6c21-48a7-9402-dea43a213ccf/image.png)

  const userInfo = users.find(el => el.user_id === userId && el.user_password === userPassword);
  console.log(req.body)

  if (!userInfo) {
    res.status(401).send("로그인 실패");
  } else {
    // 유저가 존재하는 경우 user의 id 정보를 세션에 저장
    jwt.sign({userId: userInfo.user_id}, secretKey, {expiresIn : 1000 * 60 * 10})
    res.send("⭐️세션 생성 완료!");
  }
});
  • 로그인 시 응답 디코드
    • payload : eyJ1c2VySWQiOiJvel91c2VyMyIsImlhdCI6MTc0MjM1NTI2OCwiZXhwIjoxNzQyOTU1MjY4fQ
    • 디코딩된 값 : {"userId":"oz_user3","iat":1742355268,"exp":1742955268}
      • exp - iat = 600000 (만료 시간 - 생성 시간 : 우리가 설정한 만료 기간 (10분))
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJvel91c2VyMyIsImlhdCI6MTc0MjM1NTI2OCwiZXhwIjoxNzQyOTU1MjY4fQ.vpsvf_IDxfdqTgh7KdzfwOuobrH2MLpiMEoR9tj2cFs
  • 로그인시 accessToken 쿠키로 저장
      if (!userInfo) {
        res.status(401).send("로그인 실패");
      } else {
        // 유저가 존재하는 경우 user의 id 정보를 세션에 저장
        const accessToken = jwt.sign({userId: userInfo.user_id}, secretKey, {expiresIn : 1000 * 60 * 10})
        res.cookie("accessToken", accessToken)
        res.send("토큰 생성 완료!");
      }

업로드중..

  • get 요청 처리
    • req.cookies에서 accessToken을 구조 분해 할당으로 추출

    • jwt.verify()를 사용하여 accessToken을 검증하고, 토큰의 페이로드(payload)를 추출

      • secretKeyJWT를 서명(sign)하고 검증할 때 사용하는 비밀 키
      • 검증이 성공하면, 페이로드(사용자 정보)가 반환됨
      • 만약 토큰이 유효하지 않다면 예외가 발생할 수 있음 → try-catch로 감싸는 것이 좋음
    • users 배열에서 JWT 페이로드에 있는 userId와 일치하는 사용자 정보를 찾음

      • find() 메서드를 사용하여 조건(el.user_id === payload.userId)을 만족하는 첫 번째 요소를 반환
      • 해당 사용자의 정보가 userInfo에 저장됨
    • userInfoJSON 형식으로 클라이언트에 응답

      // GET 요청
      app.get("/", (req, res) => {
        const {accessToken} = req.cookies
        const payload = jwt.verify(accessToken, secretKey)
        const userInfo = users.find(el => el.user_id === payload.userId)
        return res.json(userInfo);
      });
  • delete 요청 처리
    • accessToken이 저장된 쿠키를 삭제함으로써 로그아웃

      // DELETE 요청
      app.delete("/", (req, res) => {
        res.clearCookie('accessToken')
        res.send("🧹세션 삭제 완료");
      });

token을 변수로 저장할 때

  • 클라이언트에서 변수로 저장해서 헤더에 전달하는 방식
  • server-header.js & login-header.js 파일 생성
  • accessToken을 login.js에서 변수로 저장
    • server.js의 post 요청에서 accessToken으로 바로 응답
        if (!userInfo) {
          res.status(401).send("로그인 실패");
        } else {
          // 유저가 존재하는 경우 user의 id 정보를 세션에 저장
          const accessToken = jwt.sign({userId: userInfo.user_id}, secretKey, {expiresIn : 1000 * 60 * 10})
          res.send(accessToken);
        }
    • login.js에서 받은 데이터를 accessToekn 변수에 저장
      let accessToken = ""
      
      // 로그인 함수
      function login() {
        const userId = idInput.value;
        const userPassword = passwordInput.value;
      
        return axios.post("http://localhost:3000", { userId, userPassword })
        .then(res => accessToken = res.data)
      }
  • get요청 처리
    • jwt의 accessToken을 헤더로 담아서 보낼 수 있도록 설정
      // 유저 정보를 받아오는 함수
      function getUserInfo() {
        return axios.get("http://localhost:3000", {
          header : { 'Authorization' : `Bearer ${accessToken}`}
        });
      }
    • headers에 담긴 accessToken 정보로 인증 진행
      // GET 요청
      app.get("/", (req, res) => {
        const accessToken = req.headers.authorization.split(" ")[1];
        const payload = jwt.verify(accessToken, secretKey)
        const userInfo = users.find(el => el.user_id === payload.userId)
        return res.json(userInfo);
      });
  • 로그아웃 기능
    • 클라리언트 측에서 accessToken을 초기화하면 됨

      // 로그아웃 함수
      function logout() {
        accessToken = ''
      }
      // 로그아웃 버튼을 클릭하는 경우
      logoutButton.onclick = () => {
        logout()
        renderLoginForm();
      };

내용 정리

종류특징상태 저장 위치장점단점
쿠키클라이언트에 저장되는 짧은 텍스트
HTTP의 무상태성 보완
클라이언트HTTP 요청과 응답의 정보를 저장할 수 있다.
자동으로 서버에 전달된다.
쿠키 자체는 인증을 위한 것이 아니다.
세션인증 정보를 서버에 저장하고 관리
세션 아이디를 쿠키로 전송
서버서버에서 인증 정보를 관리해 보안성이 좋다.로그인한 사용자가 많아지면 서버에 부하가 걸릴 수 있다.
서버 분산이 어렵다.
토큰토큰 자체로 인증 상태 증명 가능
클라이언트에 인증 정보 저장
클라이언트인증 정보를 클라이언트에 저장해 서버의 부하를 줄여준다.토큰이 탈취당하면 무효화하기 힘들다.
profile
hello world!

0개의 댓글