Spring Security 와 Jwt
현재 진행중인 <내잔고를 부탁해> 프로젝트는 Spring Security 프레임워크를 토대로 하고 있으며, username/password 기반의 Form 로그인이 아닌 OAuth 2.0을 통해 로그인 처리를 수행한다. 또한, stateless 한 서버를 만들기 위해 Session 을 사용하지 않고 Jwt 를 발급/검증 하여 Authentication 을 하고 있다. 이는 단일 서버로 구동중인 본 프로젝트에서는 불필요한 방식이라고도 볼 수 있으나, 서비스가 커져서 여러개의 서버로 구성되는 상황을 염두에 둔 설계이다.
Spring Security + OAuth2.0 + JWT 는 최근 프로젝트에서 꽤 정형화된 패턴의 보안 설계로 보인다. 그런데 그만큼 프레임워크가 많은 부분을 커버하고 있을 줄 알았는데, 실제로는 개발자가 직접 구현을 해야하는 부분이 꽤 많았다. 특히 Jwt 부분은 전부 직접 로직을 짜야 한다.
이번 글에서는
- Jwt 를 핸들링 하는 법
- Jwt 를 다룰 때 고려해야할 보안 위협
을 다룬다.
아무래도 보안 부분은 많은 부분을 프레임워크에 의존하고 있기도 하고, 내가 직접 해킹을 하며 보안 취약점을 분석하는 입장이 아니기 때문에 휘발성이 강한 학습 영역으로 다가온다. 프론트엔드 쪽도 직접 했더라면 토큰 핸들링 과정에서 체감하는 것이 더 많았겠지만 서버 측면에서만 생각하다보니 어쩔 수 없이 깊이가 얕기도 하다. 종종 다시 보면서 상기하고자 최대한 자세히 + 종종 업데이트 하며 글을 유지하려고 한다.
1. Jwt 를 어떻게 클라이언트로 전달할 것인가?
Spring Security 는 Jwt를 이용한 인증 절차를 직접적으로 지원하지 않는다. (프레임워크가 특정 기술에 종속적인 것도 앞뒤가 안맞는 것 같긴 하다.) Spring Security 는 OAuth2.0 통신규약(RFC 5849)에 따라 로그인 처리를 해주는 역할까지만 수행하는데, 그 이후의 인증처리는 인증 객체를 매번 직접 넣어주든 세션을 발급하든 JWT 를 발급하든 모두 개발자의 책임이며 당연히 Jwt를 생성/검증 하고 핸들링 하는 것도 이에 해당한다.
JWT 은 OAuth 인증이 성공하여 SuccessHandler 필터(FilterChain에 직접 추가해야한다.) 에 도달하면 생성, 발급하게 된다. (이 단계는 OAuth 제공자에게 회원 정보를 제공 받고, DB에 있는 유저 데이터 또한 모두 로드 되어 Principal 이 생성된 이후의 단계이다.) 이 때의 서버 상태는 클라이언트에게서 토큰을 직접 요청을 받은 상태가 아니라 여기저기로 리다이렉트를 거친 상태인데, 절차상 발급된 Jwt 를 응답 Body 에 넘겨주게 되면 프론트엔드 쪽에서는 해당 토큰을 활용할 수 없게 된다.
이 때 백엔드에서 생성된 Jwt를 클라이언트 쪽으로 전달하는 것이 문제가 되는데, 여기서 선택할 수 있는 방법이 대략 세가지가 있다.
1) 리다이렉트(302) : Query Parameter 활용
우선 생성된 Jwt 를 원하는 프론트 페이지에 전달하기 위해 리다이렉트를 이용하면서 해당 응답에 정보를 실어 보내는 방법이다. 리다이렉트에서는 응답 바디가 무시되기 때문에 다른 방식으로 정보(토큰값)를 전달해야하는데 가장 간단하게 생각할 수 있는 방법이 바로 쿼리 스트링을 이용하는 방법이다.
예) http://URI/callback?accesstoken=1234
위 처럼 리다이렉트를 해주게 되면 프론트에서는 URL 에 담긴 토큰 값을 저장하여 활용할 수 있다. 하지만 이 방법은 아래와 같은 보안 위협 사항이 있기 때문에 그다지 추천되지 않는 방식이다.
Query String 을 통한 토큰 전달 시 보안 위협 사항 | |
1. 쿼리 파라미터의 가시성 | 쿼리 파라미터로 JWT를 전송하면 해당 JWT가 URL에 노출되기 때문에 URL을 공격자가 쉽게 볼 수 있습니다. 이로 인해 JWT의 내용이 노출되고 악용될 가능성이 높아집니다. |
2. 로그 기록 및 이력 | 쿼리 파라미터로 JWT를 전송하면 해당 JWT가 웹 서버의 로그에 기록될 수 있습니다. 이는 웹 서버나 중간 네트워크 장비에 노출될 가능성을 높여 보안상 취약점이 될 수 있습니다. 반면, 응답 바디에 포함시킨다면 로그에는 기록되지 않으므로 보안성이 향상됩니다. |
3. 캐싱 문제 | 쿼리 파라미터로 JWT를 사용하면 프록시 서버나 웹 브라우저에서 이를 캐시할 수 있습니다. 이로 인해 다른 사용자가 동일한 URL을 사용하여 동일한 JWT에 접근할 수 있게 되며, 보안상 문제가 발생할 수 있습니다. |
4. Referer(Referrer) 정보 노출 | 쿼리 파라미터로 JWT를 전송하면 브라우저나 웹 애플리케이션에서 이를 Referer 헤더에 함께 전송할 수 있습니다. 이는 다른 도메인이나 서드파티 사이트에 JWT가 노출되는 가능성을 높여 보안상 위험이 될 수 있습니다. |
(사실 1, 3 번은 이해가 가는데 2번은 서버 로그가 공격자에 노출이 될 수있는지 잘 모르겠고, 4번은 사실 무슨 말인지 잘 모르겠다.)
2) 정상응답(200) : Response Body 활용
앞서 말한대로 Spring Security 의 OAuth 기능을 온전히 활용할 때는 Jwt 응답 바디를 통해 프론트로 전달할 수가 없다. 이 때 Spring Security 의 프레임워크를 조금 다른 방식으로 이용하여 정상 응답을 활용할 수 있다.
원래 Spring Security 는 OAuth Provider 와 인가코드와 토큰을 주고 받는 과정을 프레임워크 자체적으로 수행하는데, 이 방식을 포기하고 직접 OAuth 통신 과정을 전부 구현하는 것이다. 이렇게 되면 인가코드를 서버의 특정 엔트리포인트로 리다이렉트 받지 않고 프론트엔드의 콜백 페이지로 보내줄 수 있다. 그러면 프론트의 콜백 페이지는 다시 백엔드 서버로 해당 코드와 함께 Jwt 를 요청하는 것이다. 이런식으로 프론트를 한번 경우하도록 만들면 정상 응답으로 Response Body 에 담겨져오는 Jwt 를 활용할 수 있다.
이 방법의 장점은 프론트에서 Jwt 를 핸들링하기 쉬워진다는 점, 위 쿼리 파라미터로 토큰을 전달할때의 보안 취약점이 어느정도 사라진다는 점이 있다. 그러나 이또한 인가코드를 전달받는 방식에서 쿼리 스트링을 이용하게 되고, 최종적으로 응답 바디에 토큰을 보내는 방식 또한 다른 방법에 비해 XSS 공격에 노출되어 있다고 생각한다.
사실 위의 이유보다는 개인적으로 Spring Security 가 만들어 놓은 OAuth 프레임 워크를 활용하지 않고 그걸 직접 구현한다는 데서 좀 찝찝함을 느낀다. 사실 프레임워크를 십분 이용하면서 응답 바디를 이용해 보고자 프론트로 리다이렉트 된 인가코드를 다시 서버의 엔트리포인트에 쏘는 방법도 고려해보았는데, 서버에서는 단순히 인가코드만 받는게 아니고 또 다른 객체를 같이 받고 있었다.(OAuth 를 하는 과정에서 발생하는 오류 및 응답 메세지 등을 담는 객체) 여튼 잘하는 사람들이 프레임 워크를 그렇게 짜놓은데는 다 이유가 있지 않을까... 하는 생각이 있다.
리다이렉트(302) : Cookie 활용
이 방법은 첫번째로 언급했던 리다이렉트 방식을 이용하되, 쿼리 파라미터 대신 쿠키 헤더를 통해 토큰을 전달하는 방식이다. 쿠키는 여러가지 보안설정을 할 수가 있어서 앞서 말한 보안 위협들 중 많은 부분을 보완할 수 있기 때문에 인터넷을 찾아보면 가장 선호되는 방식으로 꼽히는 것 같다. (나도 이 방식을 택했다.) 쿠키는 아래와 같은 보안 사항을 설정할 수 있다.
Cookie 의 보안 관련 설정 | |
1. Secure | Secure 쿠키는 HTTPS 연결을 통해서만 전송되고 접근할 수 있습니다. 이를 통해 중요한 정보가 안전하게 전송되고 저장됩니다. 웹 서버는 쿠키를 설정할 때 Secure 속성을 설정하여 이를 구현할 수 있습니다. |
2. HttpOnly | HttpOnly 쿠키는 JavaScript를 통해 접근할 수 없습니다. 이를 통해 쿠키를 사용하여 세션 정보와 같은 중요한 데이터가 악의적인 스크립트로부터 안전하게 보호됩니다. |
3. Same-Site | SameSite 쿠키 설정은 Cross-Site Request Forgery (CSRF) 공격을 방지하기 위한 것입니다. SameSite 설정은 쿠키가 어떤 사이트에서 요청되는지 제한하여 사이트 간 요청 위조 공격을 방지합니다. |
4. Domain, Path | 쿠키의 도메인 및 경로를 설정하여 어떤 페이지에서 쿠키에 액세스할 수 있는지를 제어할 수 있습니다. 이를 통해 쿠키의 범위를 제한할 수 있습니다. |
쿠키를 이용하면 위와 같은 보안 설정을 통해
- XSS 공격을 차단하고(HttpOnly)
- 암호화된 프로토콜을 이용하여 쿠키 탈취를 방지하며(Secure)
- 도메인을 동일하게 설정하여 CSRF 공격을 방어할 수 있다.(Same-Site)
결론
본 프로젝트에서는 Stateless 서버 구현을 위해 Jwt 를 활용하였다. 본 포스팅에서는 백엔드에서 생성된 Jwt 를 프론트 단으로 전송하기 위한 방법 세가지(리다이렉트 : 쿼리스트링 활용 / 정상 응답 : Response Body 활용 / 리다이렉트 : 쿠키 활용)를 다루어 보았다. 이 중 기본적인 보안 위협 사항인 XSS, CSRF 에 대응하여 가장 안전하다고 판단되는 리다이렉트 : 쿠키 방식을 선택했다.
전달 이후 프론트엔드에서의 Jwt 핸들링 또한 좋은 토픽이 될 것 같아서 추후 보완 하고 싶다. 또한, AccessToken 은 그 자체로 유효성 검증이 되니 stateless 한 서버구현이 가능하지만 RefreshToken을 활용하게 되면 DB를 사용하게 되고 세션인증과 동작방식에서 큰 차이가 없는 것 같은데 어떻게 jwt인증 방식이 stateless 인가? 라는 좀 더 근본적인 질문에 대해서도 조금 생각을 정리해보고 싶다.