JWT를 사용하여 Next.js에서 토큰 인증을 구현하는 방법

토큰 인증은 무단 액세스로부터 웹 및 모바일 애플리케이션을 보호하는 데 사용되는 널리 사용되는 전략입니다. Next.js에서는 Next-auth에서 제공하는 인증 기능을 활용할 수 있습니다.

또는 JWT(JSON 웹 토큰)를 사용하여 사용자 지정 토큰 기반 인증 시스템을 개발하도록 선택할 수 있습니다. 이렇게 하면 인증 논리를 더 효과적으로 제어할 수 있습니다. 기본적으로 프로젝트 요구 사항에 정확하게 일치하도록 시스템을 사용자 정의합니다.

Next.js 프로젝트 설정

시작하려면 터미널에서 아래 명령을 실행하여 Next.js를 설치하세요.

 npx create-next-app@latest next-auth-jwt --experimental-app 

이 가이드에서는 앱 디렉터리가 포함된 Next.js 13을 활용합니다.

다음으로 노드 패키지 관리자인 npm을 사용하여 프로젝트에 이러한 종속성을 설치합니다.

 npm install jose universal-cookie 

호세 JSON 웹 토큰 작업을 위한 유틸리티 세트를 제공하는 JavaScript 모듈입니다. 유니버설 쿠키 종속성은 클라이언트 측 환경과 서버 측 환경 모두에서 브라우저 쿠키를 사용하는 간단한 방법을 제공합니다.

로그인 양식 사용자 인터페이스 만들기

src/app 디렉터리를 열고 새 폴더를 만들고 이름을 login으로 지정합니다. 이 폴더 안에 새 page.js 파일을 추가하고 아래 코드를 포함하세요.

 "use client";
import { useRouter } from "next/navigation";

export default function LoginPage() {
  return (
    <form onSubmit={handleSubmit}>
      <label>
        Username:
        <input type="text" name="username" />
      </label>
      <label>
        Password:
        <input type="password" name="password" />
      </label>
      <button type="submit">Login</button>
    </form>
  );
}

위의 코드는 사용자가 사용자 이름과 비밀번호를 입력할 수 있도록 브라우저에 간단한 로그인 양식을 렌더링하는 로그인 페이지 기능 구성 요소를 만듭니다.

코드의 use client 문은 앱 디렉터리의 서버 전용 코드와 클라이언트 전용 코드 사이에 경계가 선언되도록 합니다.

이 경우 로그인 페이지의 코드, 특히 handlerSubmit 함수가 클라이언트에서만 실행된다는 것을 선언하는 데 사용됩니다. 그렇지 않으면 Next.js에서 오류가 발생합니다.

  Apple AirPods의 일반적인 문제를 해결하는 방법

이제 handlerSubmit 함수에 대한 코드를 정의해 보겠습니다. 기능 구성 요소 내에 다음 코드를 추가합니다.

 const router = useRouter();

const handleSubmit = async (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);
    const username = formData.get("username");
    const password = formData.get("password");
    const res = await fetch("/api/login", {
      method: "POST",
      body: JSON.stringify({ username, password }),
    });
    const { success } = await res.json();
    if (success) {
      router.push("/protected");
      router.refresh();
    } else {
      alert("Login failed");
    }
 };

로그인 인증 로직을 관리하기 위해 이 함수는 로그인 양식에서 사용자 자격 증명을 캡처합니다. 그런 다음 확인을 위해 사용자 세부 정보를 전달하는 API 엔드포인트에 POST 요청을 보냅니다.

자격 증명이 유효하면 로그인 프로세스가 성공했음을 나타냅니다. API는 응답으로 성공 상태를 반환합니다. 그런 다음 핸들러 기능은 Next.js의 라우터를 사용하여 사용자를 지정된 URL(이 경우 보호된 경로)로 이동합니다.

로그인 API 끝점 정의

src/app 디렉터리 안에 새 폴더를 만들고 이름을 api로 지정합니다. 이 폴더 안에 새 login/route.js 파일을 추가하고 아래 코드를 포함하세요.

 import { SignJWT } from "jose";
import { NextResponse } from "next/server";
import { getJwtSecretKey } from "@/libs/auth";

export async function POST(request) {
  const body = await request.json();
  if (body.username === "admin" && body.password === "admin") {
    const token = await new SignJWT({
      username: body.username,
    })
      .setProtectedHeader({ alg: "HS256" })
      .setIssuedAt()
      .setExpirationTime("30s")
      .sign(getJwtSecretKey());
    const response = NextResponse.json(
      { success: true },
      { status: 200, headers: { "content-type": "application/json" } }
    );
    response.cookies.set({
      name: "token",
      value: token,
      path: "https://www.makeuseof.com/",
    });
    return response;
  }
  return NextResponse.json({ success: false });
}

이 API의 기본 작업은 모의 데이터를 사용하여 POST 요청에 전달된 로그인 자격 증명을 확인하는 것입니다.

성공적으로 확인되면 인증된 사용자 세부정보와 연결된 암호화된 JWT 토큰을 생성합니다. 마지막으로 응답 쿠키의 토큰을 포함하여 클라이언트에 성공적인 응답을 보냅니다. 그렇지 않으면 실패 상태 응답을 반환합니다.

토큰 확인 로직 구현

토큰 인증의 초기 단계는 성공적인 로그인 프로세스 후 토큰을 생성하는 것입니다. 다음 단계는 토큰 확인을 위한 논리를 구현하는 것입니다.

  iOS의 App Store에서 선물을 보내는 방법

기본적으로 Jose 모듈에서 제공하는 jwtVerify 기능을 사용하여 후속 HTTP 요청과 함께 전달된 JWT 토큰을 확인합니다.

src 디렉터리에 새 libs/auth.js 파일을 만들고 아래 코드를 포함합니다.

 import { jwtVerify } from "jose";

export function getJwtSecretKey() {
  const secret = process.env.NEXT_PUBLIC_JWT_SECRET_KEY;
  if (!secret) {
    throw new Error("JWT Secret key is not matched");
  }
  return new TextEncoder().encode(secret);
}

export async function verifyJwtToken(token) {
  try {
    const { payload } = await jwtVerify(token, getJwtSecretKey());
    return payload;
  } catch (error) {
    return null;
  }
}

비밀 키는 토큰 서명 및 확인에 사용됩니다. 서버는 디코딩된 토큰 서명을 예상 서명과 비교함으로써 제공된 토큰이 유효한지 효과적으로 확인하고 궁극적으로 사용자의 요청을 승인할 수 있습니다.

루트 디렉터리에 .env 파일을 만들고 다음과 같이 고유한 비밀 키를 추가합니다.

 NEXT_PUBLIC_JWT_SECRET_KEY=your_secret_key 

보호된 경로 생성

이제 인증된 사용자만 액세스할 수 있는 경로를 만들어야 합니다. 이렇게 하려면 src/app 디렉터리에 새 protected/page.js 파일을 생성하세요. 이 파일 안에 다음 코드를 추가합니다.

 export default function ProtectedPage() {
    return <h1>Very protected page</h1>;
  }

인증 상태를 관리하기 위한 후크 생성

src 디렉터리에 새 폴더를 만들고 이름을 Hooks로 지정합니다. 이 폴더 안에 새 useAuth/index.js 파일을 추가하고 아래 코드를 포함합니다.

 "use client" ;
import React from "react";
import Cookies from "universal-cookie";
import { verifyJwtToken } from "@/libs/auth";

export function useAuth() {
  const [auth, setAuth] = React.useState(null);

  const getVerifiedtoken = async () => {
    const cookies = new Cookies();
    const token = cookies.get("token") ?? null;
    const verifiedToken = await verifyJwtToken(token);
    setAuth(verifiedToken);
  };
  React.useEffect(() => {
    getVerifiedtoken();
  }, []);
  return auth;
}

이 후크는 클라이언트 측의 인증 상태를 관리합니다. verifyJwtToken 함수를 사용하여 쿠키에 있는 JWT 토큰의 유효성을 가져와 확인한 다음, 인증된 사용자 세부정보를 인증 상태로 설정합니다.

이를 통해 다른 구성요소가 인증된 사용자의 정보에 접근하고 활용할 수 있도록 합니다. 이는 인증 상태에 따른 UI 업데이트, 후속 API 요청, 사용자 역할에 따른 다양한 콘텐츠 렌더링과 같은 시나리오에 필수적입니다.

이 경우 후크를 사용하여 사용자의 인증 상태에 따라 홈 경로에서 다양한 콘텐츠를 렌더링합니다.

  Google 문서에서 페이지마다 다른 헤더를 만드는 방법

고려할 수 있는 또 다른 접근 방식은 Redux Toolkit을 사용하여 상태 관리를 처리하거나 Jotai와 같은 상태 관리 도구를 사용하는 것입니다. 이 접근 방식은 구성 요소가 인증 상태 또는 기타 정의된 상태에 대한 전역 액세스를 얻을 수 있도록 보장합니다.

app/page.js 파일을 열고 상용구 Next.js 코드를 삭제한 후 다음 코드를 추가하세요.

 "use client" ;

import { useAuth } from "@/hooks/useAuth";
import Link from "next/link";
export default function Home() {
  const auth = useAuth();
  return <>
           <h1>Public Home Page</h1>
           <header>
              <nav>
                {auth ? (
                   <p>logged in</p>
                ) : (
                  <Link href="https://wilku.top/login">Login</Link>
                )}
              </nav>
          </header>
  </>
}

위의 코드는 useAuth 후크를 활용하여 인증 상태를 관리합니다. 이를 통해 사용자가 인증되지 않은 경우 로그인 페이지 경로에 대한 링크가 포함된 공개 홈 페이지를 조건부로 렌더링하고 인증된 사용자에 대한 단락을 표시합니다.

보호된 경로에 대한 승인된 액세스를 적용하기 위해 미들웨어 추가

src 디렉터리에 새로운 middleware.js 파일을 생성하고 아래 코드를 추가하세요.

 import { NextResponse } from "next/server";
import { verifyJwtToken } from "@/libs/auth";

const AUTH_PAGES = ["https://wilku.top/login"];

const isAuthPages = (url) => AUTH_PAGES.some((page) => page.startsWith(url));

export async function middleware(request) {

  const { url, nextUrl, cookies } = request;
  const { value: token } = cookies.get("token") ?? { value: null };
  const hasVerifiedToken = token && (await verifyJwtToken(token));
  const isAuthPageRequested = isAuthPages(nextUrl.pathname);

  if (isAuthPageRequested) {
    if (!hasVerifiedToken) {
      const response = NextResponse.next();
      response.cookies.delete("token");
      return response;
    }
    const response = NextResponse.redirect(new URL(`/`, url));
    return response;
  }

  if (!hasVerifiedToken) {
    const searchParams = new URLSearchParams(nextUrl.searchParams);
    searchParams.set("next", nextUrl.pathname);
    const response = NextResponse.redirect(
      new URL(`/login?${searchParams}`, url)
    );
    response.cookies.delete("token");
    return response;
  }

  return NextResponse.next();

}
export const config = { matcher: ["https://wilku.top/login", "/protected/:path*"] };

이 미들웨어 코드는 가드 역할을 합니다. 사용자가 보호된 페이지에 액세스하려고 할 때 인증을 받고 해당 경로에 액세스할 수 있는 권한이 있는지 확인하고, 승인되지 않은 사용자를 로그인 페이지로 리디렉션합니다.

Next.js 애플리케이션 보안

토큰 인증은 효과적인 보안 메커니즘입니다. 그러나 이것이 무단 액세스로부터 애플리케이션을 보호하는 데 사용할 수 있는 유일한 전략은 아닙니다.

역동적인 사이버 보안 환경에 맞춰 애플리케이션을 강화하려면 잠재적인 보안 허점과 취약성을 전체적으로 해결하여 철저한 보호를 보장하는 포괄적인 보안 접근 방식을 채택하는 것이 중요합니다.