Skip to content

Commit dd5160f

Browse files
authored
Merge pull request #395 from WeGo-Together/chiyoung-fix/refresh
[Fix] refreshToken이 유효하지 않을 때 무한 새로고침 발생하는 버그 수정
2 parents b00d240 + 9113e37 commit dd5160f

2 files changed

Lines changed: 64 additions & 26 deletions

File tree

src/api/core/base/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ baseInstance.interceptors.response.use(
3737
if (isServer) {
3838
throw refreshError;
3939
} else {
40+
if (window.location.pathname === '/login') {
41+
throw errorResponse;
42+
}
4043
const currentPath = window.location.pathname + window.location.search;
4144
window.location.href = `/login?error=unauthorized&path=${encodeURIComponent(currentPath)}`;
4245
return;

src/proxy.ts

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,79 @@
11
import { NextRequest, NextResponse } from 'next/server';
22

3-
import { API } from './api';
3+
import { RefreshResponse } from './types/service/auth';
4+
5+
const GUEST_ONLY_PATHS = ['/login', '/signup'];
6+
const MEMBER_ONLY_PATHS = ['/mypage', '/create-group', '/message', '/schedule', '/notification'];
47

58
export const proxy = async (request: NextRequest) => {
69
const accessToken = request.cookies.get('accessToken');
710
const refreshToken = request.cookies.get('refreshToken');
8-
let hasValidToken = !!accessToken;
9-
10-
const protectedPaths = ['/mypage', '/create-group', '/message', '/schedule', '/notification'];
11-
const isProtected = protectedPaths.some((path) => request.nextUrl.pathname.startsWith(path));
11+
let isLoggedIn = !!accessToken;
12+
let refreshFailed = false;
1213

13-
const publicPaths = ['/login', '/signup'];
14-
const isPublic = publicPaths.some((path) => request.nextUrl.pathname.startsWith(path));
15-
16-
// 인증된 사용자가 public 페이지 접근 시 홈으로
17-
// refresh 중복 실행을 방지하기 위해 최상단으로 이동
18-
if (isPublic && refreshToken) {
19-
return NextResponse.redirect(new URL('/', request.url));
20-
}
14+
const isGuestOnly = GUEST_ONLY_PATHS.some((path) => request.nextUrl.pathname.startsWith(path));
15+
const isMemberOnly = MEMBER_ONLY_PATHS.some((path) => request.nextUrl.pathname.startsWith(path));
2116

2217
// 일반 응답 생성
2318
const response = NextResponse.next();
2419

25-
// accessToken이 없으면 refresh 실행하여 일반 응답에 set cookie 설정
26-
if (!accessToken && refreshToken) {
20+
// accessToken이 없을 때 refreshToken 있으면 refresh 시도 - 응답에 set cookie 설정
21+
if (!isLoggedIn && refreshToken) {
2722
try {
28-
const res = await API.authService.refresh();
29-
const data = res;
30-
hasValidToken = true;
23+
const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/auth/refresh`, {
24+
method: 'POST',
25+
headers: { Cookie: `refreshToken=${refreshToken.value}` },
26+
});
27+
if (!res.ok) throw new Error('refresh failed');
28+
const json = await res.json();
29+
const data: RefreshResponse = json.data;
30+
isLoggedIn = true;
3131
response.cookies.set('accessToken', data.accessToken, {
3232
httpOnly: false,
3333
maxAge: data.expiresIn,
3434
domain: 'wego.monster',
3535
secure: process.env.NODE_ENV === 'production',
3636
});
37-
} catch {
38-
hasValidToken = false;
37+
// 서버가 발급한 새 refreshToken Set-Cookie 헤더를 브라우저에 포워딩
38+
const setCookieHeader = res.headers.get('set-cookie');
39+
if (setCookieHeader) {
40+
response.headers.append('Set-Cookie', setCookieHeader);
41+
}
42+
} catch (err) {
43+
console.log('refresh failed', err);
44+
isLoggedIn = false;
45+
refreshFailed = true;
46+
response.cookies.set('refreshToken', '', {
47+
maxAge: 0,
48+
domain: 'wego.monster',
49+
path: '/',
50+
httpOnly: true,
51+
secure: process.env.NODE_ENV === 'production',
52+
});
3953
}
4054
}
4155

42-
// 보호되지 않은 경로는 그냥 통과
43-
if (!isProtected) {
44-
return response;
56+
// 로그인 상태에서 Guest Only Path 에 접근 시 / 로 Redirect
57+
if (isGuestOnly && isLoggedIn) {
58+
return NextResponse.redirect(new URL('/', request.url));
4559
}
4660

47-
// accessToken 없으면 login redirect
48-
if (!hasValidToken) {
61+
// 로그아웃 상태에서 Member Only Path에 접근 시 /login 으로 Redirect
62+
if (isMemberOnly && !isLoggedIn) {
4963
const loginUrl = new URL('/login', request.url);
5064
loginUrl.searchParams.set('error', 'unauthorized');
5165
loginUrl.searchParams.set('path', request.nextUrl.pathname);
52-
return NextResponse.redirect(loginUrl);
66+
const redirectResponse = NextResponse.redirect(loginUrl);
67+
if (refreshFailed) {
68+
redirectResponse.cookies.set('refreshToken', '', {
69+
maxAge: 0,
70+
domain: 'wego.monster',
71+
path: '/',
72+
httpOnly: true,
73+
secure: process.env.NODE_ENV === 'production',
74+
});
75+
}
76+
return redirectResponse;
5377
}
5478

5579
return response;
@@ -58,3 +82,14 @@ export const proxy = async (request: NextRequest) => {
5882
export const config = {
5983
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
6084
};
85+
86+
// 0. refreshToken만 있고 accessToken이 없는 경우 refresh 시도
87+
// 0-1. refreshToken이 유효하지 않을 때는 로그아웃 상태로 판정됨. 이후 규칙에 의해 동작이 결정됨
88+
// 1. 로그인 상태에서 /login, /signup 접근 시 /로 redirect
89+
// 2. 로그아웃 상태에서 인증이 필요한 경로 접근 시 /login으로 redirect
90+
// 3. member only 도 아니고 guest only 도 아닌 경로 접근 시 그대로 통과(ex. / 접근 시)
91+
92+
// 기본 정보
93+
// - logout API는 accessToken이 유효하지 않을 경우 401 에러 반환됨.
94+
// - 즉 refreshToken이 저장되어있지만 유효하지 않으면 logout api 실행 불가
95+
// - 따라서 refreshToken이 유효하지 않을 경우 logout api를 호출하는 것이 아닌 직접 setcookie 설정으로 cookie 정보를 삭제해야함.

0 commit comments

Comments
 (0)