Post

Next.js 서버 영역에서의 인증 토큰 관리

Next.js 서버 영역에서의 인증 토큰 관리

인증 시스템 개선 경험

이전 프로젝트에서는 클라이언트가 인증 토큰을 LocalStorage에 저장하고 매 요청마다 백엔드에 전송하는 방식을 사용했습니다.
하지만 이 방식은 보안성과 유지보수성 측면에서 여러 문제를 드러냈습니다.

  • 보안 취약성: LocalStorage에 저장된 토큰이 XSS 공격에 노출될 위험
  • 토큰 갱신의 복잡성: 클라이언트가 직접 갱신 로직을 처리해야 함
  • 클라이언트 종속성 증가: 인증 상태를 유지하는 로직이 클라이언트에 집중됨

이를 해결하기 위해 Next.js API Routes와 Middleware를 활용한 안전한 인증 시스템을 설계했습니다.


개선 과정

1. JWT를 HttpOnly 쿠키로 안전하게 저장

토큰을 LocalStorage 대신 HttpOnly, Secure 속성이 적용된 쿠키에 저장했습니다.

  • XSS 방지: JavaScript 접근 차단
  • CSRF 방어: sameSite: 'lax' 설정으로 추가 보안 강화
  • HTTPS 전용: Secure 속성으로 안전한 전송 보장
1
2
3
4
5
6
res.cookies.set('accessToken', accessToken, {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'lax',
  maxAge: 3600,
})

2. Next.js API Routes로 인증 중앙화

클라이언트가 백엔드와 직접 통신하지 않도록 Next.js API Routes를 통해 토큰 관리와 인증 처리를 중앙화했습니다.

  • 토큰 조회: 클라이언트가 필요한 경우 토큰 상태 확인 가능
  • 자동 갱신: 만료된 토큰을 백엔드와 연동해 갱신

Access Token 조회 API

1
2
3
const accessToken = (await cookies()).get('accessToken')?.value
if (!accessToken) return NextResponse.json({ success: false }, { status: 401 })
return NextResponse.json({ success: true, accessToken })

Access Token 갱신 API

1
2
3
4
5
6
7
const { oldAccessToken, refreshToken } = await req.json()
const { result: { accessToken } } = await backendApi.post('v1/auth/new-token', {
  json: { oldAccessToken, refreshToken },
  headers: { Authorization: `Bearer ${refreshToken}` },
}).json()
res.cookies.set('accessToken', accessToken, { httpOnly: true, secure: true, sameSite: 'lax', path: '/', maxAge: 1800 })
return res

3. Middleware로 인증 자동화

Next.js Middleware를 활용해 모든 요청에서 인증 상태를 자동 점검하고, 토큰이 만료되면 갱신하거나 로그인 페이지로 리다이렉트하도록 했습니다.

  • 클라이언트 부담 감소
  • 사용자 경험 개선(인증 끊김 최소화)

Middleware 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
const oldAccessToken = req.cookies.get('accessToken')?.value
const refreshToken = req.cookies.get('refreshToken')?.value
if (!oldAccessToken || !refreshToken) return NextResponse.redirect(new URL('/login', req.url))

try {
  const decodedToken = jwt.decode(oldAccessToken)
  if (decodedToken?.exp > Math.floor(Date.now() / 1000)) return NextResponse.next()
  const newTokenResponse = await requestNewToken(oldAccessToken, refreshToken)
  if (newTokenResponse.success) return NextResponse.next()
} catch {
  return NextResponse.redirect(new URL('/login', req.url))
}

4. 로그아웃 처리

로그아웃 시 토큰을 제거하도록 구현했습니다.

1
2
3
res.cookies.set('accessToken', '', { httpOnly: true, secure: true, sameSite: 'strict', path: '/', maxAge: 0 })
res.cookies.set('refreshToken', '', { httpOnly: true, secure: true, sameSite: 'strict', path: '/', maxAge: 0 })
return res

적용 후 결과

  • 보안성: XSS와 CSRF 위협 감소
  • 자동 토큰 갱신: Middleware를 활용한 인증 흐름 개선으로 사용자 경험 최적화
  • 유지보수: 인증 로직이 서버에서 통합 관리되어 코드 가독성과 확장성 향상

이 경험을 통해 보안과 효율성을 동시에 잡는 설계의 중요성을 깨달았고, 이후 프로젝트에서도 비슷한 접근법으로 팀의 개발 속도를 높이는 데 기여했습니다.

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