Post

웹 인증, 세션과 쿠키

웹 인증, 세션과 쿠키

들어가며

프로젝트 진행 중 별도 Web API 프로젝트에서 관리자 권한을 체크한 뒤, 다른 Web 프로젝트에서 관리자 요청들을 필터링해서 수행하고 이 권한에 따라 클라이언트 측에서도 화면 구분이 필요한 상황에 마주했다.

권한 데이터를 어떻게 저장하고 관리할지 고민하던 중, /{Area}/{Controller}/{Action} 같은 URL 형태로 수백 개의 권한을 관리해야했다.

이걸 어디에 저장해야 하지? 라는 고민부터 어떻게 관리할지 고민이 됐다.

처음에는 단순하게 생각했다. 사용자가 로그인하면 권한 목록을 Claim에 넣어서 관리하면 되지 않을까? 하지만 곧 문제를 발견했다. 권한 URL 하나가 약 4~50 bytes, 300개면 12~15KB다. 일반적인 브라주어 쿠키 제한인 4KB를 생각하면 훨씬 초과하는 수치다.

LocalStorage에 넣으면 크기 문제는 해결되지만 JavaScript로 접근 가능하다는 보안 위험이 있다.

결국 세션 을 선택했다. 이 글에서는 어떤 사고 과정을 거쳐 이 결론에 도달했는지 정리하고자 한다.


Claim이란

먼저 Claim이 무엇인지 명확히 해야 한다. Claim ≠ 쿠키다.

Claim은 사용자 정보의 한 조각일 뿐이다. “이름”, “이메일”, “역할” 같은 key-value 데이터 구조다.

1
2
3
4
5
6
7
8
var claims = new List<Claim>
{
    new Claim(ClaimTypes.NameIdentifier, "12345"),
    new Claim(ClaimTypes.Name, "홍길동"),
    new Claim(ClaimTypes.Role, "Admin"),
    new Claim("Permission", "/Area1/Controller1/Action1"),
    new Claim("Permission", "/Area2/Controller2/Action2")
};

이 Claim들을 어디에 저장하느냐 가 핵심이다. 저장 위치에 따라 쿠키 인증, JWT 인증, 세션 인증으로 나뉜다.


저장 방식 비교

쿠키 인증

Claim 데이터를 암호화하여 쿠키에 저장한다.

1
2
3
4
5
6
var claims = new List<Claim> { /* 수백 개 권한 */ };
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);

await HttpContext.SignInAsync(principal);
// 브라우저 쿠키: .AspNetCore.Cookies = [암호화된 Claim 데이터]

문제:

  • 쿠키 크기 제한: 4KB
  • 수백 개 권한은 물리적으로 불가능

JWT (JSON Web Token)

Claim을 JWT로 인코딩하여 클라이언트에 전달한다.

1
2
3
var claims = new List<Claim> { /* 수백 개 권한 */ };
var token = new JwtSecurityToken(claims: claims, ...);
var jwtToken = new JwtSecurityTokenHandler().WriteToken(token);

저장 위치 선택:

  • 쿠키: 4KB 초과 → 불가능
  • LocalStorage: XSS 공격 위험
  • Authorization 헤더: 매 요청마다 20~30KB 전송

문제:

  • 여전히 크기가 큼
  • 권한 변경 시 새 토큰 발급 필요 (기존 토큰은 만료 전까지 유효)

세션

서버에 데이터를 저장하고, 클라이언트에는 식별자만 전달한다.

1
2
3
4
5
6
// 로그인 시
var claims = new List<Claim> { /* 수백 개 권한 */ };
HttpContext.Session.SetString("claims", JsonSerializer.Serialize(claims));

// 쿠키에는 세션 ID만 저장 (약 20 bytes)
// .AspNetCore.Session = abc123xyz

장점:

  • 클라이언트 전송 크기: 20 bytes
  • 서버에서 실시간 권한 변경 가능
  • 클라이언트가 데이터 조작 불가능


세션을 선택했던 사고 흐름

1. 권한 데이터 저장 위치 후보

“권한 데이터를 어디에 저장할까?”

선택지:

  • 클라이언트 (브라우저)
  • 서버

처음에는 클라이언트 저장을 고려했다. 매번 서버에 물어볼 필요 없이 빠르게 확인할 수 있을 것 같았다.

2. 클라이언트 저장의 문제점

쿠키

  • 크기: 15KB
  • 제한: 4KB

LocalStorage

  • 크기: 5~10MB (충분함)
  • 보안: JavaScript 접근 가능 → XSS 공격 위험
  • 조작: 클라이언트에서 권한 수정 가능
  • 위험함

그래서 클라이언트 저장 포기했다.

3. 서버 저장

서버에 저장 후 어떻게 매번 식별하지?

=> 식별자를 클라이언트에 주고 그 식별자를 쓰면 되겠다.

4. 식별자의 크기는?

A) 권한 데이터를 암호화해서 토큰으로 (JWT)

1
2
3
권한 300개 → JSON 직렬화 → 15KB
→ JWT 암호화 → 20KB
→ Base64 인코딩 → 약 27KB

문제: 여전히 크다. 매 요청마다 20~30KB

B) 식별자만 주고 실제 데이터는 서버 보관 (세션)

1
2
3
권한 300개 → 서버 메모리/Redis 저장
세션 ID 생성 → "abc123xyz" → 약 20 bytes
→ 쿠키에 저장 가능

장점:

  • 쿠키 크기: 20 bytes « 4KB
  • 매 요청마다 자동 전송
  • 클라이언트 조작 불가능
  • 서버에서 실시간 권한 변경 가능

5. 세션 ID

세션 특징

크기 문제

방식 클라이언트 → 서버 전송량
JWT 매 요청마다 27KB
세션 매 요청마다 20 bytes
차이 1,350배

보안 문제

방식 데이터 위치 조작 가능성
JWT 클라이언트 디코딩 가능 (내용 볼 수 있음)
세션 서버 ID만 알고 내용 모름


브라우저 저장소 크기 비교

실제로 각 저장소의 크기를 확인해보자.

저장소 크기 제한 특징
쿠키 4KB 매 요청마다 자동 전송
LocalStorage 5~10MB JavaScript 접근 가능 (XSS 위험)
SessionStorage 5~10MB 탭 종료 시 삭제, XSS 위험 동일
서버 세션 서버 메모리 한계 클라이언트는 ID만 보유

권한 URL 크기 계산:

1
2
3
권한 하나: "/Qna/QnaTicket/Update" = 23 bytes (영문 기준)
Claim 래핑: JSON 직렬화 시 약 50~60 bytes
300개 권한: 50 × 300 = 15,000 bytes = 15KB

쿠키 제한:

  • 1개당: 4KB
  • 도메인당: 50~180개
  • 15KB는 물리적으로 불가능


마치며

수백 개의 권한을 관리하는 문제를 통해 세션 기반 인증의 필요성을 깨달았다.

핵심 원칙:

개념 설명
Claim 데이터 구조일 뿐, 저장 위치가 중요
쿠키 인증 4KB 제한으로 대량 데이터 불가능
JWT 여전히 크고, 실시간 변경 불가
세션 서버 저장 + 식별자 전달 = 최적

실무에서 “왜 이 방식을 사용하는가?”를 이해하는 것이 중요하다.

단순히 “세션이 좋다더라”가 아니라, 크기 제한과 보안, 유연성의 트레이드오프를 직접 계산하고 비교해보면 자연스럽게 답이 나온다.

하지만 항상 이게 정답이다 라고 단정지을 순 없으므로 과감하게 코드를 바꾸기도 할 것 같다.


References

This post is licensed under CC BY 4.0 by the author.