본문 바로가기
개발 기초 다지기

JWT/인증 미들웨어 복습(팀프로젝트와 회고하며 복습하기)

by 너의고래 2024. 6. 10.

이번에 팀프로젝트를 하며 내가 맡은 부분은 댓글 CRUD, 좋아요 부분이었기에 인증 부분에 대한 이해가 조금 부족하게 느껴졌다. 팀원의 코드를 분석해가며 인증 부분의 기초인 쿠키와 세션에 대해 복습을 진행해보려한다.

 

 

** 인증 및 권한 부여

// Access Token을 생성하는 함수
function createAccessToken(id) {
  const accessToken = jwt.sign(
    { id: id }, // JWT 데이터
    ACCESS_TOKEN_SECRET, // Access Token의 비밀 키
    { expiresIn: TOKEN_EXPIREDIN }, // Access Token이 10초 뒤에 만료되도록 설정합니다.
  );

  return accessToken;
}

// Refresh Token을 생성하는 함수
function createRefreshToken(id) {
  const refreshToken = jwt.sign(
    { id: id }, // JWT 데이터
    REFRESH_TOKEN_SECRET, // Refresh Token의 비밀 키
    { expiresIn: REFRESH_TOKEN_EXPIREDIN }, // Refresh Token이 7일 뒤에 만료되도록 설정합니다.
  );

  return refreshToken;
}

 

createAccessToken 함수:

- 입력: 사용자의 ID (id)

- 동작:

jwt.sign() 함수를 사용하여 JWT(access token)를 생성

JWT 데이터로는 사용자의 ID를 포함 - 이 데이터를 통해 서버에서 특정 사용자를 식별할 수 있음

Access Token의 비밀 키로는 ACCESS_TOKEN_SECRET를 사용 - 이는 서버에서 토큰을 검증할 때 사용되는 비밀 키

토큰의 만료 시간은 TOKEN_EXPIREDIN 변수에 지정된 만료 시간으로 설정(10초)로 설정

- 출력: 생성된 Access Token

 

 

createRefreshToken 함수

-입력: 사용자의 ID (id)

-동작:

jwt.sign() 함수를 사용하여 JWT(refresh token)을 생성

JWT 데이터로는 사용자의 ID를 포함하고 있습니다. 이 데이터를 통해 서버에서 특정 사용자를 식별

Refresh Token의 비밀 키로는 REFRESH_TOKEN_SECRET를 사용 - 서버에서 토큰을 검증할 때 사용되는 비밀 키

토큰의 만료 시간은 REFRESH_TOKEN_EXPIREDIN 변수에 지정된 만료 시간으로 설정( 7일)로 설정

-출력: 생성된 Refresh Token

 

 

** 로그인

//로그인

   /** Access Token, Refresh Token 발급 API **/
    const { id } = user;
    const accessToken = createAccessToken(id);
    const refreshToken = createRefreshToken(id);

    // Refresh Token을 가지고 해당 유저의 정보를 서버에 저장합니다.
    tokenStorage[refreshToken] = {
      id: id, // 사용자에게 전달받은 ID를 저장합니다.
      ip: req.ip, // 사용자의 IP 정보를 저장합니다.
      userAgent: req.headers["user-agent"], // 사용자의 User Agent 정보를 저장합니다.
    };

    res.cookie("accessToken", `Bearer ` + accessToken); // Access Token을 Cookie에 전달한다.
    res.cookie("refreshToken", `Bearer ` + refreshToken); // Refresh Token을 Cookie에 전달한다.

 

 

- Access Token과 Refresh Token 생성:

사용자의 ID를 이용하여 createAccessToken 함수와 createRefreshToken 함수를 호출하여 액세스 토큰과 리프레시 토큰 생성

- Refresh Token을 저장:

사용자의 리프레시 토큰을 서버의 특정 위치에 저장

tokenStorage 객체를 사용하여 리프레시 토큰과 사용자 정보(IP 주소 및 User Agent)를 저장 - 사용자의 리프레시 토큰을 검증하고 사용자 정보를 확인할 수 있음

- 클라이언트에게 토큰 전달:

생성된 액세스 토큰과 리프레시 토큰을 클라이언트에게 전달

res.cookie() 함수를 사용하여 쿠키에 토큰을 저장 - 클라이언트는 이를 받아서 이후 요청에서 토큰을 사용할 수 있음

Access Token은 accessToken이라는 이름의 쿠키에, Refresh Token은 refreshToken이라는 이름의 쿠키에 저장

각 토큰 앞에는 "Bearer "를 붙여서 클라이언트에게 전달

 

**authMiddleware

//authMiddleware    
    
    
    const authorization = req.cookies.accessToken;
    //  **Authorization** 또는 **AccessToken이 없는 경우** - “인증 정보가 없습니다.”
    if (!authorization) throw new Error(`인증 정보가 없습니다.`);

    // - **JWT 표준 인증 형태와 일치하지 않는 경우** - “지원하지 않는 인증 방식입니다.”
    const [tokenType, token] = authorization.split(" "); // 토큰타입 Bearer와 payload를 분리

    if (tokenType !== "Bearer")
      throw new Error("지원하지 않는 인증 방식입니다.");

    const decodedToken = jwt.verify(token, ACCESS_TOKEN_SECRET); // 토큰의payload와 SECRETKEY가 동일하면 해당 데이터를 해석하여 변수로할

    const userId = decodedToken.id; // 해석한 데이터객체 내 userId키의 값을 userId 변수에 할당 / 해당변수는 숫자로 된 문자열
    // userId 변수가 데이터베이스 users테이블 내 userId 키의 일치한 값이 있는지 확인
    // 없다면 쿠키 삭제 후 에러  메세지 반환
    // - **Payload에 담긴 사용자 ID와 일치하는 사용자가 없는 경우** - “인증 정보와 일치하는 사용자가 없습니다.”
    const user = await prisma.user.findFirst({
      where: { id: userId },
    });

    if (!user) {
      res.clearCookie("authorization");
      throw new Error("인증 정보와 일치하는 사용자가 없습니다.");
    }

 

- 클라이언트로부터 전송된 HTTP 요청에서 쿠키를 추출하여 accessToken 변수에 저장 - 사용자의 Access Token을 포함

- accessToken이 없다면(클라이언트가 요청에 Access Token을 제공하지 않은 경우) 오류를 발생시킴

- JWT의 경우, 토큰은 "Bearer [토큰값]"의 형태를 가짐 -  accessToken을 공백 문자로 분리하여 토큰의 종류와 실제 토큰 값을 추출

- 추출된 토큰의 종류가 "Bearer"가 아닌 경우(다른 형태의 인증 방식을 사용하는 경우) 오류를 발생

- jsonwebtoken 패키지의 verify 함수를 사용하여 토큰을 해독 - 토큰의 유효성을 검사, 토큰에 포함된 데이터를 반환(사용자 ID)

- 해독된 토큰에서 사용자 ID를 추출하여 userId 변수에 저장합니다.

- 추출된 사용자 ID를 사용하여 데이터베이스에서 사용자를 찾음

- 사용자가 존재하지 않는 경우 (데이터베이스에서 일치하는 사용자를 찾지 못한 경우) 쿠키를 삭제하고 오류를 발생

 

쿠키 (Cookie)

  • 브라우저가 서버로부터 응답으로 Set-Cookie 헤더를 받은 경우 해당 데이터를 저장한 모든 요청에 포함하여 보냄
  • 주로 웹 브라우저에 저장되며, 클라이언트와 서버 간의 상호 작용에서 사용
  • 이름, 값, 만료 날짜 등의 정보로 구성
  • 클라이언트 측에서 수정할 수 있으므로 보안에 취약할 수 있음
  • 주로 사용자의 로그인 상태, 언어 설정, 사용자 환경 설정 등을 유지하기 위해 사용
- cookie-parser

이전에는 req.headers.cookie와 같이 여러 프로퍼티를 넘어서야만 쿠키를 사용할 수 있었지만  cookie-parser 미들웨어를 이용하면 더욱 간편하게 쿠키를 관리할 수 있음

ex)
- 쿠키 조회 부분을 req.cookies로 변경
- 쿠키의 형태가 name=sparta에서 { name: 'sparta' } 형태의 객체로 변환

세션 (Session):

  • 쿠키를 기반으로 구성된 기술 , 클라이언트가 마음대로 데이터를 확인 있던 쿠키와는 다르게 세션은 데이터를 서버에만 저장
  • 세션은 서버 측에서 사용자의 상태를 유지하는 메커니즘
  • 각 사용자는 고유한 세션을 가지며, 클라이언트와 서버 간의 연결을 식별하는 세션 ID를 사용하여 식별
  • 주로 서버의 메모리, 데이터베이스 또는 파일 시스템에 저장
  • 클라이언트 측에서 수정할 수 없으므로 쿠키보다 보안에 더 우수
  • 주로 사용자의 로그인 상태, 장바구니 내용, 사용자 프로필 등을 유지하고 관리할 때 사용
/set-session API를 호출했을 때 name=sparta 의 정보를 서버에 저장하고, 저장한 시간 정보를 쿠키로 반환.
/get-session API를 호출했을 때 쿠키의 시간 정보를 이용하여 서버에 저장된 name 정보를 출력

이런식으로 쿠키에 시간정보를 넘겨줌으로써 식별하고 보안 유지

JWT (JSON Web Token):

  • JWT는 정보를 안전하게 전송하기 위한 인터넷 표준으로, JSON 객체를 사용하여 토큰을 표현
  • 사용자의 정보와 서명된 토큰을 포함하여 페이로드로 구성
  • 서버에서 사용자를 인증하는 데 사용되며, 클라이언트 측에서 상태를 유지할 필요가 없음
  • 토큰은 Base64로 인코딩되어 있으며, 서명되어 클라이언트와 서버 간의 통신에서 안전하게 전송
  • 토큰의 만료 시간을 지정하여 보안을 강화할 수 있음
  • 주로 사용자의 로그인 상태, 권한 부여 등을 관리할 때 사용
  • 데이터를 교환하고 관리하는 방식 쿠키/세션과 달리, JWT 단순히 데이터를 표현하는 형식
** JWT와 쿠키, 세션 다른 점

- JWT로 만든 데이터는 변조가 어렵고, 서버에 별도의 상태 정보를 저장하지 않기 때문에, 서버를 Stateless(무상태)로 관리할 수 있음
- 쿠키와 세션은 사용자의 로그인 정보나 세션 데이터를 서버에 저장하므로 상태를 유지 - Stateful(상태 보존)하게 데이터가 관리됨

**JWT를 써야하는 이유
- JWT가 인증 서버에서 발급되었는지 위변조 여부를 확인할 수 있음
- 누구든지 JWT 내부에 들어있는 정보를 확인할 수 있음 (복호화)

 

 

아직 refresh token에 대한 부분에 대해 복습을 진행하지 못하기도 했고, 눈에 보이지 않는 이 인증 개념이 명확하게 이해가 가지 않는다. 개념으로만 이해하는 것은 한계가 있는 것 같으니, 최대한 많은 코드를 접하고 직접 만들어보며 이해해나가보려한다. 

댓글