들어가며
이번에 블로그 관리자 로그인 구조를 다시 정리했습니다. 처음에는 빠르게 붙이는 게 우선이라 브라우저 localStorage에 인증 정보를 저장하는 방식으로 구현했습니다.
처음엔 이 방식이 편했습니다. 로그인 직후 화면 상태를 바꾸기도 쉽고, 클라이언트에서 로그인 여부를 바로 읽어 분기하기도 간단했기 때문입니다. 다만 관리자 기능처럼 권한이 분명한 영역을 다루기 시작하니, 인증 기준이 브라우저 저장소에 있는 구조가 점점 어색하게 느껴졌습니다.
특히 인증값을 JavaScript가 직접 읽을 수 있다는 점이 계속 걸렸습니다. XSS 같은 문제가 생기면 값이 노출될 수 있고, 서버 입장에서도 "이 요청이 정말 관리자 세션에서 온 것인가"를 좀 더 명확하게 판단할 필요가 있었습니다.
그래서 지금은 비밀번호 검증은 AWS Lambda가 맡고, 브라우저에는 httpOnly 쿠키 기반 세션만 남기는 구조로 바꿨습니다. 이 글은 현재 블로그 프로젝트 기준으로 관리자 로그인 세션이 어떻게 흐르는지, 그리고 왜 localStorage 방식에서 쿠키 세션 방식으로 바꾸게 됐는지를 정리한 글 입니다.
처음 구조: localStorage에 인증 정보를 저장
처음 구조는 단순했습니다. 로그인 화면에서 비밀번호를 입력하고, 그 값을 클라이언트 저장소에 넣어 둔 뒤 이후 요청에서 꺼내 쓰는 방식이었습니다.
이 방식은 초반에는 분명 장점이 있습니다.
구현이 빠릅니다.
클라이언트에서 로그인 여부를 바로 확인할 수 있습니다.
헤더나 관리자 UI를 조건부로 바꾸기 쉽습니다.
별도 세션 저장 로직 없이도 바로 동작합니다.
작은 프로젝트나 임시 관리자 기능이라면 한동안은 충분히 쓸 수 있는 방식입니다. 다만 "일단 된다"와 "구조적으로 적절하다"는 다른 문제였습니다.
왜 바꾸게 되었는가
가장 큰 이유는 관리자 인증의 기준을 브라우저 저장소에 두는 방식이 점점 불편해졌기 때문입니다.
인증 정보를 브라우저 JavaScript가 직접 읽을 수 있습니다.
XSS가 생기면
localStorage값이 그대로 노출될 수 있습니다.클라이언트가 인증의 원천처럼 보이기 시작하면 서버와 프론트의 책임이 섞입니다.
로그인 상태를 서버 기준으로 판단해야 하는 페이지와 잘 맞지 않습니다.
관리자 페이지 보호를 클라이언트 분기에 기대는 방식이 점점 불안해졌습니다.
이번에는 단순히 로그인 버튼 상태만 바꾸는 수준이 아니라, 글쓰기·수정·삭제 같은 관리자 작업 전체를 한 흐름으로 정리해야 했습니다. 그러다 보니 인증의 기준도 브라우저 저장소보다는 서버 쪽에 두는 편이 더 자연스럽다고 판단했습니다.
지금 구조는 어떻게 바뀌었는가
지금 구조를 요약하면 이렇습니다. AWS Lambda가 로그인 비밀번호를 검증하고 세션 토큰을 발급합니다. Next.js는 그 토큰을 httpOnly 쿠키로 저장하고, 이후 관리자 요청에서 그 쿠키를 읽어 Lambda로 전달합니다.
즉 관리자 비밀번호 자체는 AWS Lambda 쪽에만 두고, Next.js는 세션 쿠키를 다루는 중간 계층 역할을 맡도록 정리했습니다.
각 역할의 책임
브라우저
사용자가 로그인 폼에 비밀번호를 입력합니다.
로그인 성공 후에는
httpOnly쿠키만 받습니다.세션 토큰 값 자체는 직접 읽을 수 없습니다.
Next.js
로그인 폼을 렌더링합니다.
/api/admin/session에서 로그인 요청을 받아 Lambda로 전달합니다.Lambda가 돌려준 세션 토큰을 쿠키로 저장합니다.
글쓰기나 삭제 요청이 오면 쿠키의 세션 토큰을 읽어 Lambda로 중계합니다.
AWS Lambda
ADMIN_PASSWORD와 입력 비밀번호를 비교합니다.일치하면 세션 토큰을 발급합니다.
이후 관리자 요청에서 해당 토큰이 유효한지 검사합니다.
전체 로그인 흐름
사용자가
/admin/login에서 비밀번호를 입력합니다.클라이언트는 Next.js의
/api/admin/session으로POST요청을 보냅니다.Next.js는 그 비밀번호를 Lambda의
?action=auth엔드포인트로 전달합니다.Lambda는 자신의
ADMIN_PASSWORD와 비교합니다.비밀번호가 맞으면 세션 토큰을 생성해 응답합니다.
Next.js는 그 토큰을
margot_blog_admin_session쿠키에 저장합니다.이후 브라우저는 같은 사이트 요청에 그 쿠키를 자동으로 함께 보냅니다.
Next.js는 관리자 요청을 프록시할 때 쿠키 값을 읽어
Authorization: Bearer ...형태로 Lambda에 전달합니다.Lambda는 이 토큰이 자기가 발급한 유효한 세션인지 검사한 뒤 요청을 허용합니다.
왜 localStorage 대신 쿠키를 썼는가
핵심은 브라우저가 세션을 들고는 있되, 프론트 코드가 그 값을 직접 읽지 않는 구조를 만들고 싶었기 때문입니다.
httpOnly 쿠키는 요청에는 자동으로 포함되지만, 클라이언트 JavaScript에서는 직접 읽을 수 없습니다. 관리자 인증처럼 민감한 상태를 다룰 때 이 차이는 꽤 큽니다.
localStorage는 구현은 편하지만 인증 정보를 너무 클라이언트 쪽에 두게 됩니다.httpOnly쿠키는 브라우저가 세션을 자동으로 실어 나르되, JavaScript가 직접 만질 수는 없습니다.
관리자 기능에서는 후자가 훨씬 자연스러웠습니다. 이 경우 더 중요한 건 "화면 상태를 어떻게 바꾸느냐"보다 "서버가 세션을 신뢰할 수 있느냐"였기 때문입니다.
왜 JWT 대신 세션 토큰을 썼는가
이 프로젝트는 관리자 1명이 쓰는 개인 블로그에 가깝습니다. 이런 경우에는 로그인 상태를 유지하는 정도면 충분한데, JWT를 도입하면 보통 만료 정책, 갱신 전략, 폐기 전략, 클레임 설계 같은 고민이 함께 따라옵니다.
지금은 그 정도 복잡도가 필요하지 않았습니다. 그래서 Lambda가 단순한 서명된 세션 토큰을 발급하고, 이를 쿠키에 저장하는 쪽이 더 현실적이라고 판단했습니다.
세션 토큰은 어떻게 만들었는가
구조는 단순합니다.
만료 시각을 담은 payload를 만듭니다.
그 payload를 base64url로 인코딩합니다.
ADMIN_SESSION_SECRET으로HMAC-SHA256서명을 만듭니다.payload.signature형태의 문자열을 세션 토큰으로 사용합니다.
즉 이 토큰은 "서버가 발급했고, 만료되지 않았고, 중간에 변조되지 않았다"를 확인하기 위한 값입니다. 여기서 중요한 점은 서명 키를 아는 주체가 Lambda뿐이라는 것입니다.
환경변수는 왜 두 개가 필요한가
ADMIN_PASSWORD
실제 로그인 비밀번호입니다. 사용자가 로그인 폼에 입력한 값과 비교하는 기준값입니다.
ADMIN_SESSION_SECRET
세션 토큰 서명용 비밀값입니다. 로그인 비밀번호와는 역할이 다릅니다.
ADMIN_PASSWORD: 로그인 시 사용자를 확인하는 기준값ADMIN_SESSION_SECRET: 로그인 후 세션의 무결성을 검증하는 키
둘을 나눈 이유는 인증 비밀번호와 세션 서명 키를 같은 값으로 묶지 않기 위해서입니다.
Next.js와 Lambda 중 누가 세션을 만들어야 하나
여기서 한 가지는 분명히 할 필요가 있습니다. Next.js가 세션을 직접 생성하지 못하는 것은 아닙니다. 실제로는 Next.js도 세션을 만들고 쿠키를 발급할 수 있습니다.
이번 프로젝트에서 Lambda가 세션을 생성하도록 잡은 이유는, 비밀번호 검증 기준을 AWS 쪽에만 두고 싶었기 때문입니다. 즉 "무엇이 가능하냐"의 문제가 아니라 "어디에 인증 기준을 둘 것이냐"의 문제에 가깝습니다.
만약 Next.js가 세션을 직접 생성한다면 구조는 더 단순해집니다. 로그인 검증과 세션 발급, 세션 검증이 모두 Next.js 안에서 끝나기 때문입니다. 다만 그러려면 Next.js도 ADMIN_PASSWORD와 세션 서명 키를 알고 있어야 합니다.
반대로 지금처럼 Lambda가 세션을 생성하면 구조는 한 단계 길어지지만, ADMIN_PASSWORD를 Lambda 환경변수에만 둘 수 있습니다. 비밀번호 검증과 세션 검증 기준도 AWS 쪽에 모을 수 있습니다.
이번 블로그에서는 인증의 기준을 Next.js보다 AWS 쪽에 두고 싶었기 때문에, Lambda가 세션을 생성하고 Next.js는 쿠키를 다루는 구조를 택했습니다.
관리자 페이지 보호는 어떻게 되는가
/admin/new나 /admin/edit 같은 페이지는 서버에서 먼저 쿠키를 읽고, 세션이 없거나 유효하지 않으면 로그인 페이지로 리다이렉트합니다.
이 방식의 장점은 클라이언트에서 페이지가 먼저 보였다가 뒤늦게 막히는 게 아니라, 애초에 서버 단계에서 접근을 차단할 수 있다는 점입니다.
로그아웃은 왜 단순한가
지금 구조에서는 로그아웃도 단순합니다. Next.js가 margot_blog_admin_session 쿠키를 비우면 됩니다.
현재 구조에서 세션 상태의 기준은 쿠키이기 때문에, 쿠키를 지우면 브라우저는 더 이상 관리자 요청을 보낼 수 없습니다.
이 구조에서 얻은 점
관리자 비밀번호를 Next.js 환경변수에 둘 필요가 없습니다.
브라우저
localStorage에 관리자 인증값을 저장하지 않습니다.클라이언트 JavaScript가 세션 토큰을 직접 읽을 수 없습니다.
관리자 페이지 보호를 서버 기준으로 정리할 수 있습니다.
글쓰기·삭제 요청의 인증 흐름을 프록시 계층에 모을 수 있습니다.
더 고민해볼 부분
물론 이 구조가 완벽한 인증 시스템이라는 뜻은 아닙니다. 현재 프로젝트 규모에서는 충분히 실용적이지만, 더 확장한다면 아래 같은 지점은 추가로 고민할 수 있습니다.
세션 만료 시간을 더 엄격하게 가져갈지
로그인 시도 제한을 둘지
세션 무효화 전략을 둘지
운영 환경에서
Secure쿠키만 허용할지
다만 개인 블로그 관리자 기능이라는 범위를 생각하면, 지금 구조는 복잡도를 크게 늘리지 않으면서도 localStorage 기반보다는 훨씬 낫다는 점에서 충분히 만족스러웠습니다.
관련 문서
Next.js Route Handlers: https://nextjs.org/docs/app/building-your-application/routing/route-handlers
Next.js
cookies(): https://nextjs.org/docs/app/api-reference/functions/cookiesMDN
Set-Cookie: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-CookieMDN Secure cookie configuration: https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Cookies
OWASP Session Management Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
Node.js
crypto.createHmac(): https://nodejs.org/api/crypto.html
마무리
이번에 구조를 바꾸면서 가장 크게 느낀 점은, 관리자 인증 기준을 브라우저 저장소에 두는 것과 서버 세션 기준으로 두는 것은 생각보다 차이가 크다는 점이었습니다.
빠르게 구현할 때는 localStorage 방식이 분명 편합니다. 하지만 관리자 기능처럼 권한이 분명한 영역에서는 인증 기준을 서버 쪽으로 밀어 두는 편이 훨씬 자연스럽습니다.
이 블로그에서는 로그인 검증은 Lambda가 맡고, 세션 유지와 브라우저 연결은 Next.js가 맡도록 역할을 나눴습니다. 이렇게 정리하고 나니 코드 구조도 더 명확해졌고, 왜 localStorage보다 쿠키 세션이 더 적절한지도 스스로 훨씬 선명하게 설명할 수 있게 됐습니다.