GPT 요약

My-Books 프로젝트에서 로그인 절차를 설계하며, 평문 비밀번호 전송 문제를 해결하기 위해 고민했습니다.
기존에 Back Server에서 비밀번호를 암호화하던 방식을 Front Server에서 해싱 후 전달하는 방식으로 변경해 보안성을 강화했습니다.
로그인 요청은 호출 빈도가 낮아 추가적인 서버 호출 비용보다 보안성을 우선시하는 선택이 이루어졌습니다.

My-Books 프로젝트에서의 경험을 다룬 글입니다.

My-Books 프로젝트에서 제가 맡은 역할은 인증/인가 프로세스를 설계하고 구현하는 것이었습니다.
특히 로그인 절차(인증)를 구현하면서 가장 큰 고민은 평문과 비문의 사용과 이를 어떻게 안전하게 처리할 것인가에 대한 것이었습니다.
이번 글에서는 이러한 고민의 과정과 최종적으로 내린 선택에 대해 공유하려 합니다.

BCrypt를 어디에 사용할 것인가?

로그인 설계에서 가장 중요한 고민 중 하나는 BCrypt를 어디에서 사용할 것인가 하는 점이었습니다.

초기 설계

초기에는 프론트에서 평문 비밀번호를 받아 이를 백으로 넘긴 후 BCrypt로 암호화하고, 데이터베이스와 비교해 검증하는 방식으로 설계했습니다.
이는 일반적으로 많이 사용하는 방법으로, 간단하고 직관적이라는 장점이 있었습니다.

최종 설계

하지만 최종적으로는 프론트에서 사용자가 입력한 이메일을 백으로 전달해 검증하고 DB에 저장돼있는 비문과 입력한 비밀번호를 BCrypt를 통해 검증한 뒤 로그인 절차를 완료하는 방법을 선택했습니다.
이로 인해 로그인 절차는 다소 복잡해졌고, 백서버 호출도 기존 1번에서 2번으로 증가하게 되었습니다.

BCrypt를 사용하기 위한 Dependency 변경

왜 변경했을까?

이건 정말 단순한 이유인데요, 저는 평문 비밀번호가 통신 과정에서 평문으로 전송되는 것 자체가 너무 찝찝했습니다.
물론 HTTPS를 사용하면 암호화된 통신을 보장할 수 있지만 비밀번호 같은 민감한 데이터는 단 한 번이라도 평문으로 노출될 가능성을 없애는 게 중요하다고 생각했습니다.

또한 로그인 요청은 사용자가 자주 호출하지 않는 기능입니다.
대부분 사용자가 서비스를 시작하거나, 토큰이 만료된 후 로그인을 수행하기 때문에 호출 빈도가 비교적 낮습니다.
따라서 로그인 절차에서 발생하는 추가적인 서버 호출 비용은 최소화된 부담이라고 판단했습니다.

비용 최적화를 추구할 수도 있었지만, 보안성을 강화하는 선택이 사용자의 민감한 정보를 다루는 인증 시스템의목표와 일치한다고 결론 내렸습니다

BCrypt 값 검증 방식 

또한 다음과 같은 질문도 받게 되었는데요 
Q: 프론트,백 둘 다 BCrypt를 사용하고 프론트에서 암호화 한 후 전송한다음에 백에서 확인하면 한번으로 처리가능한거 아니에요? 

A: BCrypt를 이용해 값의 일치를 확인하려면 평문하나와 비문 하나가 필요해서 안될것 같아요 

이 것을 이해하려면 BCrypt의 동작 구조를 알아야 하는데요 BCrypt는 단방향 해쉬 알고리즘으로 한번 암호화하면 본문을 알 수 없게됩니다. 
그런데... 원문을 알 수 없는데 어떻게 입력한 비밀번호와 DB의 암호화된 비밀번호가 같다는 것을 알 수 있을까요??

정답은 간단하게도 다시 암호화를 시킨후 비교합니다.

Bcrypt는 내부적으로 Salt(임의의 값)를 생성하여 평문 비밀번호와 함께 해싱 알고리즘을 적용합니다.
생성된 Salt는 해싱 결과(비문)에 포함되며, 검증 시 동일한 Salt를 추출해 평문 비밀번호와 함께 다시 해싱합니다.
이렇게 생성된 비문이 기존 비문과 같다면, 암호화되기 전의 평문이 동일했음을 확인할 수 있습니다.

따라서.. 들어온 평문과 암호화된 비문이 같음을 알기 위해서는 무조건 평문 하나와 비문 하나가 필요하기 때문에 
백서버에서 비문 비밀번호를 응답으로 받은 후 프론트에서 들어온 평문을 이용해 검증하는 방식을 사용해야 합니다.

My-Books 로그인 절차

최종적으로 설계된 My-Books 로그인 절차는 다음과 같은 흐름으로 진행됩니다.

  1. 이메일과 비밀번호 입력
    • 사용자가 이메일과 비밀번호를 입력하여 로그인을 시도합니다. 이 정보는 Front Server를 통해 Gateway Server로 전달
  2. 이메일 인증 요청
    • Gateway Server는 Back Server로 이메일 인증 요청을 전송
    • 이메일 인증 실패 시: 로그인 절차가 중단되고, 인증 실패 응답이 반환
    • 이메일 인증 성공 시: Back Server는 암호화된 비밀번호(BCrypt로 해싱된 값)를 Gateway Server를 통해 Front Server로 응답
  3. 비밀번호 검증
    • 사용자가 입력한 평문 비밀번호와 Back Server에서 전달받은 암호화된 비밀번호를 비교하여 검증
    • 비밀번호 검증 실패 시: 인증 실패 응답이 반환
    • 비밀번호 검증 성공 시: 로그인 절차 진행 , 마지막 로그인 시간 갱신 및 포인트 적립 요청
  4. 토큰 발급
    • 토큰 발급 후 로그인처리 완료

시퀀스 다이어그램을 통해서 보면 다음과 같습니다.(Resource Server는 Back Server와 동일)

로그인 시퀀스 다이어그램

추가적으로 같이 보시면 좋은 발표 영상입니다.

GPT 요약

My-Books 프로젝트에서는 Spring Gateway를 활용해 URL 기반 라우팅과 권한별 필터로 인가 로직을 설계했으나, 코드 중복과 비효율 문제가 발생했습니다.
SonarQube 피드백을 기반으로 중복 코드 문제를 해결하기 위해 유저와 어드민 필터를 통합하여 AuthFilter로 최적화했습니다.
사용자 상태 검토와 권한 검증을 효율적으로 처리하면서도 유지보수성과 확장성을 확보한 인가 시스템을 완성했습니다.

My-Books 프로젝트에서의 경험을 다룬 글입니다.

My-Books 프로젝트에서는 인증/인가 시스템 설계가 사용자 경험과 서비스 성능에 직결된다는 점을 알게 되었습니다.
이번 글에서는 Spring Gateway를 활용해 인가 로직을 최적화한 과정을 공유합니다.

초기 설계 - 단순 URL 기반 라우팅

처음에는 인증 서버와 백엔드 서버의 URL을 기반으로 라우팅을 구현했습니다.

  • 인증 서버: /auth/**
  • 백엔드 서버: /api/**

이 방식은 기본적인 요청 분리는 가능했지만, 모든 요청에 대해 인가 처리가 일어나는 비효율적인 구조였습니다.

이는 서비스 성능에 부정적인 영향을 끼쳤고, 이를 개선하기 위해 권한별 라우팅을 도입하기로 했습니다.

인가 처리 - 권한별 요청 구분

Spring Gateway의 RouteLocator를 활용해 요청을 권한별로 분리했습니다

  • 권한이 필요 없는 요청: /api/** , /auth/**
  • 유저 권한이 필요한 요청: /api/member/**
  • 어드민 권한이 필요한 요청: /api/admin/**

이제 권한이 필요 없는 요청은 인가 로직을 생략할 수 있어, 불필요한 리소스 낭비를 줄일 수 있었습니다.
또한 권한별 인가처리 세분화를 위해 유저 필터와 어드민 필터를 각각 구현했습니다.

  • 어드민 필터
    • 어드민 권한 확인.
    • 토큰의 유효성, 만료 여부, 조작 가능성 검토.
    • 요청 URL을 백엔드 서버의 REST API 형식에 맞게 변환.

  • 유저 필터
    • 유저 권한 확인.
    • 토큰의 유효성, 만료 여부, 조작 가능성 검토.
    • 요청 URL을 백엔드 서버의 REST API 형식에 맞게 변환.

다음과 같이 백엔드 API URL을 설정하는것도 생각해봤지만

  • /api/admin/**
  • /api/member/**

REST API형식에 어긋난다 생각하였기에 Gateway Filter에서 URL형식을 변경하는 방식을 선택했습니다.
또한 권한 계층을 두어 어드민 권한을 갖고 있을 시 모든 유저 기능을 사용 가능하게 구현하였습니다.

유저 상태 검토 - 왜 필요했을까?

권한에 따라 요청을 분리하고 인가 로직을 적용하던 중, 사용자 권한 외에도 사용자의 상태에 대해서도 고민하게 됐습니다.

이런 경험이 있지 않은가요?
오랜만에 사이트에 접속해 로그인을 하니 휴면계정으로 보호되어있다거나.. 로그인 시도가 계속시도되어 계정이 잠겼다던가..

저 또한 이런 경험이 있었기에 권한 뿐 아니라 상태에 대해서도 검증이 필요하다 생각했으며 이를 위해 유저 상태 검토 로직을 필터에 추가했습니다.

adminAuthFilter
userAuthFilter

그런데 Filter를 완성하고 나니 다음과 같은 피드백을 받게됩니다.

Q: SonarQube보니까 코드 중복도가 엄청 높던데 이거 통합하면 안되나요?

A: 역할이 다르기 때문에 분리해놓은 것이라 통합할 생각 없는데요?

그런데... 정말 그런가요??

중복되는 로직 개선 - 필터 통합

초기 설계에서는 userAuthFilteradminAuthFilter를 분리한 이유가 있었습니다.
권한별로 역할을 분리해두면 코드가 명확해지고, 확장성을 유지할 수 있다고 생각했기 때문입니다.

하지만, SonarQube를 활용해 코드 중복도를 분석한 결과 두 필터 간 중복 코드가 상당히 많았고, 이로 인해 코드 품질이 저하된다는 피드백을 받았습니다.

필터를 통합하는건 어렵진 않았습니다. 하지만 , "서로 역할이 달라보이는데 중복이 많다는 이유로 통합해야할까?" 라는 고민에 빠졌습니다.

역할별로 타입을 나누어 구현하는 가장 큰 이유는 확장성과 유지 보수성에 있다고 생각합니다.
하나의 객체가 다양한 역할을 수행하면 코드가 복잡해지고 확장성이 떨어지니까요, 그런데 이 경우는 어떤가요?

  • "인가 처리 로직이 자주 변경될 가능성이 있을까?"
  • "유저나 어드민 외에 새로운 권한이 추가될 가능성이 있을까?"
  • "활성, 잠금, 휴면 외에 추가적인 상태가 필요한 시나리오가 있을까?"

이 질문들을 깊이 고민해본 결과, 인가 처리 로직이 자주 변경될 가능성이 낮으며, 새로운 권한이나 상태가 추가될 가능성 또한 희박하다고 판단했습니다.

따라서, 유저와 어드민 필터를 하나의 필터로 통합해도 충분히 안정적이고 효율적인 구조를 유지할 수 있다는 결론에 도달했습니다.

그렇게 통합된 AuthFilter 코드입니다.

항상 느끼는 것이지만 여러 사람의 의견을 듣고 그 의견들을 깊이 고민하는 과정이 좋은 결과물을 만드는 것 같습니다.
이렇게 Gateway 인가 로직을 최적화하는 여정은 마무리됩니다.

같이 보시면 좋은 발표 영상입니다.

GPT 요약

My-Books는 웹 환경을 고려해 JWT 만료기간을 재설계했습니다.
리프래시 토큰을 3일에서 1시간으로 축소해 보안을 강화했습니다.
서비스 특성에 맞춘 유연한 설정이 중요합니다.

My-Books 프로젝트에서의 경험을 다룬 글입니다.

My-Books 프로젝트에서 제가 맡은 역할은 인증/인가 프로세스를 설계하고 구현하는 것이었습니다.
이번 글에서는 JWT 만료기간에 대해 고민했던 계기와, 기존 정책에서 변경된 정책까지의 과정을 공유하려 합니다.

JWT 만료기간의 중요성

My-Books 프로젝트를 설계하면서 깊게 고민한 부분 중 하나는 토큰의 만료기간 설정이었습니다.

JWT는 클라이언트와 서버 간 인증 정보를 주고받을 때 사용되는데, 만료기간 설정이 짧을수록 보안이 강화되는 반면, 사용자는 더 자주 로그인을 해야 하는 불편함을 겪게 됩니다.

따라서 보안과 사용자 경험 간의 Trade-Off를 적절히 설정하는 게 중요합니다.

기존의 JWT 만료기간

기존의 JWT 만료기간입니다.

  • 액세스 토큰 30분
  • 리프래시 토큰 3일

(Secret의 경우 Key Manager가 관리하는 값이기 때문에 걱정안하셔도 됩니다.)

My-Books 프로젝트 초기에 리프래시 토큰의 만료기간을 3일로 설정했을 때는 문제가 없다고 생각했습니다.

30분짜리 액세스 토큰이 만료되더라도 리프래시 토큰을 통해 새 액세스 토큰을 발급받는 방식은 흔히 사용하는 방법이었기 때문입니다.

하지만, PC방에서 My-Books 서비스에 접속해 친구에게 자랑하던 중 한 가지 생각이 스쳤습니다.
"내가 로그아웃하지 않고 그냥 나가면 계속 로그인된 상태로 유지되겠네?"

더욱이, My-Books에서는 액세스 토큰이 재발급될 때 리프래시 토큰도 함께 재발급되어 만료기간이 재설정되는 구조였기 때문에, 이 문제가 더욱 심각하게 느껴졌습니다.

만약 로그아웃하지 않은 상태로 공용 PC를 떠나면, 다음 사용자가 내 계정을 그대로 사용할 가능성이 존재했던 것입니다.

이 경험을 통해, 웹 환경에서 공용 PC(도서관, 학교, 공항 등) 사용 가능성이 크다는 점과 사용자가 로그아웃하지 않거나 토큰이 노출될 경우 심각한 보안 문제가 발생할 수 있음을 인지하게 되었습니다.

따라서.. 사용자 경험이 다소 저하되더라도 리프래시 토큰 만료기간을 축소해야 한다는 결론에 도달하게 되었습니다.

도메인을 생각한 JWT 만료기간 재설정

다음은 변경된 JWT 만료기간입니다.

  • 액세스 토큰 30분 (변경 없음)
  • 리프래시 토큰 1시간 (기존 3일에서 대폭 축소)

(Secret의 경우 Key Manager가 관리하는 값이기 때문에 걱정안하셔도 됩니다.)

"3일에서 1시간이라니, 너무 짧은 거 아닌가?"라는 생각이 들 수 있습니다. 하지만 이 결정은 도메인의 특성을 고려한 조치입니다.

쿠팡과 같은 종합몰 서비스의 체류 시간에 비해 서점과 같은 전문몰의 경우 체류 시간이 상대적으로 짧게 측정됩니다.

또한 대부분의 사용자가 어떤 책을 구매할 것인지를 특정 짓고 방문하며, 책의 구매가 빈번하지 않다고 판단했기 때문에 1시간 정도의 만료기간도 충분할 것으로 판단했습니다.

모바일 환경이였다면?

만약 My-Books가 모바일 환경을 기반으로 설계되었다면, 만료기간을 더 길게 설정했을 것입니다.
대부분의 사람들은 개인적으로 핸드폰을 사용하기 때문에, 공용으로 사용될 가능성이 매우 낮습니다.

카카오톡은 모바일 환경에서 매우 긴 만료시간을 설정하여 오랜 시간 로그인을 유지합니다.
덕분에 우리는 몇 일 동안 카카오톡에 접속하지 않아도 로그인 요청을 받지 않는 것이죠.

이처럼 서비스의 사용 환경과 특성에 따라 JWT 만료기간은 크게 달라질 수 있습니다.

이와 관련해 추가적인 링크 남기면서 마무리 하겠습니다.

 

GPT 요약

My-Books 프로젝트에서 초기 JWT 방식의 보안 취약점을 개선하기 위해 리프래시 토큰을 Redis에 저장하고 IP와 User-Agent 정보를 활용해 보안성을 강화했습니다.
토큰에는 UUID를 사용하고 Redis에 사용자 정보를 매핑해 추가적인 보안을 구현했습니다.
이를 통해 확장성과 보안성을 모두 충족하는 인증/인가 시스템을 설계했습니다.

My-Books 프로젝트에서의 경험을 다룬 글입니다.

My-Books 프로젝트에서 저의 주된 역할은 인증/인가 프로세스를 설계하고 구현하는 것이었습니다.
이번 글에는 JWT 안정성을 개선한 과정에 대해서 설명하려 합니다.

JWT의 안전성

이전 포스팅 'JWT는 왜 사용할까?'의 마지막 부분에서 다음과 같은 언급을 한 적이 있습니다.

 

JWT는 왜 사용할까?

GPT 요약My-Books 프로젝트에서 세션 불일치 문제를 해결하기 위해 JWT 인증을 도입했습니다.JWT는 서버 자원 없이 확장성이 높고 MSA에 적합합니다.사용 시 보안에 유의해야 합니다.My-Books 프로젝트

masiljangajji-coding.tistory.com

이번 글에는 JWT 안정성을 개선한 과정에 대해서 얘기하려 합니다.

초기의 JWT 사용

초기의 방법은 엑세스 토큰만 운용하는 방식이었습니다.
엑세스 토큰을 운용하여 토큰의 유효기간에 가까워 진다면(5~10분 이내) 엑세스 토큰을 refresh하는 요청을 주어 갱신해주는 것이죠

하지만 클라이언트가 쿠키로 내려진 토큰을 탈취당하는 경우에 이 방법은 보안이 매우 취약해집니다.
(공격자가 계속해서 토큰을 갱신하며 사용가능하기 때문에)

또한 매 요청마다 유효기간을 확인해 줘야 하기 때문에 불필요한 추가 연산이 필요해집니다.
이러한 방법을 해결하고자 다음과 같은 과정을 거치게됩니다.

JWT 안전성 개선 - 쿠키

처음에는 쿠키의 탈취를 어렵게 만드는 방법을 우선적으로 고려했습니다. 쿠키는 다양한 보안 옵션을 설정할 수 있으며, 제가 사용한 옵션은 다음과 같습니다.

  1. Secure - HTTPS에서만 쿠키전송
  2. HttpOnly - JavaScript로 쿠키 접근 불가 , XSS 방어
  3. SameSite - CSRF 방어를 위한 정책

이를 통해 클라이언트-서버 간 통신에서 쿠키 탈취를 어렵게 만들었습니다.
그러나 쿠키가 탈취된 이후에는 여전히 대응할 방법이 부족하다는 문제가 남았습니다.

JWT 안전성 개선 - 리프래시 토큰

여러 문서를 찾아본 결과 가장 많이 사용되는 방법이 액세스 토큰과 리프래시 토큰을 사용하는 것이었습니다.

짧은 유효기간을 가진 액세스 토큰과 긴 유효기간을 가진 리프래시 토큰을 두어 액세스 토큰 탈취 시에도 짧은 유효기간으로 인해 공격을 막을 수 있다는 것입니다.

또한 크게 2가지 운용방식이 존재했습니다.

  1. 액세스 토큰과 리프래시 토큰을 모두 쿠키로 내려주기
  2. 액세스 토큰은 쿠키로 리프래시 토큰은 Redis와 같은 별도의 서버 자원에 저장

액세스 토큰과 리프래시 토큰을 모두 쿠키로 내려주기의 경우에 서버의 자원을 사용하지 않기 때문에 JWT의 Stateless 한 특성을 잘 살릴 수 있지만 액세스 토큰(쿠키)가 탈취당한 경우를 가정한다면 똑같이 쿠키로 저장돼있는 리프래시 토큰은 멀쩡할까?라는 의구심이 들었습니다.

액세스 토큰은 쿠키로 리프래시 토큰은 Redis와 같은 별도의 서버에 저장의 경우에 탈취가 어려워진다는 장점이 있지만 서버에 데이터가 저장됨으로 Stateful 해진 다는 단점이 존재합니다.

저의 경우 액세스 토큰은 쿠키로 리프래시 토큰은 Redis와 같은 별도의 서버에 저장방법을 선택했는데요

  1. 부분적으로 Stateful 해지는 것은 맞으나 Stateless의 이점을 전부 사용 가능함
  2. Redis에 리프래시 토큰을 저장하고 부르는 로직은 자주 변경 및 호출되지 않는다
  3. JWT 일반 문자열이기 때문에 매우 많은 사용자를 커버할 수 있다

MSA 환경이나 Scale-Out으로 구성된 서버에서 확장성을 보장하는 것이 JWT를 사용하는 가장 큰 이유라 생각합니다.

리프래시 토큰을 각각의 서버에 저장시킨다면 이는 확장성에 제약이 생길 수 있지만 목적이 아예 분리된 Redis 서버 사용 시 Front와 Back 서버에 확장에 영향을 주지 않을 것이라 생각했습니다.

또한 리프래시 토큰을 저장하고 부르는 로직은 최초 로그인 요청 혹은 액세스 토큰은 유효하지만 유효기간이 만료돼 갱신하는 경우만 존재합니다.
이는 자주 변경되거나 호출될 일이 적음을 의미하고 별도의 서버를 둠으로써 버그 발생의 위험이나 복잡도가 크게 증가한다고 생각하지 않았습니다.

마지막으로 JWT는 결국 문자열 덩어리기 때문에 큰 자원 소모 없이 많은 사용자를 커버할 수 있다고 생각했습니다.

그렇게 해서 개선된 코드는 다음과 같습니다.

Redis에 저장된 리프래시 토큰을 액세스 토큰만으로 접근이 가능하다면 문제가 생길 수 있기 때문에 IP주소를 추가로 조합해 주었습니다.
또한 액세스 토큰과 리프래시 토큰을 일회용으로 사용하게끔 만들어 보안성을 높이려 했습니다.

RemoteAddr 이슈

그런데.. 이상한 일이 발생했습니다.
왜.. Redis에 저장되는 IP 주솟값이 다 똑같지?, 심지어 내 IP 주소도 아니잖아?

로그를 남겨 IP와 관련된 모든 값을 확인해 봤습니다.
그럼에도 문제점을 파악하지 못했기 때문에 Front 서버에서 직접 헤더에 정보를 넣어 보내줬습니다.

여기서 알게된 것은 사용자의 요청이 Front 서버를 거쳐 Auth 서버로 넘어오는 것이기 때문에
실제로 확인되는 IP 정보는 Front 서버의 IP라는 것입니다.

따라서 Front 서버에서 직접 IP 주소와 User-Agent 정보를 DTO 형태로 넘겨주게 변경했습니다.
(IP 주소뿐 아니라 User-Agent 정보를 이용해 보안성을 더욱 높였습니다.)

JWT 안전성 개선 - PayLoad

그런데 코드 리뷰 중 다음의 피드백을 받게 됩니다.

지금 user_id를 그대로 토큰에 기입하고 있는데 이거 그냥 보여줘도 돼?

처음에는 사용자에게 쿠키를 내려주는 것이기 때문에 디코딩 해서 내용을 까 볼 사람이 있을까? 정도로 생각했었지만 더 깊게 생각해 보니 이 정보를 기반으로 사용자의 정보나 API의 형태를 유추 가능해짐을 알았습니다.

또한 user_id 정보를 굳이 알려줄 이유도 없기 때문에 토큰에는 UUID를 기입하고 Redis에 user_id정보를 넣어줬습니다.

Key : UUID+IP+User-Agent , Value : user_id

최종적으로 구성된 코드입니다.

UUID를 기반으로 새로운 키를 생성해 Redis에 user_id를 기입하는 코드가 추가됐습니다.

또한 인증/인가 프로세스의 추가적인 개선 가능성을 모색했습니다. 정보를 추가적으로 저장하는 것에 비해 얻는 보안상에 이점을 더 갖고싶었기 때문입니다.

그 과정에서 기존 로그아웃 프로세스를 점검하던 중 로그아웃 이후에도 기존 액세스 토큰이 유효해 재사용이 가능하다는 문제또한 발견할 수 있었습니다.

JWT 안전성 개선 - UUID를 활용한 추가 보안 로직

기존의 GatewayFilter입니다.

토큰의 유효성과 사용자의 권한 및 상태를 확인하는 절차를 가집니다.

로그아웃시 사용자의 쿠키에 있는 정보는 지워지지만 그 정보를 기억하고 있다면 토큰 자체는 유효하기 때문에 Filter를 통과해 요청을 보낼 수 있게됩니다.

기존에는 이를 처리할 방법이 없었지만 추가된 UUID를 통해서 이제는 해결이 가능합니다.

  1. 사용자가 로그아웃하면, Redis에서 해당 UUID에 매핑된 정보를 즉시 삭제
  2. Gateway에서 Redis 상태를 조회하도록 로직 추가
  3. 이로 인해 이후의 액세스 토큰 검증 과정에서 UUID를 찾을 수 없게 되어 토큰을 Invalid로 판단
  4. Redis와 UUID를 활용한 상태 검증으로 로그아웃 이후 액세스 토큰 재사용을 완벽히 차단

인증서버에 로그아웃시 UUID정보를 삭제하는 로직 추가 후 

Gateway에도 UUID를 기반으로 유저를 찾는 로직을 추가해줬습니다.

그 후 Filter의 리펙토링 과정을 거치게되면... 

redisService의 isValidateUser를 이용해 액세스 토큰 재사용을 막은 후 토큰과 유저의 권한 및 상태를 확인하게 됩니다.

이 Filter를 기반으로 최종적으로는 다음과 같은 프로세스가 완성됩니다.



이렇게 단계별로 JWT의 안전성을 개선한 여정은 마무리됩니다.
같이 보시면 좋은 발표 영상입니다.

GPT 요약

My-Books 프로젝트에서 Spring Security를 검토했지만, MSA 환경에서의 인증정보 불일치와 자원 소모 문제로 사용하지 않았습니다.
대신, 간단한 인증/인가 요구사항을 충족하기 위해 Spring AOP를 활용해 효율적인 인가 처리를 설계했습니다.
이를 통해 사용자 경험을 개선하고 시스템 자원 소비를 최소화했습니다.

My-Books 프로젝트에서의 경험을 다룬 글입니다.

My-Books 프로젝트에서 저의 주된 역할은 인증/인가 프로세스를 설계하고 구현하는 것이었습니다.
처음에는 당연히 Spring Security를 기반으로 구현하려 했지만 결과적으로는 선택하지 않았는데요 그 과정을 얘기하려 합니다.

Spring Security란?

Spring Security는 Spring Framework 기반의 애플리케이션에서 인증(Authentication)과 인가(Authorization)를 손쉽게 구현할 수 있도록 제공되는 강력한 보안 프레임워크입니다.

사용자가 애플리케이션에 로그인하면, Authentication 객체가 생성되며 Authentication 객체는 Spring Security의 SecurityContext에 저장되어 애플리케이션 전반에서 사용자의 인증 상태를 관리합니다.

또한 Filter Chain을 통해 모든 HTTP 요청을 가로채고, 보안 검사를 수행합니다.

사용하지 않은 이유

Spring Framework에 완벽하게 호환되면서 쉽게 구현이 가능하다니.. 사용하지 않을 이유가 없어 보입니다.
하지만 제가 마음에 걸렸던 건 Authentication 객체가 SecurityContext에저장된다는 것입니다.

Security Context는 기본적으로 세션에 저장하여 상태를 유지하기 때문에 MSA로 구성된 My-Books 프로젝트에서는 인증정보 불일치 문제가 발생 가능합니다.

이 문제는 SessionCreationPolicyStateless하게 설정하면 해결이 가능하지만
세션을 사용하지 않을 뿐 매 요청마다 SecurityContext를 생성하기 때문에 추가적인 자원 소비가 이뤄지게 됩니다.

만약 세분화된 접근 제어가 필요하다면 요청마다 고정적인 자원 소비가 발생함에도 불구하고 Spring Security를 사용했겠지만
단순히 책을 사고파는 온라인 서점인 My-Books에서는 불필요하다고 생각했습니다.

Filter vs Interceptor vs AOP

Spring Security의 대안으로 생각한 것은 Filter, Interceptor, AOP였습니다.
위에서 언급한 것들은 모두 횡단 관심사를 처리할 수 있는 수단으로, 인가 처리에 사용 가능합니다.

이들은 비슷해 보이지만 차이점이 존재했는데요. Filter는 Spring 스펙이 아닌 Servlet의 스펙으로, DispatcherServlet 이전에 실행되어 모든 HTTP 요청을 대상으로 작동합니다. 그렇기 때문에 Filter는 Spring 컨테이너와 무관하게 작동하므로, Spring Bean이나 컨트롤러 또는 서비스 계층의 로직과 연동된 처리는 어렵습니다.

Interceptor는 Spring MVC에서 제공하는 기능으로, 컨트롤러 실행 전후에 추가 로직을 삽입하는 데 사용됩니다.
Spring MVC 컨트롤러 수준에서 동작하기 때문에 Filter보다 Spring Application과 밀접하게 협력한다는 장점은 있지만, 컨트롤러보다 추상화 정도가 낮은(실제 비즈니스 로직이 존재하는) 서비스 계층에서의 상세한 처리는 불가능합니다.

또한, Filter와 Interceptor 모두 요청 전과 후에 대한 처리를 지원하지만, 이를 하나의 메서드에서 전후를 동시에 처리하는 방법은 없기 때문에 AOP를 선택하게 됐습니다.

내가 사용한 Spring AOP

현재 My-Books는 Front Server에서 사용자 요청 시 Gateway Server로 전달되어 인가 처리를 하는 구조를 가집니다.

또한 인가 처리 실패 시 에러가 발생하고 Front Server는 그에 따른 알맞은 응답을 반환해 사용자의 사용성을 높이는 목표가 존재했습니다.따라서 호출 후 에러발생에 따른 추가적인 로직 실행이 가능해야 됐으므로 Spring AOP의 @Around를 사용하기로 결정했습니다.

인가 처리가 실패하는 경우에 따라서 액세스 토큰을 재발급하거나 특정 페이지로 보내는 등의 작업을 상세 설계하여
사용자의 사용성을 높일 수 있었습니다.

이와 관련해 추가적인 링크 남기면서 마무리 하겠습니다.

GPT 요약

My-Books 프로젝트에서 세션 불일치 문제를 해결하기 위해 JWT 인증을 도입했습니다.
JWT는 서버 자원 없이 확장성이 높고 MSA에 적합합니다.
사용 시 보안에 유의해야 합니다.

My-Books 프로젝트에서의 경험을 다룬 글입니다

기존에 진행했던 토이 프로젝트에서는 Cookie-Session 기반으로 인증/인가를 구현했기에, My-Books에서도 동일한 방식을 적용하려 했지만 MSA 환경이라는 특수성 때문에 다음과 같은 문제에 직면했습니다.

세션 불일치 문제

Cookie-Session 방식은 세션에 유저 정보를 담아두고 쿠키에 세션 id를 기입하는 방식입니다.
쿠키의 경우 사용자가 HTTP 요청 시 자동으로 헤더에 담겨 서버로 전송되기 때문에 간단하고 직관적으로 구현이 가능하며 세션 데이터는 서버에 저장되므로 보안성도 챙긴 방식입니다.

여기서 중요한 건 세션 데이터는 서버에 저장된다는 것입니다. 세션 자체가 서버 자원의 일부이기 때문에 서버에 종속적임을 의미하고
이는 MSA 환경 혹은 Sacle-Out으로 서버가 구성돼있는 경우에 서버 간 세션 불일치 문제가 발생합니다.

사용자가 A 서버에서 로그인을 한 후 B 서버로 라우팅 된다면 세션에 유저 정보가 존재하지 않기 때문에
다시 로그인을 하라는 요청을 보낼 것입니다.

이는 사용자의 경험을 저하시키고 서버 측에서는 불필요한 데이터를 중복으로 갖게 되는 문제를 발생시킵니다.
또한 사용자의 트래픽이 몰려 요청을 분산시켜야 하는 경우에 확장성에 문제또한 발생합니다.

세션 불일치

이러한 문제는 세션을 모아주는 Session Clustering, 혹은 동일한 서버로만 요청을 주고받는 Sticky Session으로 해결이 가능하지만
추가적인 작업이 필요해 복잡도가 올라가기 때문에 또 다른 제약이 생기게 됩니다.

Session  Clustering
Sticky Session


해결법

따라서 저는 JWT를 기반으로 한 인증/인가 방식을 도입했습니다.
JWT는 Json Web Token의 약자로 Json 포맷의 데이터를 인코딩하여 사용하는 토큰입니다.

JWT 구조

방식은 Cookie-Session과 동일합니다.

  1. 사용자가 로그인 시 JWT 토큰을 발급
  2. 쿠키로 JWT 내려줌
  3. 사용자의 요청에 JWT 정보를 헤더에 담아 보냄

여기서 차이점은 JWT는 그 자체로 사용자의 상태와 권한 정보를 포함하고 있다는 것입니다.

JWT를 디코딩시 유저정보가 나오는 모습

서버의 자원인 세션을 사용할 필요가 없으며 각각의 서버에 JWT를 검증하고 읽어내는 코드만 추가되면 됩니다.

오픈소스인 auth0 사용 간단한 구현

서버에 종속되지 않기 때문에 자유롭게 Scale-Out 하거나 Load Balancing을 통해 요청을 분산시켜도 문제가 발생하지 않습니다.
또한 앞서 설명한 Session Clustering과 Sticky Session보다 훨씬 낮은 복잡도를 가지며 직관적입니다.

만약 모든 요청이 몰리는 Gateway 서버를 둔다면 JWT 관련 코드를 백서버와 아예 분리시켜 관리가 가능하며
My-Books에서도 동일한 방법을 사용 중입니다.

My-Books프로젝트 아키텍처

이외에도 서버의 자원인 세션을 사용하지 않기 때문에 서버의 자원을 아낄 수 있다는 이점도 존재합니다.

다만 주의할 것은 JWT는 그 자체로 암호화된 무언가가 아니라 그냥 인코딩한 문자열이라는 것입니다.
따라서 사용 시 고려해야 할 사항이 많으며 이와 관련해 추가적인 링크를 남깁니다.

JWT 안전성 개선기

 

+ Recent posts