토큰 인증을 활용하는 것은 무단 침입으로부터 웹 및 모바일 애플리케이션을 보호하는 일반적인 관행입니다. Next.js를 사용하면 Next-auth에서 제공하는 기본 제공 인증 기능을 활용하여 애플리케이션의 민감한 데이터를 효과적으로 보호할 수 있습니다.

또 다른 선택은 토큰을 생성하는 방법으로 JSON 웹 토큰(JWT)을 활용하여 맞춤형 토큰 기반 인증 솔루션을 설계하고 구현하는 것입니다. 이 접근 방식을 사용하면 인증 프로세스를 보다 유연하게 제어할 수 있으므로 프로젝트의 특정 요구 사항을 충족하도록 세밀하게 조정할 수 있습니다.

Next.js 프로젝트 설정

프로세스를 시작하려면 터미널에 제공된 명령을 사용하여 Next.js를 설치하여 진행할 수 있습니다.

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

이 가이드에서는 해당 범위 내의 전체 애플리케이션 디렉토리 구조를 포괄하는 최신 버전의 Next.js, 즉 v13을 사용합니다.

프로젝트의 원활한 기능을 보장하려면 Node.js 애플리케이션을 위해 특별히 설계된 필수 패키지 관리자인 npm의 서비스를 사용하여 일련의 필수 구성 요소를 통합해야 합니다.

 npm install jose universal-cookie 

Jose 은 JSON 웹 토큰 작업을 위한 일련의 유틸리티를 제공하는 JavaScript 모듈이며, 유니버설 쿠키 종속성은 클라이언트 측과 서버 측 환경 모두에서 브라우저 쿠키로 작업할 수 있는 간단한 방법을 제공합니다.

이 프로젝트의 소스 코드는 지정된 URL로 이동하여 액세스할 수 있는 GitHub 리포지토리를 통해 액세스할 수 있습니다.

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

더 나은 도움을 드릴 수 있도록 조잡한 영어로 된 원문을 제공해 주세요.

 "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>
  );
}

위의 코드는 사용자 인터페이스 요소, 특히 최종 사용자의 입력을 캡처하기 위해 설계된 로그인 화면을 생성합니다. 이 입력은 ‘사용자 이름’과 ‘비밀번호’로 지정된 두 개의 필드에 영숫자를 입력하는 것으로 구성됩니다. 이 설계의 목적은 텍스트 입력 수단을 통해 필요한 자격 증명을 제공하여 개인이 자신의 신원을 인증할 수 있도록 하는 것입니다.

소스 코드 내에서 클라이언트 명령문 구현을 활용하면 애플리케이션의 디렉토리 구조 내에서 서버 측에서만 실행되는 구성 요소와 클라이언트 측에서만 실행되는 구성 요소를 명확하게 구분할 수 있습니다.

앞서 언급한 문장은 로그인 페이지 내에서 핸들 제출 함수의 구현이 클라이언트 측에서만 수행되는 것으로 제한된다는 것을 의미합니다. 반대로 Next.js가 해당 함수를 실행하려고 시도하면 잘못된 결과가 발생할 수 있습니다.

이 글도 확인해 보세요:  Reqwest로 Rust에서 HTTP 요청 만들기

이제 주어진 기능 컴포넌트 내에서 `handleSubmit` 함수를 구현하는 데 필요한 프로그래밍 로직을 간결하고 정교하게 표현해 봅시다. 이를 위해 해당 함수형 컴포넌트 내에 다음 코드 줄을 통합합니다.

 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의 라우팅 기능을 활용하여 사용자를 지정된 위치(이 시나리오에서는 제한된 경로)로 리디렉션합니다.

로그인 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: "/",
    });
    return response;
  }
  return NextResponse.json({ success: false });
}

이 애플리케이션 프로그래밍 인터페이스(API)의 주요 책임 중 하나는 시뮬레이션된 데이터를 활용하여 사후 요청을 통해 제출된 사용자 정보를 인증하는 것입니다.

철저한 검증을 통해 확인을 획득하면, 시스템은 확인된 사용자의 관련 정보에 해당하는 안전하게 인코딩된 JSON 웹 토큰(JWT)을 생성합니다. 그 후 프로세스 완료를 나타내는 응답을 전송하고 해당 토큰을 클라이언트로 다시 전송되는 세션 쿠키에 포함시켜 액세스 목적으로 사용하거나, 인증에 실패하면 실패 상태 코드와 함께 인증 실패를 알리는 메시지를 전달합니다.

토큰 확인 로직 구현

로그인에 성공하면 인증 프로세스의 첫 번째 단계로 인증 토큰 생성이 시작됩니다. 그 후 해당 토큰에 대한 유효성 검사 프로토콜 구현이 수행됩니다.

본질적으로 Jose 모듈에서 제공하는 jwtVerify 함수는 후속 HTTP 요청에서 전달되는 JWT 토큰을 검증하는 데 사용됩니다.

이 글도 확인해 보세요:  VPN 제공 업체가 말하는 '군사급 암호화'란 무엇인가요?

“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;
  }
}

토큰의 신뢰성을 보장하기 위해 서명 및 확인 목적으로 비밀 키가 사용됩니다. 서버는 해독된 토큰 서명과 예상 서명을 비교하여 제공된 토큰의 적법성을 확인한 후 사용자 요청에 대한 권한을 부여할 수 있습니다.

환경 변수 파일을 생성하는 프로세스에는 기본 위치 내에 환경 변수 파일을 설정하는 과정이 포함되며, 그 후에는 인증 목적으로 단일하고 독점적인 비밀 코드로 채워야 합니다.

 NEXT_PUBLIC_JWT_SECRET_KEY=your_secret_key 

보호 경로 생성

권한이 부여된 개인만 액세스할 수 있는 경로를 설정하려면 “src/app” 디렉터리 내에 새 page.js 파일을 생성해야 합니다. 이 특정 파일 내에 다음 코드 스니펫을 삽입하세요.

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

인증 상태를 관리하는 훅 생성

애플리케이션 내에 안전한 인증 시스템을 구축하기 위해 먼저 프로젝트의 기본 “src” 디렉터리 내에 “hooks”라는 이름의 전용 하위 디렉터리를 생성해야 합니다. 새로 생성된 “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 토큰을 검색하고 유효성을 검사하여 클라이언트 측에서 인증 상태를 처리하는 역할을 담당합니다. 그 후 인증 상태 내에서 확인된 사용자 정보를 설정합니다.

사용자를 인증하면 다른 시스템 구성 요소가 확인된 신원 정보에 액세스할 수 있습니다. 이러한 기능은 인증 상태에 따라 사용자 인터페이스를 업데이트하거나 추가 API 호출을 실행하거나 사용자의 역할에 따라 차별화된 콘텐츠를 표시하는 등 다양한 상황에서 매우 중요합니다.

이 경우, 후크는 사용자의 인증 상태에 따라 홈 경로에 다양한 콘텐츠를 제시하는 데 활용됩니다.

이 글도 확인해 보세요:  Rust의 제네릭 형식 알아보기

검토해 볼 만한 잠재적 전략으로는 Redux 툴킷을 활용하여 상태를 관리하거나, Jotai와 같은 상태 관리 솔루션을 구현하는 것이 있습니다. 이 방법론을 채택하면 컴포넌트에 인증 상태 및 기타 지정된 상태를 비롯한 중요한 데이터 요소에 대한 전역 액세스 권한이 부여됩니다.

기존의 상용구 Next.js 코드를 삭제하고 아래에 제공된 코드 스니펫을 통합하여 “app/page.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="/login">Login</Link>
                )}
              </nav>
          </header>
  </>
}

이 코드는 useAuth 훅이 제공하는 기능을 활용하여 애플리케이션 내에서 인증 상태를 쉽게 관리할 수 있도록 합니다. 따라서 사용자에게 적절한 자격 증명이 없는 경우 로그인 페이지로 연결되는 URL이 포함된 공개 홈페이지를 표시하고, 로그인에 성공한 사용자에게는 인사 메시지를 표시합니다.

보호된 경로에 대한 인증된 액세스를 적용하는 미들웨어 추가

원문을 제공해 주시면 적절하게 고칠 수 있도록 도와드리겠습니다.

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

const AUTH_PAGES = ["/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: ["/login", "/protected/:path*"] };

미들웨어는 인증 게이트키퍼 역할을 하여 제한된 콘텐츠에 액세스하려는 개인이 성공적으로 인증되었으며 이러한 경로를 통과하는 데 필요한 권한을 보유하고 있는지 확인합니다. 반대로 권한이 없는 사용자는 추가 조치를 위해 로그인 페이지로 이동합니다.

Next.js 애플리케이션 보안

토큰 기반 인증은 강력한 보안 수단이 될 수 있지만, 승인되지 않은 진입으로부터 애플리케이션을 보호하는 유일한 접근 방식은 아닙니다.

끊임없이 진화하는 사이버 위협으로부터 소프트웨어를 효과적으로 보호하려면 가능한 모든 약점과 취약점을 일관된 방식으로 포괄하는 종합적인 보안 전략을 구현하여 강력한 방어 조치를 마련하는 것이 중요합니다.

By 박준영

업계에서 7년간 경력을 쌓은 숙련된 iOS 개발자인 박준영님은 원활하고 매끄러운 사용자 경험을 만드는 데 전념하고 있습니다. 애플(Apple) 생태계에 능숙한 준영님은 획기적인 솔루션을 통해 지속적으로 기술 혁신의 한계를 뛰어넘고 있습니다. 소프트웨어 엔지니어링에 대한 탄탄한 지식과 세심한 접근 방식은 독자에게 실용적이면서도 세련된 콘텐츠를 제공하는 데 기여합니다.