본문 바로가기

프로젝트

우리 프로젝트는 왜 세션을 도입하게 되었는가?

지스트 청원 프로젝트에서 유저 인증 방식을 도입하는 데 있어 세션으로 결정한 과정을 작성해보고자 한다. 팀 내 발표를 정리한 글이다.

 

인증 방식

HTTP의 비연결성, 무상태성의 특징으로, 사용자의 로그인 상태를 유지하는 별도의 방법이 필요하다. 대표적인 방법에 세션 방식과 토큰 방식이 있다.

세션 방식

 

 

세션 방식은 서버 측에 로그인 정보를 저장하는 방식이다.

 

1. 어떤 정보를 서버 측 세션에 저장할 것인가

httpSession.setAttribute(SESSION_KEY, new SimpleUser(user));

key-value의 형식으로 세션에 정보를 저장할 수 있다. 이때, value에 저장되는 정보는 로그인한 유저를 식별할 수 있어야 한다. 추가적으로 우리 팀은 UserRole에 대한 정보를 추가함으로써 권한 검증을 위한 DB 접근을 최소화하려 했다. SimpleUser에는 해당 유저의 id와 UserRole정보를 포함한다.

2. 세션ID를 어떻게 주고받을 것인가.

Spring Servlet에서 기본적으로 제공해주는 기능을 사용하게 되면, 간단하게 세션 기능을 구현할 수 있다. 세션이 생성되었을 때, Set-Cookie: JSESSIONID={랜덤 값}를 응답에 담아 보내면, 클라이언트의 쿠키에 자동으로 저장되고 이후 요청에 함께 전달된다. XSS 공격으로부터 안전하기 위해 script가 세션ID를 읽지 못하게 하기 위해 HttpOnly, Secure를 Cookie의 옵션을 추가하는 것이 좋다 생각한다.

 

물론 다른 방식으로 세션ID를 주고받을 수 있지만, 쿠키를 활용하는 방식이 구현하기도 쉽고 세션 ID를 주고받기 가장 적합한 방법이라 생각한다. 토큰 부분에서는 여러 방식을 비교해보는데, 이를 세션에도 동일하게 고민해보는 것도 좋을 것 같다.

세션 방식 고려해야 할 점

  1. 서버의 메모리 관리
    세션을 생성한다는 것은 메모리에 적재하는 것이기 때문에, OOM이나 성능적인 이슈를 야기할 수 있다. 모니터링이 필수적일 것이라 생각된다.
  2. 서버의 다중화
    기본적으로 세션 저장소는 각각의 서버별로 메모리에 적재되는데, 서비스가 여러 서버로 운영이 될 경우 문제가 발생한다. 유저는 로그인을 진행한 서버에서만 로그인이 되어 있고, 다른 서버에서는 로그인 정보를 찾을 수 없게 된다. 통합 세션 저장소를 이용하거나, 유저는 같은 서버에만 요청을 보낼 수 있도록 해야 한다.
  3. 서비스의 다양화
    쿠키는 기본적으로 도메인별로 저장되고 요청을 보낼 때 모두 함께 전달된다. 만약 여러 서비스가 각각의 도메인을 가지고 동작한다면, 쿠키로 전달하는 방식을 사용하기 위해서는 각각 도메인 별로 인증을 진행해야 할 것이다.

토큰 방식

 

토큰 방식은 로그인을 했을 때 토큰을 전달받고, 이후의 요청에서는 토큰 정보를 함께 전달하는 방식의 인증이다.

 

토큰 방식은 세션 방식과는 다르게 여러 가지 결정할 부분들이 있다. 이 내용들을 하나하나씩 분석하고자 한다.

1. 어떤 정보를 토큰에 담을 것인가

2. 로그인 시 응답의 어디에 토큰을 담아 전달할 것인가

3. 클라이언트는 어디에 토큰을 저장할 것인가

4. 이후 요청 어디에 토큰을 담아 전달할 것인가

 

우리 서비스는 학교 계정이 인증된 사용자에 대해서만 제공하는 서비스이기 때문에, 써드파티 인증인 Oauth2.0 방식을 사용하지 않고 JWT를 사용한다.

 

1. 어떤 정보를 토큰에 담을 것인가

JWT 토큰은 토큰 전체를 암호화하지 않고 base64 인코딩만을 진행한다. 즉, 누구나 토큰을 획득하면 그 내용을 확인할 수 있다. 때문에, 패스워드와 같이 공개되었을 때 위험한 정보들을 담지 않는 것이 좋다. (이와 관련해서 JWT 토큰의 인증 방식을 공부해 보는 것을 추천한다.)

토큰은 해당 정보를 통해 유저를 식별할 수 있는 정보를 포함해야 한다. 우리 서비스에서는 사용자의 pk값 또는 이메일 정도가 되겠다.

추가로, 토큰으로부터 DB 접근을 최소화할 수 있는 방법 또한 고려해봐야 한다. 유저의 권한 정보가 거의 대부분 요청에 필요한데, 권한을 토큰에 추가하면 DB 접근 없이도 작업을 진행할 수 있다.

-> 이메일유저 권한 정도를 토큰에 저장하면 좋다고 생각한다.

2. 로그인 시 응답의 어디에 토큰을 담아 전달할 것인가

  1. ResponseBody (json)
  2. Set-Cookie : accessToken={토큰 값}
  3. 별도의 Custom Header

클라이언트가 쿠키로 저장하는 경우에는 Set-Cookie방식, 그렇지 않은 경우에는 ResponseBody가 적합하다고 생각한다. 별도의 Custom Header를 작성했을 때 가질 수 있는 장점은 없다 생각한다.

3. 클라이언트는 어디에 토큰을 저장할 것인가

  1. LocalStorage
  2. SessionStorage
  3. Cookie (+HttpOnly, Secure)
  4. JavaScript 메모리 (로컬 변수)

여기서 중요하게 여긴 부분은 XSS 공격으로부터 안전하기 위해 script가 토큰을 읽지 못하게 하는 것이었다. 이를 만족하는 방법은 HttpOnly로 설정된 Cookie와 JavaScript 메모리 정도가 가능하다. 

4. 이후 요청 어디에 토큰을 담아 전달할 것인가

  1. RequestBody
  2. ReqeuestParam => ex) url?accessToken=asdasdqawqwasdasd
  3. Cookie
  4. Authorization 헤더

메타데이터의 성격의 값이기 때문에 ReqeustBody나 RequestParam에는 적합하지 않다고 생각한다. 인증의 역할을 위한 별도로 존재하는 Authorizaion 헤더를 이용하는 것이 바람직하다 생각하지만, HttpOnly Cookie에 경우에는 script에서 직접 쿠키에 접근이 불가하기 때문에 Cookie로 전달하면 좋을 듯하다.


Access 토큰과 Refresh 토큰

갑자기 Access 토큰과 Refresh 토큰이라는 주제가 나와 당황스럽겠지만, 각 토큰의 특성마다 위의 의사결정이 결과가 달라지기 때문에 가져왔다.

보안적인 측면에서는 토큰의 만료 시점을 빠르게 둘수록 안전하지만, 사용자의 경험 측면에서는 매번 로그인을 통해 새로 토큰을 발급받는다면 큰 불편함을 느낄 것이다. 이 부분을 해결하기 위해 만료 시간을 짧게 설정한 Access 토큰과 이를 갱신하기 위해 별도의 Refresh 토큰을 두는 방식으로 문제를 해결한다. 그럼 각 토큰은 위의 1~4번에 어떤 결정을 내릴 수 있을까?

 

1. 어떤 정보를 토큰에 담을 것인가

Access 토큰은 사용자의 식별에 사용되기 때문에 위의 설명처럼 '이메일'과 '유저 권한'정도가 적당하다 판단했다.

Refresh 토큰은 Access 토큰을 생성할 수 있어야 하기 때문에, Access 토큰의 정보들을 가지고 있어야 한다고 생각한다. (이 부분은 유추한 부분이다.) 1~2주 정도 Refresh토큰 기한을 두기 때문에 DB에 저장하여 관리하는 것이 좋다고 생각하며,  패스워드와 같은 원리로 암호화 후 저장하여야 한다고 생각한다.

2.3.4. 토큰 전달 정송 방식

Access토큰은 Refresh토큰이 존재하기 때문에, 항상 저장되어야 할 필요성이 없다. 그렇기에 Javascript 메모리 변수에 저장해도 충분하다 생각한다. 쿠키를 사용하지 않기 때문에, ResponseBody를 통해 토큰을 전달받고 Authorizaion 헤더를 통해 이후의 요청들을 진행하면 된다.

Refresh토큰은 저장되어 있어야 하는 동시에 탈취(대표적으로 XSS 공격)로부터 안전해야 한다.  이에 최적에 저장 장소는 HttpOnly Cookie라 판단했다. 자연스레 Set-Cookie를 통해 토큰을 전달받고, Access 토큰 갱신 요청을 보낼 때에도 Cookie를 통해 토큰을 전달할 수 있다.


그래서 우리 서비스에는 어떤 방식을?

세션에 비해 토큰에 대해 길게 설명을 이어나갔지만, 우리 팀은 세션으로 진행하기로 결정했다. 이유는 아래와 같다.

1. 여러 서버나 서비스를 띄울 것인가? - No

세션과 토큰을 비교할 때 가장 비교되는 부분은 확장성이다. 토큰은 암호키만 공유하고 있다면 무한정 확장 가능하지만 세션은 그렇지 않다.

하지만, 우리 서비스가 다양한 서비스로 뻗어 나갈 것이라 생각하지 않는다. 오히려, 학생들의 의견을 모으고 답변을 받는 그런 독립적인 사이트로 남았으면 하는 개인적인 바람도 있다. 확장성이 큰 의미가 없는 서비스에서 토큰의 사용은 옳은가에 대한 의문이 남는다.

서버의 다중화 측면에서는, 만약 사용자 수가 많아져 서버 메모리가 터진다면 기쁜 마음으로 통합 세션 저장소를 붙이는 작업을 하면 된다.

2. DB의 조회가 매번 일어나지는 않는가?

지난 프로젝트에서 토큰을 이용하여 인증을 진행했는데, 이때 대부분의 API콜이 토큰 정보로부터 DB에 접근해 사용자 정보를 불러왔다. "어차피 DB를 찌르는 경우라면, 메모리에 유저 정보가 저장되는 세션이 훨씬 효율적이지 않을까?"라는 생각이 들었다. 이외에도 별도의 암호화 검증이 과정이 매 요청마다 진행되는 것도 무시할 수는 없다고 생각한다. 이 부분에 대해 조금 더 다루는 다음 글을 읽어보는 것도 추천한다.  Why JWTs Suck as Session Tokens

 


세션 방식을 도입한 우리 서비스가 앞으로 고민해야 할 부분

1. 글을 작성하는 도중에 어떻게 세션이 만료되지 않게 할 것인가?

많은 글이나 지원서 작성 플랫폼 같은 경우, 직접 세션을 연장하도록 구현한 것을 자주 본다. 사용자에게는 조금 불편한 방식이지만 그 방법을 유지하는 것이 좋을지, 아니면 또 다른 방법이 있을지 팀원들과 함께 고민해볼 예정이다.

2. 좋은 사용자 경험을 위한 로그인 유지 기능의 도입

Refresh 토큰과 마찬가지로 로그인 유지 기능을 도입해 사용자의 경험을 높이고 싶다. Spring Security에 remember-me 방식을 참고하여 진행해 볼 수 있을 것으로 예상된다.

 


정리

프로젝트의 인증 과정을 결정하는 데 있어 개인적으로, 그리고 팀적으로 고민한 부분들에 대해 정리해봤다. 부족한 식견에 고려하지 못한 부분도 많이 있겠지만, 팀원들과 함께 지금의 방식을 잘 적용해 보려 한다. 마찬가지로 팀 프로젝트에서 인증 방법에 대해 고민하는 분들에게 도움이 되었으면 한다.

 

참고 문헌

- [JWT] 토큰(Token) 기반 인증에 대한 소개

- 백엔드가 이정도는 해줘야 함 - 5. 사용자 인증 방식 결정