From 43f0e7855772fbeed2423a277ae1c77cb33e5546 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Mon, 23 Feb 2026 23:19:31 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20middleware=EC=97=90=EC=84=9C=20acce?= =?UTF-8?q?ssToken=20=EC=9E=90=EB=8F=99=20=EA=B0=B1=EC=8B=A0=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80=20-=20refreshToken=EC=9D=B4=20?= =?UTF-8?q?=EC=9E=88=EA=B3=A0=20accessToken=EC=9D=B4=20=EC=97=86=EC=9D=84?= =?UTF-8?q?=20=EB=95=8C=20=EC=9E=90=EB=8F=99=EC=9C=BC=EB=A1=9C=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_lib/axiosInstance.ts | 2 + apps/web/middleware.ts | 85 +++++++++++++++++++++++++----- 2 files changed, 75 insertions(+), 12 deletions(-) diff --git a/apps/web/app/_lib/axiosInstance.ts b/apps/web/app/_lib/axiosInstance.ts index 694497a5..4a1b0d84 100644 --- a/apps/web/app/_lib/axiosInstance.ts +++ b/apps/web/app/_lib/axiosInstance.ts @@ -6,6 +6,7 @@ import axios, { import * as Sentry from '@sentry/nextjs' import { getToken } from '@/_apis/services/login' import { getCookie } from '@/_utils/getCookie' +import { setCookie } from 'cookies-next' const axiosInstance = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL, @@ -54,6 +55,7 @@ axiosInstance.interceptors.response.use( try { const { accessToken: newAccessToken } = await getToken() + setCookie('accessToken', newAccessToken) if (originalRequest.headers) { originalRequest.headers.Authorization = newAccessToken diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 3f53e0c6..630b8a31 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -1,32 +1,93 @@ import { NextRequest, NextResponse } from 'next/server' -import { cookies } from 'next/headers' -import { CLIENT_PATH } from 'app/_constants/path' +import { API_PATH, CLIENT_PATH } from 'app/_constants/path' export async function middleware(request: NextRequest) { - const cookieStore = await cookies() - const accessToken = cookieStore.get('accessToken')?.value + const accessToken = request.cookies.get('accessToken')?.value + const refreshToken = request.cookies.get('refreshToken')?.value - if (!accessToken) { + // [Case 1] 액세스 토큰이 유효한 경우 -> 가장 먼저 통과시킴 (Early Return) + if (accessToken) { + return NextResponse.next() + } + + // [Case 2] 토큰이 아예 없는 경우 -> 곧바로 로그인 페이지로 (Early Return) + if (!refreshToken) { return NextResponse.redirect(new URL(CLIENT_PATH.LOGIN, request.url)) } - return NextResponse.next() + // [Case 3] 액세스 토큰 만료 & 리프레시 토큰 존재 -> 갱신 시도 + return await handleTokenRefresh(request, refreshToken) } export const config = { matcher: [ '/likes', '/profile', - - // '/places/new', - // '/places/new/success', - // '/places/new/fail', - '/requests', '/requests/:path*', - '/events/lucky-draw', '/events/gifticon', '/events/gifticon/:path*', ], } + +const handleTokenRefresh = async ( + request: NextRequest, + refreshToken: string, +) => { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}${API_PATH.AUTH.TOKEN}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Cookie: `refreshToken=${refreshToken}`, + }, + }, + ) + + if (!response.ok) throw new Error('Token refresh failed') + + // 구조 분해 할당을 한 번에 처리하여 코드 간소화 + const { + data: { accessToken: newAccessToken, accessTokenExpiresIn }, + } = await response.json() + + // 1. [서버 컴포넌트 동기화] Request Header 조작 + const requestHeaders = new Headers(request.headers) + requestHeaders.set('Authorization', `Bearer ${newAccessToken}`) + + const res = NextResponse.next({ + request: { + headers: requestHeaders, + }, + }) + + // 2. [브라우저 동기화] 쿠키 세팅 + res.cookies.set('accessToken', newAccessToken, { + path: '/', + httpOnly: false, + secure: process.env.NODE_ENV === 'production', + maxAge: accessTokenExpiresIn, + }) + + // 3. 백엔드 쿠키(Set-Cookie) 포워딩 + const backendSetCookie = response.headers.get('set-cookie') + if (backendSetCookie) { + res.headers.append('set-cookie', backendSetCookie) + } + + return res + } catch (error) { + console.error('Middleware Token Refresh Error:', error) + + // 갱신 실패 시 로그인 리다이렉트 및 만료 토큰 정리 + const redirectRes = NextResponse.redirect( + new URL(CLIENT_PATH.LOGIN, request.url), + ) + redirectRes.cookies.delete('refreshToken') + + return redirectRes + } +} From 017aa3a4aa6fe6c9f8fedf7166055ae08bfd98c2 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Tue, 24 Feb 2026 13:02:05 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=EA=B2=BD=EB=A1=9C=20import=EB=A5=BC?= =?UTF-8?q?=20app/=5Fconstants=EC=97=90=EC=84=9C=20@/=5Fconstants=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 630b8a31..25a1f5fb 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { API_PATH, CLIENT_PATH } from 'app/_constants/path' +import { API_PATH, CLIENT_PATH } from '@/_constants/path' export async function middleware(request: NextRequest) { const accessToken = request.cookies.get('accessToken')?.value From 1d43299ffd0b423e6fe9ccd5564f68e3caecbcba Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Tue, 24 Feb 2026 13:02:24 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20accessToken=20=EC=BF=A0=ED=82=A4=20?= =?UTF-8?q?=EC=98=B5=EC=85=98=20sameSite=20=EB=B0=8F=20expires=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/middleware.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 25a1f5fb..1566130a 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -67,9 +67,9 @@ const handleTokenRefresh = async ( // 2. [브라우저 동기화] 쿠키 세팅 res.cookies.set('accessToken', newAccessToken, { path: '/', - httpOnly: false, secure: process.env.NODE_ENV === 'production', - maxAge: accessTokenExpiresIn, + sameSite: 'lax', + expires: new Date(Date.now() + accessTokenExpiresIn), }) // 3. 백엔드 쿠키(Set-Cookie) 포워딩 From 75ef32e244b08e52b23d71731047adbe421e8a15 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Tue, 24 Feb 2026 13:04:34 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=EB=B0=B1=EC=97=94=EB=93=9C=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=97=AC=EB=9F=AC=20Set-Cookie=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=EB=A5=BC=20=EC=98=AC=EB=B0=94=EB=A5=B4=EA=B2=8C=20?= =?UTF-8?q?=ED=8F=AC=EC=9B=8C=EB=94=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/middleware.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 1566130a..bcabd2f0 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -73,9 +73,9 @@ const handleTokenRefresh = async ( }) // 3. 백엔드 쿠키(Set-Cookie) 포워딩 - const backendSetCookie = response.headers.get('set-cookie') - if (backendSetCookie) { - res.headers.append('set-cookie', backendSetCookie) + const backendSetCookies = response.headers.getSetCookie() + for (const cookie of backendSetCookies) { + res.headers.append('set-cookie', cookie) } return res From 28a93df5bbd409df11ab172bfa09aeed321e3cf3 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Tue, 24 Feb 2026 13:11:08 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20accessToken=20=EC=BF=A0=ED=82=A4=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=8B=9C=20=EC=98=B5=EC=85=98(path,=20sec?= =?UTF-8?q?ure,=20sameSite,=20expires)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_lib/axiosInstance.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/web/app/_lib/axiosInstance.ts b/apps/web/app/_lib/axiosInstance.ts index 4a1b0d84..388d4307 100644 --- a/apps/web/app/_lib/axiosInstance.ts +++ b/apps/web/app/_lib/axiosInstance.ts @@ -54,8 +54,15 @@ axiosInstance.interceptors.response.use( originalRequest._retry = true try { - const { accessToken: newAccessToken } = await getToken() - setCookie('accessToken', newAccessToken) + const { accessToken: newAccessToken, accessTokenExpiresIn } = + await getToken() + + setCookie('accessToken', newAccessToken, { + path: '/', + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + expires: new Date(Date.now() + accessTokenExpiresIn), + }) if (originalRequest.headers) { originalRequest.headers.Authorization = newAccessToken