authjs 라이브러리 (구. NextAuth)를 사용하여 구글 로그인 기능을 구현하고, middleware를 사용하여 조건부 라우트를 설정하는 방법에 대해 알아보자.
npm install next-auth
api/auth/[…nextauth]
폴더 생성 후 해당 폴더에 route.ts
파일 생성/api/auth
로 시작하는 모든 경로에 대한 요청 처리 가능handler
변수를 선언하고 next-auth
가 리턴하는 값을 할당api/auth
경로에 들어오는 GET 요청과 POST 요청을 처리할 수 있도록 TypeScript as
문법을 사용해 export
api/auth/[…nextauth]/route.ts
import NextAuth from "next-auth";
const handler = NextAuth({});
export { handler as GET, handler as POST };
NEXTAUTH_URL
: 웹 사이트 기본 주소 할당NEXTAUTH_SECRET
: 인증키를 암호화하고 서명하는데 사용할 암호 문자열rand
: 무작위 값 생성base64
: Base64 알고리즘 사용하여 인코딩32
: 문자열 크기 (바이트)openssl rand -base64 32
NEXTAUTH_SECRET
환경 변수로 사용한다..env
NEXTAUTH_URL="http:localhost:3000"
NEXTAUTH_SECRET="This is an example"
구글 클라우드 플랫폼 계정을 생성하고 설정을 진행해보자.
앱 등록 수정
https://{YOUR_DOMAIN}/api/auth/callback/google
http://localhost:3000/api/auth/callback/google
.env
GOOGLE_CLIENT_ID="YOUR CLIENT ID"
GOOGLE_CLIENT_SECRET="YOUR CLIENT SECRET"
api/auth/[…nextauth]/route.ts
파일로 이동하여 공식 문서를 참고하여 코드 구성GOOGLE_CLIENT_ID
, GOOGLE_CLIENT_SECRET
뒤에 느낌표 를 붙인 이유는 provider 속성 값이 undefined
를 허용하지 않기 때문api/auth/[…nextauth]/route.ts
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
const handler = NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
});
export { handler as GET, handler as POST };
app/NavBar.tsx
import React from "react";
import Link from "next/link";
const NavBar = () => {
return (
<div>
<Link className="mr-5" href="/">
Next.js
</Link>
<Link href="/users">Users</Link>
<Link href="/api/auth/signin">Google Login</Link>
</div>
);
};
export default NavBar;
next-auth.session-token
을 확인할 수 있다.한번 확인해보자.
auth
폴더에 token
이라는 폴더를 생성하고 route.tsx
파일을 생성한다.next-auth/jwt
에서 제공하는 getToken
함수를 사용하여 JSON 형태로 token
을 리턴하는 함수 작성app/api/auth/token/route.tsx
import { getToken } from "next-auth/jwt";
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const token = await getToken({ req: request });
return NextResponse.json(token);
}
sub
, iat
는 발행시간을 나타내며 기본적으로 토큰은 30일 동안 유효하다.app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import NavBar from "./NavBar";
import { SessionProvider } from "next-auth/react";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" data-theme="winter">
<body className={inter.className}>
<SessionProvider>
<NavBar />
<main className="p-5">{children}</main>
</SessionProvider>
</body>
</html>
);
}
use client
지시문을 추가하고 다시 테스트하면 또 다른 오류 발생 ^^;SessionProvider
컴포넌트를 만들어서 해결할 수 있다.AuthProvider
컴포넌트 생성app/auth/provider.tsx
"use client";
import { ReactNode } from "react";
import { SessionProvider } from "next-auth/react";
const AuthProvider = ({ children }: { children: ReactNode }) => {
return <SessionProvider>{children}</SessionProvider>;
};
export default AuthProvider;
SessionProvider
대신에 AuthProvider
사용use client
지시문 삭제app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import NavBar from "./NavBar";
import AuthProvider from "./auth/provider";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" data-theme="winter">
<body className={inter.className}>
<AuthProvider>
<NavBar />
<main className="p-5">{children}</main>
</AuthProvider>
</body>
</html>
);
}
AuthProvider
는 자식 요소인 내비게이션 컴포넌트로 세션 정보를 제공한다.useSession
훅을 사용하여 세션에 접근할 수 있다.useSession
훅이 반환하는 객체에는 세션의 상태를 나타내는 status
와 데이터를 포함하는 data
프로퍼티가 포함되어 있다.클라이언트 컴포넌트 NavBar에서 직접 확인해보자!
useSession
가 반환하는 data 속성을 session으로 수정하겠다.authenticated
: 인증됨loading
: 로딩중unauthenticated
: 비인증됨app/NavBar.tsx
"use client"; // 클라이언트 컴포넌트
import React from "react";
import Link from "next/link";
import { useSession } from "next-auth/react";
const NavBar = () => {
const { status, data: session } = useSession();
// 로딩 중인 경우
if (status === "loading") {
return null;
}
return (
<div className="flex">
<Link className="mr-5" href="/">
Next.js
</Link>
<Link href="/users" className="mr-5">
Users
</Link>
{/* 인증 된 경우 사용자 이름 출력 */}
{status === "authenticated" && <div>Hello {session.user!.name}</div>}
{/* 인증되지 않은 경우 로그인 버튼 출력 */}
{status === "unauthenticated" && (
<Link href="/api/auth/signin">Google Login</Link>
)}
</div>
);
};
export default NavBar;
GetServerSession
함수를 사용한다.import
해서 사용할 수 있도록 설정 파일 수정api/auth/[…nextauth]/route.ts
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
export const authOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
app
폴더의 page
파일에 GetServerSession
함수를 호출하고 session 가져오기app/page.tsx
import Link from "next/link";
import ProductCard from "@/components/ProductCard";
import { getServerSession } from "next-auth";
import { authOptions } from "./api/auth/[...nextauth]/route";
export default async function Home() {
const session = await getServerSession(authOptions);
return (
<main>
<h1>{session && <span>{session.user!.name}님</span>} 반갑습니다.</h1>
<Link href="/users">Users</Link>
<ProductCard />
</main>
);
}
app/NavBar.tsx
"use client";
import React from "react";
import Link from "next/link";
import { useSession } from "next-auth/react";
const NavBar = () => {
const { status, data: session } = useSession();
// 로딩 중인 경우
if (status === "loading") {
return null;
}
return (
<div className="flex">
<Link className="mr-5" href="/">
Next.js
</Link>
<Link href="/users" className="mr-5">
Users
</Link>
{/* 인증 된 경우 사용자 이름과 로그아웃 링크 출력 */}
{status === "authenticated" && (
<div>
<span className="mr-2">Hello {session.user!.name}</span>
<Link href="/api/auth/signout">Sign Out</Link>
</div>
)}
{/* 인증되지 않은 경우 로그인 버튼 출력 */}
{status === "unauthenticated" && (
<Link href="/api/auth/signin">Google Login</Link>
)}
</div>
);
};
export default NavBar;
미들웨어를 사용하여 로그인한 사용자만 접근할 수 있는 컴포넌트를 생성해보자.
컴포넌트를 렌더링 하기 전 사용자 세션을 확인하고, 사용자 세션에 인증된 토큰이 없는 경우 홈페이지로 리다이렉트 하고 인증 토큰이 있는 경우 특정 컴포넌트에 접근할 수 있도록 구현하면 된다.
middleware
라는 지정된 파일명을 앱 폴더에 생성하면, 서버에 요청이 들어왔을 때 해당 middleware
파일을 먼저 실행하고 다음 단계로 넘어간다.리다이렉트할 페이지를 만들고, 미들웨어 파일을 작성해보자.
app/new-page/page.tsx
import React from "react";
const NewPage = () => {
return <div>NewPage</div>;
};
export default NewPage;
middleware
함수를 정의하고 NextRequest
인자 전달NextResponse
사용하여 /new-page
로 리다이렉트 하도록 작성middleware.ts
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL("/new-page", request.url));
}
*
패턴은 0개 또는 그 이상 일치하는 경우를 의미한다.middleware.ts
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL("/new-page", request.url));
}
export const config = {
matcher: ["/users/:id*"],
};
http://localhost:3000/users
페이지로 접속하면 http://localhost:3000/new-page
로 리다이렉션 되는 것을 볼 수 있다.middleware.ts
import { NextRequest, NextResponse } from "next/server";
import middleware from "next-auth/middleware";
export default middleware;
export const config = {
matcher: ["/users/:id*"],
};
default
키워드를 사용하면 코드를 단순화할 수 있다.middleware.ts
export { default } from "next-auth/middleware";
export const config = {
matcher: ["/users/:id*"],
};
우와... 이건 교과서인가요? 친절한 설명 감사합니다.