카테고리 없음

[새싹 금천 5기] FAST JWT 인증 (이노그리드_기업맟춤형 클라우드 인재 양성 캠프)

z00h 2025. 5. 29. 17:07

 

 

✅ JWT 인증 시스템 구현

1. 세션 기반 인증 이해하기

  • 전통적인 세션 로그인 방식 설명
  • 분산 서버 환경에서의 한계

2. JWT 소개

  • JWT 구조 (Header, Payload, Signature)
  • Authorization 헤더와 Bearer 토큰
  • 로컬스토리지 vs 세션스토리지 차이

3. 암호화 방식 기초

  • 대칭키 vs 비대칭키
  • 해시(HASH), SALT, 무결성 개념

4. JWT 인증 기능 구현

  • auth/ 폴더 구조 생성
  • hash_password.py: 비밀번호 해싱 및 검증
  • jwt_handler.py: JWT 생성 및 검증 함수
  • .env: SECRET_KEY 설정

5. 사용자 인증 기능 적용

  • 회원가입 시 비밀번호 해싱 저장
  • 로그인 시 해시된 비밀번호 검증
  • 로그인 성공 시 JWT 액세스 토큰 생성 및 반환

6. JWT 인증 처리 로직 추가

  • authenticate.py: OAuth2 토큰 추출 및 검증
  • 인증된 사용자만 이벤트 등록 가능하도록 Depends 적용

7. JWT 인증 TEST

  • 토큰을 발급받은 후 토큰으로 이벤트 등록

 

 

전통 세션 기반 인증 시스템

인증 과정

  1. 사용자 이름(id), password전달
  2. sessionID 발행
  3. 세션 서버 객체를 찾아 사용자에게 맞는 서비스 제공

→ 서버에서 사용자 식별 정보가 포함된 세션ID를 유지, 관리

→ 서버가 분산되고 많아지면 세션 정보 공유의 문제가 발생한다.

문제점 : 서버 인스턴스가 여러대이면 세션들끼리 인스턴스를 공유하기가 어렵다.

문제점을 보완하기 위해 JWT 토큰을 사용한 인증 사용

JWT (JSON Web Token)

특징

  • 토큰 방행 후 사용자 정보를 일체 보관하고 관리하지 않는다.
  • 토큰 형식과 내용 검증만 처리한다.
  • 서버들 간에 검증 규칙만 공유하고 있으면 서버 개수, 위치에 관계 없이 동일하게 인증을 제공할 수 있다.

 

예전에는 쿠키에 저장하였으나 여러 제한사항이 있고 사용 데이터가 많아져 스토리지 공간에 토큰을 주로 저장한다.

 

 

로컬 스토리지

  • 브라우저를 껐다 켜도 만료시간이 남아있으면 계속 남아있음

세션 스토리지

  • 브라우저가 켜져있는동안 남아있는 스토리지

 

요청헤더를 통해 토큰 전달시 형식 내용을 검증(VALID) 후 그에 맞는 사용자에게 정보를 전달한다.

일반적으로 토큰은 Authorization: <type> <credentials> 요청 헤더를 통해서 전달한다.

Athorization 헤더 : JWT 인증 전달 헤더

Bearer : JWT 토큰이 전달되는 타입

JWT 구성

  • 헤더: 토큰 타입(typ)과 암호화 방법(alg)을 보관하는 곳으로 BASE64로 인코딩
  • 페이로드: 서버들이 공유하여야할 내용, 정보들
  • 크레임 : ‘이름, 값’ 한쌍을 크레임 이라고한다. 등록된(registered), 공개(public), 비공개(private) 크레임으로 구분된다.
  • registered claim
  • 시그니처: 헤더, 페이로드, 시크릿 키의 조합

 

 

암호화의 간단 개념

암호화 : 평문 → 암호문

복호화 : 암호화 → 평문

  • 분류방법 : key로 분류
  • 대칭키 : 암호화, 복호화 사용하는 key가 동일하다
  • 비대칭키(공개키 암호화방식) : 개인키, 공개키로 키쌍을 만든다.
  • HASH(Message Digest) : 임의 길이 값을 고정길이 유일한 값으로 표현, 역연산이 불가능하다 (단방향) → 생성 주체만 원문을 알고 있음

무결성 : 데이터의 상태를 원본 그대로 유지하는것

salt: 크래킹을 방지하기 위해 임의로 부여해주는 입력값

 

 

JWT 인증방식 적용하기

auth 폴더를 생성하고, 파일을 추가

(venv) c:\python\planner> mkdir auth

(venv) c:\python\planner> cd auth

(venv) c:\python\planner\auth> rem. > __init__.py

(venv) c:\python\planner\auth> rem. > jwt_handler.py ⇐ JWT 토큰 생성, 검증하는 기능

(venv) c:\python\planner\auth> rem. > authenticate.py ⇐ 인증 및 권한 관리 기능

(venv) c:\python\planner\auth> rem. > hash_password.py ⇐ 패스워드 암호화 및 검증 기능

passlib 라이브러리를 설치

  • 패스워드 암호화에 사용할 bcrypt 알고리즘 제공하는 라이브러리

jose 패키지 설치

  • jwt 인코딩/디코딩에 사용
  1. 패스워드 해싱 및 검증 기능 구현

auth\hash_password.py파일에 작성

생성자, bcrypt 해시 알고리즘을 사용하여 비밀번호를 해싱하는 클래스

from passlib.context import CryptContext


class HashPassword:
    def __init__(self): 
        self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

    def hash_password(self, password: str): 
        return self.pwd_context.hash(password)
   
    def verify_password(self, plain_password: str, hashed_password: str):
        return self.pwd_context.verify(plain_password, hashed_password)

사용자 등록시 패스워드 해싱후 db에 저장하도록 수정

 

 

 

routes\users.py

 

 

 

새로운 사용자 추가

curl -X POST http://localhost:8000/users/signup -H "Content-Type: application/json" -d "{ \"email\": \"go@test.com\", \"password\": \"password\", \"username\": \"고길동\" }" -v

로그를 보면 암호가 해쉬화되어 화면에 출력된 것을 볼 수 있다.

 

 

 

로그인 시 패스워드 해시를 검증하도록 수정

route/users.py

기존 로직에서 user Password를 평문 → 해시화 비교 로직으로 바꿔주었다.

 

 

 

 

 

.env 파일에 추가한 비밀키를 저장할 변수를 추가

해당 비밀키는 JWT 토큰 서명시 사용된다.

SECRET_KEY=SECRET+FASTAPI

 

 

JWT 토큰 생성 및 검증 기능 구현

auth\jwt_handler.py

 

주석 참고

from time import time
from fastapi import HTTPException, status
from jose import jwt
from database.connection import Settings




settings = Settings()


# JWT 토큰 생성
def create_jwt_token(email: str, user_id: int) -> str: # 토큰 생성(식별자 : email,user_id)
    payload = {"user": email, "user_id": user_id, "iat": time(), "exp": time() + 3600}  # iat : 발급시간, exp : 만료시간
    token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") # 설명 : payload+SECRET KEY를 HS256 알고리즘으로 암호화
    return token


# JWT 토큰 검증
def verify_jwt_token(token: str):
    try:
        payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) # 토큰 복호화
        exp = payload.get("exp") # 만료시간
        if exp is None:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid token")
        if time() > exp: # 현재시간이 만료시간보다 크면(유효시간 초과)
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Token expired")
        return payload # payload 리턴
    except:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid token")

 

 

 

 

로그인 성공시 액세스 토큰을 생성해서 반환

routes\users.py

기존 로그인 로직에서 반환값에 “acces_token” 추가, 매개변수로 email, id 전달

 

 

 

 

로그인 테스트

curl -X POST http://localhost:8000/users/signin -H "Content-Type: application/json" -d "{ \"email\": \"go@test.com\", \"password\": \"password\" }" -v

access_token 반환 값 확인

 

 

 

 

로그에 찍힌 acces_token 값 복사 → jwt.io 에서 붙여넣기 → 시그니처 영역 시크릿키 입력

검증 성공

 

 

 

JWT를 사용하여 사용자 인증후 인증된 사용자의 user_id를 반환하는 함수 정의

auth\authentication.py

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer


from auth.jwt_handler import verify_jwt_token


# 요청이 들어올 때 Authorization 헤더의 토큰 값을 추출
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/users/signin")


async def authenticate(token: str = Depends(oauth2_scheme)):
    if not token:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="액세스 토큰이 누락되었습니다.",
            headers={"WWW-Authenticate": "Bearer"},
        )
   
    payload = verify_jwt_token(token)
    return payload["user_id"]

 

 

 

 

인증된 사용자만 이벤트를 등록할 수 있도록 수정

routes\events.py

 

로직

등록 요청 함수 실행시 authenticate 호출

→ Depends (authenticate.py) oauth2_scheme 실행

→ Bearer 부분 뺀 토큰 값 추출 (OAuth3PasswordBearer)

→ 토큰 인증 (누락되면 403 ERROR 호출)

events.py

 

 

 

authenticate.py

 

 

 

클라이언트에서 서버로 전달하는 값은 사용자가 직접 입력하는 값으로만 제한해야함

→ ueserID는 토큰에서 id 추출하여 사용

data.user_id = user_id

 

이벤트 등록 테스트

회원가입

curl -X POST http://localhost:8000/users/signup -H "Content-Type: application/json" -d "{ \"email\": \"hong@test.com\", \"password\": \"password\", \"username\": \"홍길동\" }" -v

 

로그인

curl -X POST http://localhost:8000/users/signin -H "Content-Type: application/json" -d "{ \"email\": \"hong@test.com\", \"password\": \"password\" }" -v

 

Authorization 헤더 또는 헤더의 값 없이 이벤트 등록

curl -X POST http://localhost:8000/events/ -H "Content-Type: application/json" -d "{ \"title\": \"첫번째 타이틀\", \"image\": \"https://dummyimage.com/200x200/ccc/ff0.png?text=sample\\", \"description\": \"첫번째 샘플 이벤트 입니다.\", \"tags\": [\"python\", \"fastapi\", \"sample\"], \"location\": \"Google Doc\" }" -v

401 Unauthorized 반환

 

 

 

잘못된 형식의 토큰으로 이벤트 등록

curl -X POST http://localhost:8000/events/ -H "Content-Type: application/json" -d "{ \"title\": \"첫번째 타이틀\", \"image\": \"https://dummyimage.com/200x200/ccc/ff0.png?text=sample\", \"description\": \"첫번째 샘플 이벤트 입니다.\", \"tags\": [\"python\", \"fastapi\", \"sample\"], \"location\": \"Google Doc\" }" -v -H "Authorization: Bearer xyz"

400 Bad Request 반환

 

 

 

로그인시 발급받은 정상적인 토큰으로 게시글 등록 요청

curl -X POST http://localhost:8000/events/ -H "Content-Type: application/json" -d "{ \"title\": \"첫번째 타이틀\", \"image\": \"https://dummyimage.com/200x200/ccc/ff0.png?text=sample\\", \"description\": \"첫번째 샘플 이벤트 입니다.\", \"tags\": [\"python\", \"fastapi\", \"sample\"], \"location\": \"Google Doc\" }" -v -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiaG9uZ0B0ZXN0LmNvbSIsInVzZXJfaWQiOjEsImlhdCI6MTc0Nzg3NzE0Ni4zNjg0NzU0LCJleHAiOjE3NDc4ODA3NDYuMzY4NDc2fQ.KZElr4cyEiQ2gYhmUGjyORuPrxQFqJCmUehrjsqxG-8"

201 Created 반환

 

 

 

게시글 확인

curl -X GET http://localhost:8000/events/

발급받은 토큰으로 게시글 작성시 정상적으로 게시글이 등록된 것을 확인 할 수 있다