"Bearer Token Authentication" 방식에서 쿠키 기반 인증 시스템으로 마이그레이션
"Bearer Token Authentication" 방식으로 진행을 하던 도중에
게시물을 불러오는 home page 작업 중에 문제가 발생했다
서버에서 게시물을 전송해 주는 함수가
/**
* Get paginated articles
* @auth optional
* @route {GET} /articles
* @queryparam offset number of articles dismissed from the first one
* @queryparam limit number of articles returned
* @queryparam tag
* @queryparam author
* @queryparam favorited
* @returns articles: list of articles
*/
router.get('/articles', auth.optional, async (req: Request, res: Response, next: NextFunction) => {
try {
const result = await getArticles(req.query, req.user?.username);
res.json(result);
} catch (error) {
next(error);
}
});
이렇게 되어있는데 보면 auth가 optional로 되어있다
로그인 상태면 팔로우된 게시물을 보여주고
아니면 기본적으로 팔로우가 false로 되는 함수다
여기서 "Bearer Token Authentication" 으로 하게되면 문제점이
나는 ssr로 하고 싶었는데
클라이언트에서만 token이 존재하고 쿠키를 사용하지 못했기 때문에
서버로 token을 전달할 방법이 없어 강제적으로
'use client'를 사용해야 했다
하지만 곰곰히 사용해 보면 nextjs는 backend의 역할을 수행할 수 있는 프레임워크이기 때문에
next.js 서버에서 쿠키를 발행할 수 있지 않을까? 라는 생각이 들어 검색해 보니 실제로 가능했다
메인 백엔드 서버가 쿠키가 지원이 안된다는 것에 매몰되어 생각을 못 했었다
그래서 나온 구조가
next.js(front)와 next.js 서버 사이에 쿠키를 사용하여 인증 과정을 처리하고
next.js 서버와 메인 백엔드 서버 사이에는 "Bearer Token Authentication" 방식을 사용했다
이렇게 사용하니깐 안되던 것들이 너무 쉽게 풀렸고
https://lim-2.tistory.com/94 이 삽질 하며 복잡하게 짜여있던 코드들이
훨씬 간편하게 만들어졌다 그래서 빠르게 마이그레이션을 완료할 수 있었다.
// utils/authGuard.ts
import { PROTECTED_ROUTES } from "@/constant/auth";
import { NextRequest, NextResponse } from "next/server";
export function authGuard(request: NextRequest) {
const isProtectedPath = PROTECTED_ROUTES.some((path) =>
request.nextUrl.pathname.startsWith(path)
);
if (isProtectedPath) {
const token = request.cookies.get("token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
// 인증이 필요 없거나 인증된 경우 null 반환
return null;
}
다시 middleware로 authguard 구현
export async function updateProfile(
_: UpdateProfileState,
formData: FormData
): Promise<UpdateProfileState> {
try {
const cookieStore = await cookies();
const token = cookieStore.get("token")?.value;
if (!token) {
return {
success: false,
error: createDisplayError("로그인 되지 않았습니다."),
value: { inputData: { username: "", bio: "" } },
};
}
const inputData = {
username: formData.get("username") as string,
bio: formData.get("bio") as string,
};
const response = await fetch(`${API_URL}/user`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
user: {
username: formData.get("username"),
bio: formData.get("bio"),
},
}),
});
const responseData = await response.json();
if (!response.ok) {
return {
success: false,
error: createDisplayError(responseData.error, response.status),
value: { inputData },
};
}
cookieStore.set("token", responseData.user.token, {
...COOCIE_OPTIONS,
});
return {
success: true,
value: { inputData, token: responseData.user.token },
};
} catch (error) {
return {
success: false,
error: { name: "UnexpectedError", message: (error as Error).message },
value: { inputData: { username: "", bio: "" } },
};
}
}
export const COOKIE_OPTIONS = {
httpOnly: process.env.NEXT_PUBLIC_NODE_ENV !== "development",
secure: process.env.NEXT_PUBLIC_NODE_ENV !== "development",
path: "/",
};
httpOnly는 client에서 쿠키에 접근할 수 없게 하는 옵션인데 로컬 개발 환경에서 쿠키가 제대로 들어갔는지 확인하기 위해 저렇게 구성했고
secure는 https 프로토콜 환경이 아니면 쿠키를 전송할 수 없게 하는 옵션이여서 저렇게 구성했다
그리고 path는 특정 경로에서만 접근이 가능하게 하는 옵션인데 /으로 하면 전체 경로에서 접근이 가능하다
const updateProfileWithToken = async (
state: UpdateProfileState,
formData: FormData
) => {
const response = await updateProfile(state, formData, token);
return response;
};
const [state, formAction] = useActionState(updateProfileWithToken, {
success: false,
error: undefined,
value: { inputData: { username: "", bio: "" } },
});
에서 ->
const [state, formAction] = useActionState(updateProfile, {
success: false,
error: undefined,
value: { inputData: { username: "", bio: "" } },
});
억지로 토큰을 끼워맞춰 보내느라 코드가 더러워 졌었는데 굉장히 깔끔해졌다.
쿠키는 요청을 보낼 때 자동으로 같이 보내져서 너무 편하다.
"use client";
import { API_ENDPOINTS } from "@/constant/api";
export const fetchCurrentUser = async () => {
const response = await fetch(API_ENDPOINTS.CURRENT_USER, {
method: "GET",
credentials: "include", // 쿠키 포함
cache: "no-store",
});
if (!response.ok) {
if (response.status === 401) {
}
return null;
}
return response.json();
};
"use client";
import useSWR from "swr";
import { API_ENDPOINTS } from "@/constant/api";
import { fetchCurrentUser } from "@/utils/auth/user";
export const useUser = () => {
const { data, error, isLoading, mutate } = useSWR(
API_ENDPOINTS.CURRENT_USER,
fetchCurrentUser,
{
revalidateOnFocus: false,
revalidateOnMount: true,
}
);
const user = data?.user;
return {
user,
error,
isLoggedIn: !!user,
isLoading,
mutate,
};
};
복잡했던 fetch와 hook도 깔끔하게 정리 되었다
어떻게
next.js(front)와 next.js 서버 사이에 쿠키를 사용하여 인증 과정을 처리하고
next.js 서버와 메인 백엔드 서버 사이에는 "Bearer Token Authentication" 방식을 사용
이 구조해서 클라이언트에 fetch를 할 때 token 값을 담은 요청을 보낼 수 있을까 고민했는데
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export async function GET() {
const cookieStore = await cookies();
const token = cookieStore.get("token")?.value;
if (!token)
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const res = await fetch(process.env.NEXT_PUBLIC_API_URL + "/user", {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
return NextResponse.json(
{ error: "Failed to fetch user" },
{ status: res.status }
);
}
const data = await res.json();
return new NextResponse(JSON.stringify(data), {
headers: {
"Cache-Control": "no-store", // 항상 최신 데이터를 가져옴
},
});
}
api 라우트를 사용하면 간편했다
데이터를 변경하는 작업이면 server action을 사용하면 되고
데이터를 단순히 가져오는 작업이면 그냥 fetch를 해오면 되는데 왜 필요할까 생각했는데
이렇게 중간 다리를 놔줄 수 있다니 너무 좋은 기능이었다
여기서 더 코드들을 발전 시킨다면
리프레쉬 토큰과
토큰 관리 코드들을 좀 더 넣으면 좋을 것 같은데
지금은 시간이 부족해 우선은 진행하고 나중에 다시 봐야겠다.
일단 쿠키 덕분에 ssr을 사용할 수 있게 되었다
export default async function HomePage() {
const cookieStore = await cookies();
const token = cookieStore.get("token")?.value;
const headers = {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
const articlesResponse = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/articles`,
{
headers,
}
);
const tagsResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/tags`, {
headers,
});
const articles = await articlesResponse.json();
const tags = await tagsResponse.json();
console.log(articles);
console.log(tags);
return (
<div className="min-h-screen bg-white dark:bg-gray-900">
<div className="bg-brand-primary dark:bg-gray-800 shadow-inner">
<div className="container mx-auto px-4 py-12 text-center">
<h1 className="font-logo text-5xl md:text-6xl lg:text-7xl text-white mb-4 font-bold">
conduit
</h1>
<p className="text-white text-xl md:text-2xl font-light">
A place to share your knowledge.
</p>
</div>
</div>
</div>
);
}
아직 작업중인 예시 코드