6 분 소요

[peauty] 🔑로그인 발라먹기

Peauty를 진행하면서 로그인에 대한 지적을 받았던 적이 있습니다.

제가 담당하지 않은 파트이고 시간이 촉박해서 이 부분에 이해가 부족한데 이번 기회에 현재 로그인 기능이 어떤 식으로 구현되었으며, 리팩토링할 점에 대해서 이야기 해보겠습니다.

🤔현재 PEAUTY의 상태는?

PEAUTY는 소셜 로그인만을 지원했습니다.

JWT 기반 인증을 사용했는데, Access Token과 Refresh Token을 발급받고 토큰은 localStorage에 저장하는 형식입니다.

여기서 JWT 토큰이란 JSON Web Token의 줄임말로, 웹에서 사용자 인증과 정보를 전달하기 위해서 사용하는 토큰 기반 인증 방식입니다.


🔐JWT의 기본구조

JWT는 총 세가지 부분으로 구성된 문자열입니다.

헤더.header	.	페이로드.payload	.	서명.signature
  1. Header(헤더)

    JWT에서 헤더는 토큰 타입과 서명 알고리즘을 정의합니다.

    {
        "alg": "HS256",
        "typ": "JWT"
    }
    
  2. Payload(페이로드)

    실제 담고 싶어하는 사용자 정보를 담는 부분입니다.

    {
        "userId": "user123",
        "role": "admin",
        "exp": 12345000
    }
    
  3. Signature(서명)

    앞의 두 부분을 합쳐서 비밀 키를 사용해 만든 암호화된 서명으로 이 서명을 통해 토큰이 위변조되지 않았는지 검증할 수 있습니다.

JWT의 특징

특징 설명
무상태(stateless) 서버가 세션을 저장하지 않습니다.
클라이언트-서버 간 자율적인 정보 전달 사용자 정보가 토큰 안에 있습니다.
편리한 인증 처리 요청 시 HTTP Header에 토큰만 실어서 보냅니다.
Base64 인코딩 사람이 볼 수 있지만 암호화는 아닙니다.(보안 주의)

JWT는 암호화된 것이 아닙니다!(+지수)

https://pinnate-double-5fb.notion.site/1ccccbb28621804b92edf78ac250b0f8

JWT는 Base64로 인코딩된 문자열일 뿐입니다. 즉 토큰을 복호화하면 누구나 그 안에 담긴 정보를 읽을 수 있습니다. 실제로 디코딩 해보면 아래와 같은 정보들이 나옵니다.

{
  "user": {
    "id": 7,
    "role": "ROLE_CUSTOMER"
  },
  "exp": 1712345678
}
그럼 왜 위험한가요?
  • 디코딩 자체는 문제는 아닙니다. JWT는 설계상 누구든 디코딩할 수 있게 만든 토큰이기 때문입니다.
  • 하지만 중요한 정보(예: 이메일, 실명, 주소 등)는 절대 넣으면 안 됩니다.
  • 토큰을 디코딩한 정보를 기준으로 UI를 바꾸는 것은 가능하지만, 절대 인가(권한 체크)는 프론트에서 하면 안 됩니다.

Access Token과 Refresh Token

둘 다 로그인을 진행하고 발급 받는 JWT 토큰이지만 역할이 다릅니다.

구분 Access Token Refresh Token
🔑 목적 사용자 인증 (내가 누군지 증명) Access Token 재발급
🕐 유효기간 짧음 (몇 분~1시간) 김 (며칠~수주)
📦 포함정보 사용자 ID, 권한 등 거의 정보 없음 / 사용자 ID 정도
📡 어디에 쓰나? API 요청할 때 Authorization 헤더에 붙임 Access Token 만료 시, 새로운 Access Token을 요청할 때 사용
🧨 노출 시 위험 높음 (API 요청 가능) 중간 (AccessToken은 없지만 탈취 시 재발급 가능)

🔄 흐름

  1. 로그인 성공

    Access Token과 Refresh Token이 발급됩니다.

  2. API 호출 시

    Access Token을 Authorization 헤더에 담아서 보냅니다.

  3. Access Token 만료

    Refresh Token으로 새로운 Access Token을 발급 받습니다.

  4. Refresh Token 만료

    다시 로그인 해야합니다.

브라우저 저장소 3가지

1. LocalStorage
항목 설명
📌 특징 브라우저에 영구 저장 (브라우저 꺼도 안 없어짐)
🧠 용도 Access Token을 저장하는 데 자주 쓰임
❌ 단점 자바스크립트로 접근 가능 → XSS 공격에 취약
2. SessionStorage
항목 설명
📌 특징 브라우저 탭/창을 닫으면 자동 삭제
🧠 용도 짧은 세션에만 쓰는 정보 저장
❌ 단점 새 창/탭 열면 저장 안 돼 있음 → 유지가 안 됨
항목 설명
📌 특징 서버/클라이언트가 함께 사용하는 저장소
⏱️ 만료시간 지정 가능
🧠 용도 주로 Refresh Token을 HttpOnly 쿠키에 저장
✅ 장점 HttpOnly로 설정 시 JS 접근 불가 → XSS 방지
❌ 단점 CSRF 공격에 노출 가능성 있음 (대응 방법 필요: SameSite 설정 등)

🎯 토큰은 어디에 저장해야 할까?

저장 위치 Access Token Refresh Token
LocalStorage 가능하긴 하지만 XSS 위험 있음 ❌ 권장하지 않음
SessionStorage 가능하긴 하지만 탭 닫으면 사라짐
HttpOnly Cookie ❌ (JS로 접근 불가라 API 요청에 못 씀) ✅ 안전하게 보관 가능

➡️ 추천 조합:

  • Access Token: 메모리나 LocalStorage (단, XSS 주의)
  • Refresh Token: HttpOnly Cookie (가장 안전)

XSS란?
XSS는 악성 스크립트를 웹 페이지에 삽입해서 사용자 브라우저에서 실행되게 만드는 공격.
➡️누군가 웹페이지에 악성 자바스크립트를 심어서 그 페이지를 보는 다른 사람의 브라우저에서 실행되게 하는 것
로그인 토큰을 훔치거나 화면을 조작하거나 클릭을 유도(피싱)을 할 수 있음.


일반 사용자 기준으로 살펴보겠습니다.

image-20250406124909475

로그인 요청을 보내면 data를 응답 데이터를 return 합니다.

백엔드 코드를 살펴보겠습니다.

소셜로그인이 시작 될 때, signIn메서드를 실행합니다.

image-20250406130733815

이후 토큰이 발급되는데

기존 회원이라면 createRegisterSignInResult메서드를 통해서 처리한 뒤에 SignTokens(accessToken+ refreshToke)을 생성합니다. 그리고 회원정보와 함께 반환합니다.

미가입 회원인 경우에 createUnregisteredSignInResult메서드를 통해서 처리하고 토큰 없이 소셜 정보만 반환하고 프론트엔드에서 추가 회원가입 절차를 진행합니다.

그리고 회원가입 완료 직후에 자동으로 SignTokens를 발급합니다. 이 토큰은 SignUpResult를 통해서 클라이언트에 전달합니다.

image-20250406130007113

토큰 전달 방식은 HTTP Response에서 Response Body에 토큰을 포함하여 전달합니다.

전달받은 response.data중에 token들을 accessToken과 refreshToken으로 나눠서 저장하고

image-20250406130446698

localstorage에 저장합니다.

image-20250406130257836

그리고 jwtDecode를 통해서 JWT 토큰을 디코딩하고 있습니다.

이는 사용자의 역할을 확인하여 적절한 페이지로 라우팅하기 위함입니다.

🙄불필요한 Decoded

그러나 이는 불필요한 코드입니다.

그 이유는 백엔드를 살펴보면 designercustomer의 api가 따로 분리되어있습니다.

image-20250406133734034

image-20250406130007113

이는 디자이너와 고객 회원가입 로직이 완전히 분리되어있음을 알 수 있습니다.

프론트엔드에서도 요청하는 url이 분리 되어있습니다. 이런 경우 굳이 decode하지 않아서 회원의 role을 알 수 있습니다.

image-20250406134012583

현재 PEAUTY는 고객과 디자이너를 위한 별도의 API 엔드포인트가 존재합니다.

JWT 디코딩

PAYLOAD에 있는 사용자 정보를 빠르게 얻을 수 있고 사용법이 간단해서 자주 사용됩니다.

또한 UI 분기 처리 같이 역할을 확인할 때 자주 사용합니다.

🔼장점

즉시성

  • 추가적인 API호출 없이 즉시 사용자의 정보를 확인 할 수 있습니다.
  • 초기 로딩 속도가 빠릅니다.

네트워크 부하 감소

  • 서버 통신 없이 클라이언트에서 처리합니다.
  • 서버 리소스를 절약할 수 있습니다.

오프라인 지원

  • 네트워크 연결 없이도 기본 사용자 정보를 확인 할 수 있습니다.

🔼단점

보안 취약성

  • 클라이언트에서 토큰 내용 조작 가능성이 있습니다.
  • 토큰 유효성을 실제로 검증할 수 없습니다.

토큰 구조 의존성

  • 토큰 구조 변경시 프론트엔드 코드도 수정이 필요합니다.
  • 백엔드와의 결합도가 강합니다.

추가 라이브러리 의존

  • 현재 프로젝트에서도 jwt-decode라이브러리를 사용하고 있는데, 이 때문에 외부 라이브러리 의존성이 발생합니다.
  • 번들의 크기를 증가시킵니다.

실시간성 부족

  • 토큰 만료나 권한 변경 즉식 반영이 어렵습니다.
  • 실제 권한과 불일치 가능성이 있습니다.

🔎개선 방안 탐색

현재 PEAUTY는 역할에 따라서 다른 로그인 엔드포인트를 제공하고 있습니다.

그래서 로그인 후에 받은 JWT는 이미 역할 기반으로 발급된 것이고 따라서 로그인 했을 때 이미 사용자의 역할이 결정되있습니다.

➡️ JWT 디코딩이 불필요하다.

그렇기 때문에 JWT 디코딩을 제거하여 불필요한 의존성을 제거하고 코드를 단순화 하겠습니다.

디코드를 한 이유는 이해할 수 있습니다. 왜냐하면 Swagger를 통해 response data를 보면 토큰만 return 되기 때문입니다.

image-20250406134555169

그래서 방법을 생각해 봤을 때 세 가지를 떠올릴 수 있습니다.

  1. 백엔드에서 role 전달
    • 토큰과 함께 role 정보 전달
    • 구현은 단순하나 추가 데이터 전송 필요
  2. API 호출 시 역할 구분
    • API 요청 시점에 역할 검증
    • 실제 권한으로 검증하나 매 요청마다 검증 필요
  3. API 분리 기반 로그인
    • 로그인 시점부터 역할별 API 분리
    • 명확한 역할 구분과 불필요한 API 호출 방지

✅백엔드에서 role 전달

// 백엔드 응답
{
    accessToken: "xxxyyyzzz",
    refreshToken: "xxxyyyzzz",
    role: "customer"
}

장점

  • 단순한 구현
  • 클라이언트 부담 감소

단점

  • 추가 데이터 전송 필요
  • 토큰과 role 불일치 가능성
  • 보안상 중복 검사 필요

✅API 호출 시 역할 구분

image-20250406145542499

API 호출의 성공과 실패로 role을 부여합니다. 그리고 실제 권한으로 검증합니다.

image-20250406145553148

로그인시에 역할을 확인하고 role에 맞는 화면으로 보낼 수 있도록 AuthLayout.tsx에서 현재는 image-20250406150223818

이렇게 디코딩을 하지만

image-20250406150247762

이런식으로 분리해 줍니다.

api 인터셉터에서도 접근을 제어하여 role을 검증합니다.

image-20250406145609322

장점

  • API 요청 시점에 역할 검증
  • 백엔드와 권한 일치 보장
  • 전역 상태관리 및 상태 변경 시 자동 리렌더링
  • 역할 기반 UI 렌더링 단순화
  • 타입 안정성
  • 컴포넌트 재사용성
  • 토큰만으로도 구현 가능

단점

  • 초기 API 호출 필요
  • 추가 네트워크 요청
  • 실패시 추가 API 요청
  • 실제 권한으로 검증하나 매 요청마다 검증 필요

✅API 분리 기반 로그인

애초에 로그인 버튼도 분리되어 있으니 로그인 버튼에 따라 다른 API를 호출하고, 로그인 성공 시 role을 상태관리에 저장하는 방식

image-20250406151147907

로그인 버튼을 눌렀을 때 각각 상태를 저장하도록 각각의 버튼마다 Signtype상태를 저장합니다.

image-20250406151249780

image-20250406151325366

그리고 나서 type이 바뀌었을 때, type에 따라서 다른 API를 호출하고 user상태관리에 role또한 저장합니다.

그리고 각 role에 맞는 main 페이지로 이동합니다.

장점

  • 로그인 시점부터 역할 분리
  • 각 역할별 API 엔드포인트 활용
  • 중앙화된 상태관리
  • 자동 리렌더링
  • 잘못된 API 접근 방지
  • 역할 기반 로직 분리
  • 가독성 향상
  • 확장 용이

단점

  • 로그인 플로우가 분리되어야함
  • 역할 변경시 재로그인
  • 코드 중복 가능성

적용

적용해보니 문제점이 많았습니다.

  1. 역할말고도 userId를 JWT 디코딩을 통해 얻고 있었습니다.
  2. userId를 통해서 각종 호출을 하고 있었습니다.
  3. 버튼을 통해 저장된 signInType이 리다이렉트되면서 초기화 됐습니다.

이를 해결하는 가장 간단한 방법은 백엔드에서 userId를 같이 보내주거나 usserId를 알 수 있는 api가 존재해야합니다. 하지만 없습니다.

그래서 결국엔 토큰을 디코딩 할 수 밖에 없었습니다.

image-20250406172416166

🎯최종 결론

이미 역할이 분리되어있고, 로그인 버튼, 엔드 포인트 등이 이미 분리 되어있기 때문에 역할 기반으로 분리하여 관리하는 것이 더 적합하다고 판단됩니다.

  1. 이미 분리된 API 구조 활용 가능
  2. 로그인 시점부터 명확한 역할 구분
  3. 불필요한 API 호출 최소화
  4. 사용자 경험 향상
  5. 구현 복잡도 감소

구체적인 개선 사항

  1. 상태 관리 도입
    • Zustand를 사용한 전역 상태 관리
    • role 정보의 중앙 관리
  2. API 인터셉터 개선
    • 역할 기반 접근 제어
    • 명확한 에러 처리
  3. 로그인 로직 개선
    • 역할별 API 호출 분리
    • 상태 관리와 연동

🥰느낀 점

역시나 소통의 중요성을 느끼게 됩니다. JWT 디코딩하는 방식이 무조건 나쁘다 라는 것이 아닙니다. 다만 현재 프로젝트에서는 불필요한 작업임을 깨닫게 되었습니다.

상태관리를 잘 활용한다면 어플리케이션의 전반적인 데이터 흐름을 예측 할 수 있고 또 일관되게 하며 재사용이 용이 하다는 것을 다시 한번 느끼게 되었습니다.

카테고리:

업데이트:

댓글남기기