wr: refresh token관련
This commit is contained in:
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { LoginApi } from "@/src/entities/auth/api/login-service";
|
||||
import { CLAIM_NAME } from "@/src/shared/config";
|
||||
import { getCookie } from "@/src/shared/lib";
|
||||
|
||||
@@ -112,14 +113,51 @@ function readJwtState(): JwtState {
|
||||
|
||||
export default function Page() {
|
||||
const [jwtState, setJwtState] = useState<JwtState>(() => readJwtState());
|
||||
const isRefreshingRef = useRef(false);
|
||||
|
||||
const handleSilentRefresh = async () => {
|
||||
try {
|
||||
await LoginApi.silentRefresh();
|
||||
} catch (error) {
|
||||
console.error("Manual silent refresh failed:", error);
|
||||
} finally {
|
||||
setJwtState(readJwtState());
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const syncJwtState = () => {
|
||||
const syncJwtState = async () => {
|
||||
const nextState = readJwtState();
|
||||
|
||||
if (
|
||||
nextState.remainingMs !== null &&
|
||||
nextState.remainingMs > 0 &&
|
||||
nextState.remainingMs < 5 * 60 * 1000 &&
|
||||
!isRefreshingRef.current
|
||||
) {
|
||||
isRefreshingRef.current = true;
|
||||
try {
|
||||
await LoginApi.silentRefresh();
|
||||
} catch (error) {
|
||||
console.error("JWT test silent refresh failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
setJwtState(readJwtState());
|
||||
|
||||
if (
|
||||
nextState.remainingMs === null ||
|
||||
nextState.remainingMs >= 5 * 60 * 1000 ||
|
||||
nextState.remainingMs <= 0
|
||||
) {
|
||||
isRefreshingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
syncJwtState();
|
||||
const timer = window.setInterval(syncJwtState, 1000);
|
||||
void syncJwtState();
|
||||
const timer = window.setInterval(() => {
|
||||
void syncJwtState();
|
||||
}, 1000);
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
@@ -128,6 +166,16 @@ export default function Page() {
|
||||
<div className="space-y-4 p-8 text-base">
|
||||
<h1 className="text-2xl font-bold">JWT Exp Test</h1>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
||||
onClick={() => {
|
||||
void handleSilentRefresh();
|
||||
}}
|
||||
>
|
||||
Silent Refresh 호출
|
||||
</button>
|
||||
|
||||
<div className="rounded border p-4">
|
||||
<div className="font-semibold">쿠키명</div>
|
||||
<div>{CLAIM_NAME}</div>
|
||||
|
||||
@@ -15,9 +15,10 @@ import { NextResponse } from "next/server";
|
||||
const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token";
|
||||
const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
|
||||
const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
|
||||
const COOKIE_DOMAIN = process.env.NEXT_PUBLIC_COOKIE_DOMAIN;
|
||||
const REFRESH_TOKEN = process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token";
|
||||
const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL || "";
|
||||
const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "SN";
|
||||
const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "WR";
|
||||
|
||||
async function handler(
|
||||
request: Request,
|
||||
@@ -35,6 +36,7 @@ async function handler(
|
||||
// silent refresh
|
||||
if (isRefreshHandler) {
|
||||
const refreshToken = cookieStore.get(REFRESH_TOKEN);
|
||||
console.log("refreshToken", refreshToken?.value);
|
||||
headers[CLAIM_NAME] = refreshToken?.value ?? "";
|
||||
|
||||
if (!headers[CLAIM_NAME]) {
|
||||
@@ -52,12 +54,11 @@ async function handler(
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
// console.log(result);
|
||||
if (!result.ok) {
|
||||
console.log("Invalid Credentials ");
|
||||
return NextResponse.json(
|
||||
{ message: "Invalid Credentials" },
|
||||
{ status: 401 },
|
||||
{ message: "first Invalid Credentials" },
|
||||
{ status: result.status },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,11 +73,13 @@ async function handler(
|
||||
});
|
||||
|
||||
if (refreshToken) {
|
||||
cookieStore.set(REFRESH_TOKEN, refreshToken, {
|
||||
response.cookies.set(REFRESH_TOKEN, refreshToken, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
sameSite: "strict", // 다른 사이트에서 들어오는 모든 요청에 대해 쿠키를 보내지 않는 가장 강력한 설정
|
||||
domain: getCookieDomain(),
|
||||
path: "/",
|
||||
});
|
||||
console.log("refresh cookie setting : " + refreshToken);
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -109,3 +112,14 @@ export async function PUT(
|
||||
const params = await props.params;
|
||||
return handler(request, params, "PUT");
|
||||
}
|
||||
|
||||
function getCookieDomain(): string | undefined {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
|
||||
if (COOKIE_DOMAIN) return COOKIE_DOMAIN;
|
||||
|
||||
const { hostname } = window.location;
|
||||
if (hostname === "localhost" || hostname === "127.0.0.1") return undefined;
|
||||
|
||||
return hostname;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ function buildAuthHeaders(req: NextRequest): Headers {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||
const userId = req.cookies.get(AUTH_USER_ID)?.value;
|
||||
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
return requestHeaders;
|
||||
@@ -38,7 +39,9 @@ function buildAuthHeaders(req: NextRequest): Headers {
|
||||
/** 공통: rewrite 응답 생성 (헤더 주입 여부 선택) */
|
||||
function rewriteWith(url: string, req: NextRequest, withAuth = false): NextResponse {
|
||||
return withAuth
|
||||
? NextResponse.rewrite(new URL(url), { headers: buildAuthHeaders(req) })
|
||||
? NextResponse.rewrite(new URL(url), {
|
||||
headers: buildAuthHeaders(req),
|
||||
})
|
||||
: NextResponse.rewrite(new URL(url));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import type { LoginService, ILogin } from "../model/login-interface";
|
||||
import { tokenManager } from "../lib/token-manager";
|
||||
import {PROXY_HOST, ACCESS_TOKEN, AUTH_USER_ID,SITE_ID, CLAIM_NAME, ENDPOINT, JWT_EXPIRATION_TIME } from "../../../shared/config";
|
||||
import { ACCESS_TOKEN, AUTH_USER_ID, SITE_ID, CLAIM_NAME, ENDPOINT } from "../../../shared/config";
|
||||
|
||||
export class LoginApiService implements LoginService {
|
||||
private static instance: LoginApiService;
|
||||
@@ -18,31 +18,33 @@ export class LoginApiService implements LoginService {
|
||||
public async login(payload: ILogin): Promise<void> {
|
||||
try {
|
||||
tokenManager.clearAuthToken();
|
||||
const response = await fetch( PROXY_HOST+ ENDPOINT.USER_SERVICE.LOGIN, {
|
||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId: payload.userId,
|
||||
password: payload.password,
|
||||
}),
|
||||
});
|
||||
// const data = response.headers.get(ACCESS_TOKEN);
|
||||
const accessToken = response.headers.get(ACCESS_TOKEN);
|
||||
const tokenId = response.headers.get(AUTH_USER_ID);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.message ?? "Login failed");
|
||||
}
|
||||
|
||||
const accessToken = data[ACCESS_TOKEN];
|
||||
const tokenId = data[AUTH_USER_ID];
|
||||
|
||||
if (!accessToken || !tokenId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
}
|
||||
tokenManager.setToken(CLAIM_NAME, accessToken)
|
||||
tokenManager.setToken(AUTH_USER_ID, tokenId)
|
||||
tokenManager.setToken("site-id-key", SITE_ID)
|
||||
tokenManager.setupRefreshTimer(
|
||||
() => this.silentRefresh(),
|
||||
JWT_EXPIRATION_TIME,
|
||||
);
|
||||
tokenManager.setToken(CLAIM_NAME, accessToken);
|
||||
tokenManager.setToken(AUTH_USER_ID, tokenId);
|
||||
tokenManager.setToken("site-id-key", SITE_ID);
|
||||
tokenManager.setupRefreshTimer(() => this.silentRefresh());
|
||||
} catch (error) {
|
||||
|
||||
throw new Error(`Login failed: ${error}`);
|
||||
@@ -54,17 +56,19 @@ export class LoginApiService implements LoginService {
|
||||
console.log(' silentRefresh ')
|
||||
|
||||
try {
|
||||
const response = await fetch(PROXY_HOST+
|
||||
`${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||
const response = await fetch(
|
||||
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.message ?? "Silent refresh failed");
|
||||
}
|
||||
|
||||
const accessToken = data[ACCESS_TOKEN];
|
||||
const tokenId = data[AUTH_USER_ID];
|
||||
|
||||
@@ -74,6 +78,7 @@ export class LoginApiService implements LoginService {
|
||||
|
||||
tokenManager.setToken(CLAIM_NAME, accessToken);
|
||||
tokenManager.setToken(AUTH_USER_ID, tokenId);
|
||||
tokenManager.setupRefreshTimer(() => this.silentRefresh());
|
||||
} catch (error) {
|
||||
tokenManager.clearAuthToken();
|
||||
throw new Error(`Silent refresh failed: ${error}`);
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { LoginApi } from "../api/login-service";
|
||||
import { tokenManager } from "./token-manager";
|
||||
import { client } from "../../../shared/api";
|
||||
import type { IUserInfo } from "../../../shared/store";
|
||||
import { useUserStore } from "../../../shared/lib";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "../../../shared/config";
|
||||
import { ENDPOINT, AUTH_USER_ID, CLAIM_NAME } from "../../../shared/config";
|
||||
import { isAtLoginRedirectUrl } from "@common/network";
|
||||
import { getCookie } from "../../../shared/lib";
|
||||
|
||||
@@ -26,6 +28,11 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
}
|
||||
|
||||
const token = getCookie(CLAIM_NAME);
|
||||
if (!token || tokenManager.isExpiredToken()) {
|
||||
await LoginApi.silentRefresh();
|
||||
}
|
||||
|
||||
const response = await client.get<IUserInfo>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/info/me`,
|
||||
);
|
||||
@@ -33,6 +40,18 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const token = getCookie(CLAIM_NAME);
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
|
||||
if (!token || !userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
tokenManager.restoreRefreshTimer(() => LoginApi.silentRefresh());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const isJwtTestPage = pathname?.startsWith("/jwttest");
|
||||
(async () => {
|
||||
try {
|
||||
await authCheck();
|
||||
@@ -40,7 +59,7 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
console.error(error);
|
||||
setUserInfo(null);
|
||||
|
||||
if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) return;
|
||||
if (isAtLoginRedirectUrl(LOGIN_PAGE_URL) || isJwtTestPage) return;
|
||||
|
||||
// const hostname = window.location.hostname;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config";
|
||||
import { AUTH_USER_ID, CLAIM_NAME, REFRESH_TOKEN } from "@/src/shared/config";
|
||||
import { getCookie, removeCookie, setCookie } from "@/src/shared/lib/cookie";
|
||||
|
||||
type JwtPayload = {
|
||||
@@ -11,6 +11,7 @@ type JwtPayload = {
|
||||
export class TokenManager {
|
||||
private static instance: TokenManager;
|
||||
private refreshTimer: NodeJS.Timeout | null = null;
|
||||
private static readonly REFRESH_BUFFER_MS = 60000; // 60초
|
||||
|
||||
private constructor() {}
|
||||
|
||||
@@ -79,20 +80,71 @@ export class TokenManager {
|
||||
removeCookie(key);
|
||||
}
|
||||
|
||||
public setupRefreshTimer(
|
||||
callback: () => Promise<void>,
|
||||
expirationTime: number,
|
||||
): void {
|
||||
private handleRefreshFailure(error: unknown, message: string): void {
|
||||
console.error(message, error);
|
||||
this.clearAuthToken();
|
||||
}
|
||||
|
||||
public setupRefreshTimer(callback: () => Promise<void>): void {
|
||||
if (this.refreshTimer) {
|
||||
clearTimeout(this.refreshTimer);
|
||||
this.refreshTimer = null;
|
||||
}
|
||||
|
||||
const remainingMs = this.getRemainingMsFromToken(CLAIM_NAME);
|
||||
if (remainingMs === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (remainingMs <= 0) {
|
||||
this.clearAuthToken();
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshDelay = Math.max(
|
||||
remainingMs - TokenManager.REFRESH_BUFFER_MS,
|
||||
0,
|
||||
);
|
||||
|
||||
this.refreshTimer = setTimeout(() => {
|
||||
callback().catch((error) => {
|
||||
console.error("Silent refresh failed:", error);
|
||||
this.clearAuthToken();
|
||||
this.handleRefreshFailure(error, "Silent refresh failed:");
|
||||
});
|
||||
}, expirationTime - 60000);
|
||||
}, refreshDelay);
|
||||
}
|
||||
|
||||
public restoreRefreshTimer(callback: () => Promise<void>): void {
|
||||
if (this.refreshTimer) {
|
||||
clearTimeout(this.refreshTimer);
|
||||
this.refreshTimer = null;
|
||||
}
|
||||
|
||||
const remainingMs = this.getRemainingMsFromToken(CLAIM_NAME);
|
||||
if (remainingMs === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (remainingMs <= 0) {
|
||||
callback().catch((error) => {
|
||||
this.handleRefreshFailure(
|
||||
error,
|
||||
"Silent refresh failed during restore:",
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (remainingMs <= TokenManager.REFRESH_BUFFER_MS) {
|
||||
callback().catch((error) => {
|
||||
this.handleRefreshFailure(
|
||||
error,
|
||||
"Silent refresh failed during restore:",
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupRefreshTimer(callback);
|
||||
}
|
||||
|
||||
public clearAuthToken(): void {
|
||||
@@ -102,6 +154,7 @@ export class TokenManager {
|
||||
}
|
||||
|
||||
this.removeToken(CLAIM_NAME);
|
||||
this.removeToken(REFRESH_TOKEN);
|
||||
this.removeToken(AUTH_USER_ID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,4 +82,4 @@ export const ENDPOINT = {
|
||||
|
||||
};
|
||||
|
||||
export const JWT_EXPIRATION_TIME = 1800000;
|
||||
export const JWT_EXPIRATION_TIME = 3600000;
|
||||
|
||||
Reference in New Issue
Block a user