[Next.js]Next-Auth를 이용한 custom 로그인
- -
🐶서론
next.js 13버전이 나오고, 전반적으로 큰 변화가 있어 12버전만을 붙들고 있어서는 안되겠다 ! 싶어 13버전으로 기존프로젝트를 마이그레이션 하기 시작했다.
기존 로그인폼에서는 useState를 이용해 아이디와 비밀번호를 받았기 때문에, 당연히 로그인은 client side rendering이 되어야 한다 생각했다.
next-auth 라는 라이브러리의 존재를 알고있었지만, 우리 프로젝트에는 백엔드가 있었고 JWT토큰을 사용하여 쿠키에 저장해야했기에 굳이? 싶어 사용하지 않았다.
마이그레이션 계획을 하면서, 굉장히 편한 라이브러리 라는것을 알게되었고 기존페이지들의 렌더링방식 결정에 조금 더 도움이 되지 않을까 하는 생각에 도입하게 되었다.
1️⃣ Next-Auth 란 무엇인가?
next-auth는 next.js에서 로그인을 쉽게 구현할 수 있도록 관련기능을 제공하는 3rd Party라이브러리이다.
next-auth 공식문서에서는 다음과 같이 소개하고 있다 (참고)
⭐️ Next-auth 사용방법
API 라우트 구성하기
next auth에서 로그인을 구현하기 위해서는 동적API route를 이용해서 구현해야한다. (서버사이드에서 동작)
pages/api/auth/[...nextauth].ts
import { NextApiRequest } from "next";
import NextAuth, { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export const authOptions: NextAuthOptions ={
providers: [
],
}
export default NextAuth(authOptions)
next.js가 13버전으로 업데이트 되면서 pages 폴더가 없어졌는데, next-auth가 이 부분에 대해서는 아직 업데이트가 진행되지 않은 것같았다. 따라서 나는 src 폴더 내에 pages폴더를 생성하여 다음과 같이 Provider를 설정해주었다.
next-auth가 간편하게 로그인과 로그아웃을 도와주는데에는 next-auth에서 제공하는 signIn/singOut 함수의 역할이 매우 크다. 이 두 함수 모두 내가 설정한 이 api로 요청을 보내는 것.
api route를 이용해서 설정했기 때문에, useSession() Hook(client 단에서 session 정보를 불러올 수 있게함/ 서버단에서 세션정보가 필요할 경우 getServerSession())을 사용하기 위해 최상단에 Provider를 심어주어야한다.
src/context/AuthContext.tsx
"use client";
import { SessionProvider, useSession } from "next-auth/react";
type Props = {
children: React.ReactNode;
};
export default function AuthContext({ children }: Props) {
return <SessionProvider>{children}</SessionProvider>;
}
src/app/layout.tsx
import { Noto_Sans_KR } from "next/font/google";
import ReactQueryProvider from "./ReactQueryProvider";
import Header from "@/components/view/Header";
import AuthContext from "@/context/AuthContext";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={notoSansKr.className}>
<body>
<AuthContext>
<Header />
<ReactQueryProvider>{children}</ReactQueryProvider>
</AuthContext>
</body>
</html>
);
}
Credential Provider 설정하기
next-auth에서는 다양한 provider를 제공하는데, 현재 프로젝트는 우리가 로그인에 필요한 정보를 구성하여 로그인을 구현하고 있기 때문에, credential Provider를 사용했다.
src/pages/api/auth/[...nextauth].ts
import { NextApiRequest } from "next";
import NextAuth, { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export const authOptions: NextAuthOptions ={
providers: [
CredentialsProvider({
id: "user-credentials",
name: "Credentials",
credentials: {
email: { label: "email", type: "email", placeholder: "아이디를 입력하세요" },
password: { label: "Password", type: "password" }
},
async authorize(credentials: Record<any, any> | undefined, req: NextApiRequest | undefined) {
//credentials.status === 401 이면 없는 유저로 signup페이지로 리다이렉트 시키기
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/login`, {
method: "POST",
body: JSON.stringify(credentials),
headers: {"Content-Type": "application/json"}
})
if(res.status === 401){
console.log("에러에러에러 없는 유저다")
throw new Error("로그인 실패")
} else {
const user = await res.json();
return user;
}
}
})
],
pages: {
signIn: '/login',
}
}
export default NextAuth(authOptions)
credentials 를 통해 개발자가 어떤 정보를 로그인 시 받을지 정할 수 있다.
현 프로젝트에서는 유저가 회원가입한 이메일과 비밀번호를 통해 로그인을 하고 있으므로 email, password 두가지를 받고 있다.
next-auth는 로그인페이지나 폼 자체를 구현하지 않아도 기본적인 로그인페이지를 제공하고 있다.
기본적인 로그인페이지가 아닌, 현 프로젝트에서 사용하고 있는 디자인으로 로그인 페이지를 구성하기 위해 나는 signIn : '/login'으로 돌려주었다.
authorize은 credentials에 입력한 값을 통해 로그인이 가능한지 가능하지 않은지 판단하여 제어할 수 있는 함수이다.
나는 백엔드 서버로 입력 받은 값을 보내주어서 응답을 받고, 그 응답에 따라 처리 해야되어서 응답이 401인 경우 에러를 던져 주었고, 아닌 경우는 user 객체를 반환했다.
Callback
src/pages/api/auth/[...nextauth].ts
callbacks: {
async jwt({user, token, account}){
if(user) {
token.Authorization = user?.accessToken
token.refreshToken = user.refreshToken
}
return token;
},
async session({token, session}){
session.Authorization = token.Authorization
session.RefreshToken = token.refreshToken
return session;
},
},
콜백은 로그인 한 뒤 실행된다.
jwt 콜백은 JWT가 생성되거나 업데이트 되었을 때 실행된다. JWT를 자동으로 쿠기에 저장한다.
프로젝트에서는 토큰을 클라이언트로 노출시켜야하는 상황이 필요 했기에, session 콜백을 함께 사용해주었다.
authorize 함수를 실행한 뒤 백엔드에서 온 응답 중 access token 을 토큰 객체에 담아 보내주었고 session 콜백은 이를 받아 활용할 수 있다.
axios default header
로그인이 되었을 때 로그인 정보를 하위컴포넌트에 알리고 axios 요청시에 필요한 헤더를 설정해줘야한다.
useSession()훅 (클라이언트단에서만 사용가능)을 사용해야하기 때문에 기존에 설정한 sessionProvider 하위에 위치 해야한다.
src/app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={notoSansKr.className}>
<body>
<AuthContext>
<AuthroizationHeader>
<Header />
<ReactQueryProvider>{children}</ReactQueryProvider>
<FloatingBtn />
</AuthroizationHeader>
</AuthContext>
</body>
</html>
);
}
src/context/AuthroizationHeader.tsx
"use client";
import { setCookie } from "@/util/cookies";
import { Session } from "next-auth";
import { useSession } from "next-auth/react";
import { useEffect } from "react";
type Props = {
children: React.ReactNode;
};
type SessionData = {
Authorization: string;
RefreshToken: string;
expires: string;
};
type SessionType = {
data: SessionData | any;
status: string;
update: any;
};
export default function AuthroizationHeader({ children }: Props) {
const { status, data: session }: SessionType = useSession();
const isLogin = !!session && status === "authenticated";
const accesstoken = isLogin ? session.Authorization : "";
const refreshToken = isLogin ? session.RefreshToken : "";
useEffect(() => {
setCookie("Authroization", accesstoken);
setCookie("RefreshToken", refreshToken);
}, [isLogin]);
return <>{children}</>;
}
미들웨어
로그인 한 유저가 로그인페이지에 진입하려하면?, 로그인 되지 않은 유저가 마이페이지에 진입을 하게 된다면?
이 두 상황 모두 페이지의 진입을 막고 리다이렉트 시킬 수 있는 기능이 필요하다.
해당 기능은 next.js middleware를 통해 구현이 가능하다.(참고)
next-auth에서도 분명 미들웨어 기능을 지원할 것이라 생각해서 한참을 찾아 본 결과 미들웨어 기능을 지원하고 있다.
미들웨어는 app과 동일한 레벨에 생성해야한다 (src/middleware.ts)
현재 프로젝트에서 리다이렉트 되어야하는 경우는 로그인이 되어있을때와 되어있지 않을때, 두가지로 나뉜다.
- 로그인이 되었을 때 (withAuth) : 마이페이지, 글작성페이지 접근가능
- 로그인이 되어있지 않을 때 (withOutAuth) : 로그인페이지, 회원가입페이지 접근 가능
import { getToken } from "next-auth/jwt";
import { NextRequest, NextResponse } from "next/server";
const FALLBACK_URL =""
const withAuth = async (req: NextRequest, token: boolean) =>{
const url = req.nextUrl.clone();
const {pathname} = req.nextUrl;
if(!token) {
//토큰값이 falsy한 사용자가 withAuth페이지에 진입하려하면,
//미들웨어에서 req객체 중에 NextUrl 안에 담긴 pathname을 쿼리스트링을 붙여서 로그인페이지로 리다이렉트 시킴
url.pathname = '/login';
//로그인하면 이전페이지로 이동하기 위해서 쿼리스트링사용하여 붙여줌.
url.search =`callbackUrl=${pathname}`;
return NextResponse.redirect(url)
}
}
const withOutAuth = async (req:NextRequest, token: boolean, to: string | null) => {
const url = req.nextUrl.clone();
const {pathname} = req.nextUrl;
if(token) {
url.pathname = to ?? FALLBACK_URL;
url.search = "";
return NextResponse.redirect(url)
}
}
const withAuthList = ["/mypage", "/write"]
const withOutAuthList = ["/login", "/signup"]
export default async function middleware(req: NextRequest) {
//미들웨어 쿠키
let cookie = req.cookies.get("Authroization")?.value || ""
//setting Headers
const requestHeaders = new Headers(req.headers);
requestHeaders.set("Authroization", cookie);
const token = await getToken({ req });
const {searchParams} = req.nextUrl;
const callbackUrl = searchParams.get("callbackUrl");
const pathname = req.nextUrl.pathname
const isWithAuth = withAuthList.includes(pathname);
const isWithOutAuth = withOutAuthList.includes(pathname);
if(isWithAuth) return withAuth(req, !!token)
if(isWithOutAuth) return withOutAuth(req, !!token, callbackUrl)
}
// 미들웨어가 실행될 특정 pathname을 지정하면, 해당 pathname에서만 실행 가능
export const config = {
mathcher : [...withAuthList, ...withOutAuthList]
}
ref
'Next.js' 카테고리의 다른 글
[Next.js] SSR, CSR, SSG, Hydrate (0) | 2023.01.30 |
---|
소중한 공감 감사합니다