imhamburger 님의 블로그

데이터엔지니어 부트캠프 - MongoDB jwt토큰 decode 하여 user_id 불러오기 (22주차) 본문

데이터엔지니어 부트캠프

데이터엔지니어 부트캠프 - MongoDB jwt토큰 decode 하여 user_id 불러오기 (22주차)

imhamburger 2024. 12. 15. 16:38

지난글에서 user_id를 headers.get 하여 얻어오는 방식을 이용했었는데 그 부분을 바꿔야 한다.

이유는 우리 프로젝트는 로그인 기능을 jwt토큰을 이용하였기 때문이다.

 

여기서 jwt란 Json Web Token 으로 인증에 필요한 정보들을 Token에 담아 암호화시켜 사용하는 토큰이다.

jwt 토큰에는 Header / Payload / Signature 로 구성되어 있다. (jwt 토큰)

 

 

1. Header

헤더는 JWT의 타입과 서명에 사용된 알고리즘을 나타낸다.

{
  "typ": "JWT",
  "alg": "HS256"
}

 

 

2. Payload

페이로드는 JWT에 포함될 실제 데이터이다. 이 데이터에는 사용자 정보나 권한과 같은 정보가 들어갈 수 있다.

{
  "sub": "1234567890",
  "user_id": "test_user",
  "gender":"F",
  "iat": 1516239022
}

여기서 sub는 토큰에서 사용자에 대한 식별값, user_id는 사용자 ID, iat는 발급 시간을 의미한다.

이 부분은 서명되지 않기 때문에, 민감한 정보를 저장할 때는 암호화 또는 다른 보안 방법을 사용해야 한다.

 

 

3. Signature

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

서명은 JWT의 무결성을 확인하는 부분이다. 서버는 헤더와 페이로드를 인코딩한 후, 비밀 키를 사용해 해시 값을 생성하여 서명을 만든다. 클라이언트는 이 서명을 사용해 JWT가 변조되지 않았는지 확인할 수 있다.

 

 

JWT 프로세스

  • 사용자가 로그인하면, 서버는 사용자 정보를 기반으로 JWT를 생성하여 클라이언트에게 반환한다.
  • 클라이언트는 JWT를 저장하고, 이후 요청 시마다 이 토큰을 HTTP 헤더(보통 Authorization: Bearer <token>)에 포함시켜 서버에 보낸다.
  • 서버는 받은 JWT를 검증하여, 사용자가 유효한지 확인한다.

 

JWT 장점

  • 무상태성(Stateless): 서버는 클라이언트의 상태를 저장하지 않고, JWT에 모든 필요한 정보를 포함한다.
  • 확장성(Scalability): 토큰 기반 인증은 세션 상태를 저장하지 않기 때문에, 여러 서버에서 쉽게 확장할 수 있다.
  • 보안성(Security): 서명을 통해 JWT가 변조되지 않았는지 확인할 수 있다. 하지만 중요한 정보는 페이로드에 포함하지 않거나, 암호화해서 사용하는 것이 좋다.

 

다시 본론으로 돌아와서,

나의 경우, 로그인한 사용자들이 어떤 것을 검색했는지에 대해 분석하기 위한 로그를 만들어야 했다.

 

기존에는 다음과 같이 코드가 작성되어 있었다.

 

tickets.py (before)

current_timestamp = datetime.now().isoformat()
device = request.headers.get("User-Agent", "Unknown")
user_id = request.headers.get("user_id", "anonymous")  # user_id가 없으면 "anonymous"로 기본값 설정

try:
        log_event(
            current_timestamp=current_timestamp,
            user_id=user_id,  # 헤더에서 받은 user_id 사용
            device=device,     # 디바이스 정보 (User-Agent 또는 쿼리 파라미터)
            action="search",   # 액션 종류: 'Search'
            category=category, # 카테고리
            region=region,     # 지역
            keyword=keyword
            
    )
        print("Log event should have been recorded.")
    except Exception as e:
        print(f"Error logging event: {e}")

 

위 코드를 보면 user_id는 header에서 받아오는데 jwt토큰을 이용할 시 토큰으로 받아 decode해줘야 한다.

따라서 다음과 같이 변경해주었다.

 

 

tickets.py (after)

try:  
        token = request.headers.get('Authorization', 'anonymous')
        token = token.split('bearer ')[1]
        if token == '':
            log_event(
                user_id='anonymous',  # 헤더에서 받은 user_id 사용
                gender='unknown',
                birthday='unknown',
                device=device,     # 디바이스 정보 (User-Agent 또는 쿼리 파라미터)
                action="search",   # 액션 종류: 'Search'
                topic="search_log", #카프카 토픽 구별을 위한 컬럼
                category=category if category is not None else "None", # 카테고리
                region=region if region is not None else "None",
                keyword=keyword if keyword is not None else "None"
                
            )
            print("Anonymous Log event should have been recorded.")
        else:
            decoded_token = verify_token(
                token=token,
                SECRET_KEY=os.getenv("SECRET_KEY"),
                ALGORITHM=os.getenv("ALGORITHM"),
                refresh_token=None,
                expires_delta=None
                )
        
            user = await user_collection.find_one({"id":decoded_token["id"]},{'_id': 0})
            log_event(
                user_id=user["id"],  # 헤더에서 받은 user_id 사용
                gender=user['gender'],
                birthday=user['birthday'],
                device=device,     # 디바이스 정보 (User-Agent 또는 쿼리 파라미터)
                action="search",   # 액션 종류: 'Search'
                topic="search_log", #카프카 토픽 구별을 위한 컬럼
                category=category if category is not None else "None", # 카테고리
                region=region if region is not None else "None",
                keyword=keyword if keyword is not None else "None"
            )
            print(f"{user["id"]} Log event should have been recorded.")

    except Exception as e:
        print(f"Error logging event: {e}")

 

  • 로그 생성자 log_event()
  • 로그인한 사용자 혹은 그렇지않은 사용자 두 개로 분류하여 로그를 생성한다. 
  • 로그인한 사용자의 경우 decode 과정을 거치고 mongoDB user 컬렉션 안에 id와 일치한 데이터를 가져온다.
  • 일치한 user_id의 생년월일과 성별도 로그에 같이 넣어준다. ( 나중에 연령대별, 성별로 분석하기 위함이다.)

 

 

개선해야할 점

 

위의 방식은 로그인을 한 사용자의 경우, 매번 JWT를 디코딩하고 MongoDB에서 해당 사용자의 데이터를 가져오는 방식이다.

이는 시간이 걸릴 수 있으며, DB에 불필요한 부하를 줄 수 있다.

특히 많은 사용자가 동시에 로그인하고 데이터를 요청하는 경우, MongoDB의 성능에 영향을 미칠 수 있다.

이를 해결하기 위해 Redis를 활용하고자 한다.

Redis는 메모리 기반의 데이터 저장소로, 빠른 읽기/쓰기가 가능하여 DB의 부담을 줄이고 성능을 향상시킬 수 있다.

 

 

  • 로그인 시, JWT를 디코딩하고 해당 사용자의 정보를 MongoDB에서 가져온다.
  • 사용자 정보를 Redis에 저장: 사용자 정보를 Redis에 캐시하여, 이후 동일한 사용자가 요청할 때 MongoDB를 다시 조회하는 대신 Redis에서 빠르게 데이터를 가져온다.
  • 캐시 만료 시간 설정: Redis의 데이터를 일정 시간 후 만료되도록 설정하여, 오랜 시간 동안 사용하지 않은 데이터는 자동으로 제거되도록 한다.

 

 

나의 에러 리포트

 

 

에러 1) ObjectId' object is not iterable

ValueError: [TypeError("'ObjectId' object is not iterable"), TypeError('vars() argument must have __dict__ attribute')]

 

 

발생이유

key 필드 이름이 id 일 때, pymongo는 _id로 읽는 경우가 있다고 한다.

 

 

해결방법 

따라서, _id는 스킵하는 코드({'_id': 0})를 추가해서 해결하였다. (참고문서)

 

#before
user = await user_collection.find_one({"id":decoded_token["id"]}) 

#after
user = await user_collection.find_one({"id":decoded_token["id"]},{'_id': 0})

 

 

 

에러 2) AttributeError: module 'jwt' has no attribute 'encode'

File "/Users/seon-u/TU-tech/final_login/src/final_login/utils.py", line 27, in login
    access_token = create_access_token({"id": user["id"]}, expires_delta, SECRET_KEY, ALGORITHM)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/seon-u/TU-tech/final_login/src/final_login/auth.py", line 18, in create_access_token
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
           ^^^^^^^^^^
AttributeError: module 'jwt' has no attribute 'encode'

 

 

발생이유

PyJWT 와 python-jose 가 같이 설치되어 있을 시 충돌이 발생한 문제였다.

 

 

해결방법

python-jose를 사용할거라 PyJWT는 uninstall 해주었다.

pip uninstall PyJWT
pip install python-jose

 

 

 

일주일을 보내면서...

 

파이널 프로젝트를 시작한지 이제 한달정도? 되었다. 이제는 거의 끝자락?의 기능을 구현하면 되는데 그중 추천알고리즘이 가장 문제라고 생각한다.

멘토님의 조언을 받긴 했지만, 사실 머신러닝에 대한 이해가 부족하다 보니 그 알고리즘을 정확히 어떻게 구현해야 할지 감이 잘 잡히지 않았다. 수업 시간에 머신러닝을 제대로 다루지 않아서 이 부분이 정말 막막하고 불확실하게 느껴졌다.

일단, 만들어놓기는 했는데.... 과연 이게 메리트가 있을까 싶다.. 내가 100% 이해하고 만들었다면 의미가 있었겠지만 그렇지않아서 문제인 것 같다.

 

 

 

앞으로 나의 방향

 

다음주까지 모든 기능을 끝내고 다다음주부터는 발표자료를 만드는데 집중하고 싶다. 개발하면서 느끼지만 백엔드 부분이 많이 어렵다. 특히 로그인에서의 토큰...뭘 인증하고 디코드하고... 복잡하다.

API까지는 익숙해서 금방금방 하는 것 같지만... 그리고 기능이 돌아가는거에 그치지않고 성능을 어떻게 하면 더 좋게할 수 있을까도 고민이 많아진다. 지금은 시간에 쫓겨 만드는 것에 급급하지만 꼭 다시 복기하는 시간을 가져야겠다라는 생각이 든다.