Swagger에서 Authorize Logout이 안되는 문제 해결 (feat. Session, CORS)

JUHYUN·2024년 11월 23일
0
post-thumbnail

🛑 문제 상황

swagger에서 authorize 기능을 활용해 인증이 필요한 API를 로컬에서 테스트하던 도중, 요청 헤더에 인증 헤더가 없음에도 불구하고 인증을 필요로 하는 요청이 처리되는 현상을 겪었습니다.

자세한 과정은 다음과 같습니다.
편의를 위해 인증을 필요로 하는 요청과 그렇지 않은 요청을 다음의 엔드포인트로 표현해보겠습니다.

  • 인증 O : /auth-required
  • 인증 X : /auth-free
  1. Swagger 처음 진입시 /auth-required 요청은 실패했습니다.
    auth-required-fail-before-authorize

  2. 이 후 Swagger의 Authorize를 통해 인증 토큰 정보를 입력한 후 /auth-required 요청은 성공했습니다.
    auth-required-success-after-authorize

  3. Swagger Authorize Logout 을 통해 토큰 정보를 제거한 후 /auth-required성공하였습니다.
    auth-required-success-after-logout

인증 정보가 없었기 때문에 1번과 같이 실패해야 할 것으로 예상되었는데 API가 정상적으로 처리되었습니다. 제어해볼 수 있는 환경에서 테스트하기 위해 React를 활용해 간단한 프론트 페이지를 만들어 재현해보았습니다.

😵 React 앱에서는 재현이 안된다고..?

하지만 예상과는 다르게 위의 1-2-3 같은 과정을 반복해보아도 인증 헤더가 없을 때는 /auth-required는 처리되지 않았습니다.

🏹 접근

저는 위의 상황을 서로 별개의 문제로 접근하였습니다.
왜냐하면 Swagger 상에서 테스트 시, Spring Context를 디버깅을 통해 확인해 본 결과 authentication이 Authorize Logout 이후에도 존재했기 때문입니다. 즉 Security Context가 Request 간의 공유가 되고 있었습니다.

따라서 다음과 같이 짧게 문제 정의를 하고 각각에 별개로 접근하였습니다.

  • 1번 문제 : Request간 SecurityContext 공유
  • 2번 문제 : Swagger와 React앱에서의 테스트 결과가 다른 이유

🧩 1번 문제 : Request간 SecurityContext 공유

단순히 'Logout 적용이 안된다' 보다 위와 같이 문제를 구체화하고 나니 해결방법은 찾기 훨씬 수월했습니다.
관련 키워드로 검색해본결과 stackoverflow에서 해결방법은 찾을 수 있었습니다.
Spring Security 설정에서 SessionCreationPolicySTATELESS 로 바꿔줌으로써 해결할 수 있었습니다.

🤔 왜 Session 이지?

여기서 의문이 하나 듭니다. 현재 이슈는 SecurityContext에 관련된 것인데 왜 Session이 다뤄지고 있을까요? Spring Security - Session Management 문서에서 그 이유를 이야기하고 있습니다.

By default, Spring Security stores the security context for you in the HTTP session.

바로 Spring Security는 기본적으로 HTTP Session에 SecurityContext를 저장하고 있기 때문입니다. 즉, 같은 Session을 사용하는 요청간에 SecurityContext가 공유될 수 있다는 뜻입니다.

📌 그렇다면 원래의 SessionCreationPolicy 설정은?

Security의 SessionCreationPolicy 기본 설정은 REQUIRED 입니다. 단어 뜻 그대로 필요할 때 만든다는 것인데요. 요청에 대한 세션객체가 없으면 만들고, 있으면 만들지 않는다는 것이죠.
따라서 REQUIRED 설정에서는 요청간의 Session을 재사용할 수 있기 때문에 Request 간의 Security Context가 공유되었고, 그로 인해 Swagger의 Authorize Logout이 동작하지 않았던 것입니다.

🧩 2번 문제 : Swagger와 React앱에서의 테스트 결과가 다른 이유

1번 문제를 통해서 SecurityContext 공유의 원인이 같은 Session의 사용임을 알았습니다. 그렇다면 React를 통한 테스트에서는 왜 같은 Session을 사용하지 않았을까요?
HTTP 통신은 기본적으로 무상태성을 지니고 있기 때문에 세션 정보를 서버에 보내기 위해 쿠키(Cookie) 를 활용합니다. 이에 기반하여 쿠키를 통한 세션 전송 여부를 Swagger와 React 두 환경에서 비교해보았습니다.

SwaggerReact

Swagger에서는 JSession 정보가 쿠키로 서버에 전달되고 있었지만 React에서는 쿠키 자체가 보내지지 않았습니다. 그렇다면 React 환경에서는 쿠키가 없었던 것일까요?

React - Cookie

React에도 쿠키에 JSessionID는 잘 저장되고 있었습니다. 그렇다면 무엇이 문제였을까요?

📌 Same Origin VS Cross Origin

바로 쿠키에 관한 브라우저의 Origin 정책 때문이었습니다.
같은 로컬환경에서의 테스트지만 Swagger와 React 사이에는 큰 차이점이 존재했는데요. 서버 입장에서 볼 때 Swagger는 Same Origin, React는 Cross Origin으로 인식된다는 점이었습니다.

Origin은 요청의 Scheme(프로토콜), Host(도메인), 그리고 포트 번호로 정의됩니다. Swagger와 React 모두 서버와 같은 Scheme, Host를 사용하고 있지만 Swagger는 서버와 같은 8080 포트를, React는 서버와 다른 3000 포트를 사용했기 때문에 Cross Origin으로 인식되었던 것입니다.

브라우저는 Cross Origin 에 대해서 기본적으로 쿠키를 전송하지 않습니다. 쿠키에는 JSessionID 와 같은 인증 정보나 민감한 정보가 있을 수 있어, Cross Origin에도 쿠키를 전송한다면 CSRF 와 같은 공격에 매우 취약해지기 때문입니다.
즉, React는 이러한 브라우저 정책에 걸려 쿠키를 보내지 않아 Swagger와는 다른 결과를 보였던 것입니다.

📝 Cross Origin에서도 쿠키를 보내려면?
서버에서 응답헤더에 Access-Control-Allow-Credentials: true 를 보내도록 설정합니다.

✨ What I learned...

단순한 기능 문제의 해결이었지만 이번 Trouble Shooting은 기초가 되는 Web 개발 지식과 Spring Security에 대해서 좀 더 확실하게 아는 계기가 되었습니다. 이번 경험을 통해 새롭게 추가된 개발 중 신경써야할 리스트 2가지를 적고 글을 마치겠습니다. ⭐

✅ 쿠키 전송 여부는 친절하게 알려주지 않는다. 통신 주체간 Same Origin 여부는 항상 신경쓰고 있자

✅ SecurityContext는 무조건 요청별로 달라지는 것이 아니다. SessionCreationPolicy 기본 값(REQUIRED)이 조건부 공유라는 점을 잘 알고 있자.


Ref

Spring Security - Session Management

profile
행복과 같은 속도를 찾는 개발자

2개의 댓글

comment-user-thumbnail
2024년 11월 26일

아주 흥미로운 글이군요 !

1개의 답글