인증/인가를 위해 쿠키에 토큰을 저장하는 방식을 채택했고 이를 구현했다.
의식의 흐름은 아래와 같았다.
1. 로그인 시에 토큰을 발행하여 쿠키에 accessToken과 refreshToken을 할당?한다.
2. 특정 페이지에 접근할 때 accessToken을 검사하여 파기되었다면 재발행해준다.
3. refreshToken을 검사했을 때 이 마저도 파기되었다면 로그인 페이지로 이동 시킨다.
4. 클라이언트에서 토큰을 어떻게 활용할지 구상하여 계획을 세운다.
import { SignJWT } from "jose";
import { NextRequest, NextResponse } from "next/server";
const SECRET_KEY = new TextEncoder().encode(process.env.SECRET_KEY!);
const REFRESH_SECRET_KEY = new TextEncoder().encode(
process.env.REFRESH_SECRET_KEY!
);
export async function POST(req: NextRequest) {
try {
const { userId, userPassword } = await req.json();
const response = await fetch(
`http://localhost:5000/user?userId=${userId}&userPassword=${userPassword}`
);
if (!response.ok) {
return NextResponse.json(
{ error: "유저 DB에 접근을 실패했습니다." },
{ status: 500 }
);
}
const users = await response.json();
if (users.length === 0) {
return NextResponse.json(
{ error: "유저 정보가 없습니다." },
{ status: 401 }
);
}
const accessToken = await new SignJWT({ user: users[0] })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("15m")
.sign(SECRET_KEY);
const refreshToken = await new SignJWT({ user: users[0] })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("7d")
.sign(REFRESH_SECRET_KEY);
const responseWithCookies = NextResponse.json({
message: "로그인 성공",
});
responseWithCookies.cookies.set("accessToken", accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 15 * 60,
path: "/",
});
responseWithCookies.cookies.set("refreshToken", refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 7 * 24 * 60 * 60,
path: "/",
});
return responseWithCookies;
} catch (error) {
console.error("에러 :", error);
return NextResponse.json(
{ error: "예기치 못한 에러 발생" },
{ status: 500 }
);
}
}
위의 에러는 내 살면서 처음봤다.
바로 검색해보니 스텍 오버 플로우의 어떤 형님이 아래와 같은 코멘트를 달아놓으셨음.
jsonwebtoken은 Node.js와 다르므로 엣지 환경에서의 실행을 지원하지 않습니다. jsonwebtoken 대신 Vercel의 엣지 런타임을 지원하는 jose를 사용할 수 있습니다.
즉, Next.js에선 JWT가 호환되기 힘들다, jose 라이브러리로 구현하는 것을 추천한다라는 의미로 적어놓으신듯 함.
그래서 작성해둔 코드를 jose 라이브러리로 리팩토링 하여 다시 구현하였음.
axios를 썻다면 interceptor를 써서 하는 방식이 글로 많이 존재하더라.
하지만 나는 이번 프로젝트에서 axios가 아닌 fetch를 쓰기로 했으니 미들웨어를 구현하여 토큰 검사를 해야겠다는 생각을 초기부터 가지고 있었음.
코드는 아래와 같음.
import { SignJWT, jwtVerify } from "jose";
import { NextRequest, NextResponse } from "next/server";
const SECRET_KEY = new TextEncoder().encode(process.env.SECRET_KEY!);
const REFRESH_SECRET_KEY = new TextEncoder().encode(
process.env.REFRESH_SECRET_KEY!
);
export async function middleware(req: NextRequest) {
const accessToken = req.cookies.get("accessToken");
const refreshToken = req.cookies.get("refreshToken");
// Error: The edge runtime does not support Node.js 'crypto' module.
console.log("req.url", req.url);
if (!accessToken && !refreshToken) {
return NextResponse.redirect(new URL("/", req.url));
}
try {
if (accessToken) {
await jwtVerify(accessToken!.value, SECRET_KEY);
// 미들웨어에서 사용되는 메서드로 요청을 정상적으로 계속 처리하라는 지시를 서버에 전달
// -> 다음 단계 진행시켜!
return NextResponse.next();
}
} catch (error) {
console.log("Access token expired");
}
try {
if (refreshToken) {
const { payload } = await jwtVerify(
refreshToken!.value,
REFRESH_SECRET_KEY
);
const newAccessToken = await new SignJWT({ user: payload.user })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("15m")
.sign(SECRET_KEY);
const response = NextResponse.next();
response.cookies.set("accessToken", newAccessToken, {
httpOnly: true,
maxAge: 15 * 60,
});
return response;
}
} catch (error) {
console.error("Refresh token expired or invalid");
return NextResponse.redirect(new URL("/user/login", req.url));
}
return NextResponse.next();
}
// 후에 작업하면서 수정할 것
export const config = {
matcher: ["/user/:path*", "/"],
};
root에 위치시키라해서 최상단에 파일을 위치시켰더니 작동을 안하더라 ㅡㅡ
src 폴더 아래에 위치시켜야 했던 것이였음, src폴더 가 없을 시엔 최상단이 맞다고 생각함
NextResponse.next()는 뭐니?
미들웨어는 supabase에서 짜준 코드, 과거 node.js 환경에서 짠 코드 빼고는 처음 짜보는거라 이 친구가 생소했음.
그래서 문서를 읽어보니 Next.js 미들웨어에서만 사용되는 메서드로 아래의 뜻을 가지고있다고 이해함
요청을 정상적으로 계속 처리하라는 지시를 서버에 전달 -> 다음 단계 진행시켜!!!!
경영좌의 진행시켜!인거임
솔직히 어떤 코드던 검색과 gpt가 함께라면 무서울게 없긴함 그런데 이 코드를 이해하는 시간과 더 나은 방안이 없는지에 대한 고민을 겸하니 확실히 처음 접하는 코드를 짜는데 쉽다고 느껴지진 않더라.
이번에 한번 짜 봤으니 다음엔 좀 더 빠른 시간 안에 짤 수 있을 듯 싶다.
아 물론 작업하다보면 에러가 쏟아지고 이 상태에서 로직이 끝날 것 같진않다.
하지만 나는 모르는 부분이니 후에 다시 생각해보도록 하자.
보안적인 부분은 신경을 쓰진 않았다 왜냐? 나 프론트엔드 개발자 ㅋ 포폴용으로 짜는 중이므로 너무 딥 다이브를 하진 않았음.