개발 스토리

토큰을 헤더로 줘야할까 파라미터로 줘야할까?

타칸 2025. 3. 1. 23:40

배경

OAuth 기능을 구현하면서 로그인 플랫폼에서 받는 Access Token을 서버에 건네줄 때 2가지 방식으로 구현할 수 있었다.

  1. 헤더로 받는다.
  2. 파라미터로 받는다.

물론 그냥 아무거나 선택해서 구현해도 상관 없을 것 같다.

하지만 각 방식의 어떤 장단점이 있는지 고민해보았다.

본문

방법 1. 헤더로 받는다.

Copy
curl -v -G GET "http://{my서버}/auth/login" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}"

발급받은 Access Token을 헤더에 담아서 보내주는 방식이다. 클라이언트에서는 위와 같이 요청할 것이고

아마 서버에서는 아래와 같이 요청을 받게 될 것이다.

Copy
@GetMapping("/auth/login")
fun login(@RequestHeader("Authorization") authHeader: String) {
    if (authHeader.startsWith("Bearer ")) {
        val result = oAuthService.getTokenInfo(authHeader)
    }
}

[장점]

  • 보안성: 헤더에 담은 토큰은 URL에 노출되지 않아, 브라우저 히스토리나 서버 로그에 기록될 위험이 줄어든다.
  • 일관성: 많은 인증 API와 로그인 플랫폼이 동일한 형식(Authorization: Bearer …)을 사용하므로, 재사용 및 통합이 용이하다.

[단점]

  • 추가 파싱 로직 필요: 서버에서는 헤더에서 “Bearer “ 접두어를 확인하고 실제 토큰을 추출하는 추가 로직이 필요하다.

방법 2. 파라미터로 받는다.

Copy
curl -v -G GET "http://{my서버}/auth/login?accessToken={ACCESS_TOKEN}" \
Copy
@GetMapping("/auth/login")
fun login(@RequestParam("accessToken") accessToken: String) {
    val result = oAuthService.getTokenInfo(accessToken)
}

[장점]

  • 코드 간결성: 헤더에서 접두어를 제거하는 분기문 없이, 파라미터로 직접 토큰을 받아 처리할 수 있어 코드가 단순해질 수 있다.
  • 간편한 디버깅: URL에 토큰이 노출되므로, 요청을 확인하거나 디버깅할 때 쉽게 토큰 값을 확인할 수 있다.

[단점]

  • 보안 취약점: 토큰이 URL에 포함되면 브라우저 히스토리, 서버 로그, 프록시 캐시 등에 기록될 가능성이 있어 노출 위험이 증가한다.
  • 추가 작업: 내부적으로 Bearer을 추가해 주는 로직이 있어야 한다.
  • 복잡성 증가: 파라미터에 넣게 되면 매번 Access Token이 필요할 때마다 파라미터로 추가해줘야 한다.
  • POST 요청에서 파라미터로 전달은 일부 라이브러리나 프레임워크에서 사용 불가

고민

토큰을 파라미터로 넣든 헤더에 넣든 개발자 모드에서 요청 API를 보면 토큰을 확인할 수 있는 것은 동일하다. 따라서 보안상 헤더에 넣어다는 말은 그리 설득력이 있다고 느껴지지 않는다.

따라서 어차피 토큰을 노출시키는 것이 동일하다면 코드상 간결함의 이점을 챙겨갈 수 있는 파라미터 방식이 더 좋다고 느껴진다. 

 

하지만 파라미터로 사용하게 되면 매번 Access Token이 필요할 때마다 추가해줘야 한다. 즉, 클라이언트는 유저 정보가 필요한 API인지 아닌지를 확인하고 필요한 경우마다 Access Token을 추가해줘야 하는 번거로움이 있다. (URI도 항상 복잡해질 것이다.)

 

이러한 작업은 리소스가 매우 큰 작업이기에 헤더에 넣어두고 클라이언트는 이후에 모든 요청에 Access Token을 넣어주고, 서버에서 유저 정보가 필요한 작업인 경우 헤더에서 조회해서 사용하는 방식이 리소스가 더 작은 작업이라 판단된다. 따라서 우선 헤더를 통해 받는 방식을 채택했었다.

 

하지만 팀원(지원님)이 아래와 같은 의견을 주었다.

Authorization Server(카카오)에서 받은 토큰과 우리 서버에서 보내는 토큰을 받는 방식을 분리하자는 의견이다.

  • Authorization Access Token → 쿼리 파라미터
  • MyServer Access Token → 헤더

나는 클라이언트 입장에서 토큰을 보낼 때 한 가지 방식으로 통일해 줘야 더 편하다고 생각했었다.

하지만 오히려 혼란을 줄 수도 있을 것 같았다. 따라서 클라이언트인 안드로이드 팀원들한테 의견을 물어봤다.

당시 슬랙으로 물어봤었다.

 

안드로이드의 팀원의 의견은 딱히 어느 방법을 선호하는 것은 없지만 헤더로 주는 것을 가정하고 코드를 작성하셨다고 했다. 결국 어떤 방식으로 할지 결정은 내가 해야 했다.

 

POST에서도 파라미터로??

토큰을 파라미터로 사용하는 건 어떨지 구현을 하던 중 POST 요청에서도 토큰을 파라미터로 보내줘야 하는 건지 의문이 들었다.

 

기능적으로 POST 요청에서도 쿼리 파라미터로 전달은 가능하다.

그러면 POST 요청에서만 Body로 전달해야 할까??

위 글을 보면 다음 글이 나와있다.

Note 1: HTTP specification (1.1) does not state that query parameters and content are mutually exclusive for a HTTP server that accepts POST or PUT requests. So any server is free to accept both. I.e. if you write the server there’s nothing to stop you choosing to accept both (except maybe an inflexible framework).

HTTP/1.1 명세에서는 쿼리 문자열(Query Parameters)과 요청 본문(Request Body)이 서로 배타적(mutually exclusive)이어야 한다고 규정하지 않는다는 말이다.

 

명세상 강제하지 않기에 사용해도 문제가 없지 않을까 순간 생각했었다.

하지만 덭붙여서 일부 라이브러리나 프레임위크가 쿼리와 바디를 같이 사용하는 것을 제한할 수 있다고 말하고 있다.

그리고 이런 강제성이 실제 내가 사용하는 RestAssured에서 발생했다.

RestAssuered로 컨트롤러 테스트를 하고 있었는데 POST 요청에 쿼리 파라미터와 body를 같이 보내니 위와 같이 에러가 발생하면서 테스트가 실행되지 않았다.

 

POST에서만 헤더로??

그러면 쿼리 파라미터를 사용하기 어려운 POST 요청에서만 헤더로 토큰을 보내주고 나머지 요청에는 헤더로 보내주면 되는 걸까?

물론 가능은 하다. 하지만 이는 인지 비용이 매우 크다고 생각했다. HTTP 메서드에 따른 토큰 전달 방식의 차이는 인지 비용을 발생시키고 결국 휴먼에러로 이어져 버그를 유발할 수 있을 것 같았다.

 

 

 

결론

초기에 Access Token을 쿼리 파라미터로 전달하는 방식이 코드의 간결함 측면에서 더 적합하다고 판단했지만, 여러 가지 고려 사항을 검토한 끝에 헤더를 통해 전달하는 방식이 더 적절하다는 결론을 내렸다. 그 이유를 정리하면 다음과 같다.

1. 클라이언트의 번거로운 작업을 줄이기 위함

Authorization Server에서 받은 토큰과 저희 서버에서 받은 토큰의 전달 방식을 분리하게 되면 인지 비용이 발생할 것 같다/ 어떤 요청인지에 따라 토큰 전달 방식을 분리해줘야 하면 인지 비용이 발생할 것 같다고 생각했다.

이러한 방식은 코드의 일관성을 해치고, URI를 복잡하게 만든다. 반면, 헤더에 포함하면 클라이언트는 모든 요청에 Access Token을 자동으로 추가할 수 있어 구현이 단순해진다 즉, 리소스 소모가 적고 유지보수가 용이한 방식이다.

2. POST 요청에서의 쿼리 파라미터 사용 문제

쿼리 파라미터와 요청 본문(Request Body)은 HTTP/1.1 명세상 배타적인 관계가 아니므로 함께 사용할 수 있지만, 실질적으로는 문제가 발생할 수 있다.

일부 라이브러리(RestAssured)나 프레임워크는 POST 요청에서 쿼리 파라미터와 Body를 함께 허용하지 않는다. 실제로 테스트 코드 작성 시, POST 요청에서 쿼리 파라미터를 포함했을 때 에러가 발생했다. 즉, 일부 환경에서는 RESTful API의 일관성을 해치는 요소가 될 수 있다.

3. Authorization Server와의 일관된 방식 유지

처음에는 클라이언트 입장에서 하나의 방식으로 통일하는 것이 더 편리하다고 생각했다. 두 방식이 혼재되었을 때 발생할 수 있는 혼란을 방지할 수 있다는 점을 고려했다.