Next.js 기반으로 관리자 페이지, 관제 시스템 등 권한이 필요한 프로젝트 진행 시 Next-Auth 라는 npm 을 자주 사용하였다.
여러 프로젝트 진행하면서 고려해야 했던 포인트
- 소수 계정일 경우
- session 기간
- 권한 여부에 대한 스타일, 페이지 차이
- 비 로그인 시 홈 접근 방지 / 로그인 상태 일시 로그인 페이지 접근 방지
- user 정보 가져오기 & 로그아웃
Next-Auth npm
Next-Auth Homepage
Next-Auth 설치
npm i next-auth
// or
yarn add next-auth
Next-Auth 사용법
pages/api /auth 폴더를 만들고 […nextauth].js 파일을 만들면 된다. ( 타입 스크립트이면 ts)
providers에 이메일을 사용하여 로그인 할 수 있게 했는데 공식 홈에 Google, GitHub 등 원하는 providers를 넣으면 계정으로 로그인 할 수 있다.
providers 예시
import NextAuth from "next-auth"
import GitHubProvider from "next-auth/providers/github";
import NaverProvider from "next-auth/providers/naver"
import KakaoProvider from "next-auth/providers/kakao"
export const authOptions = {
...
providers: [
GitHubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET
}),
NaverProvider({
clientId: process.env.NAVER_CLIENT_ID,
clientSecret: process.env.NAVER_SECRET,
}),
KakaoProvider({
clientId: process.env.KAKAO_CLIENT_ID,
clientSecret: process.env.KAKAO_SECRET,
}),
],
}
export default NextAuth(authOptions)
이메일 providers & 소수 계정일 경우
아래 코드는 이메일 providers로 소수의 계정이 필요 한 경우 find() 함수를 활용해 일치하면 로그인 가능하게 하였다.
// pages/api/auth/[...nextauth].js
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export default NextAuth({
providers: [
CredentialsProvider({
name: "유저 이메일,페스워드 방식",
credentials: {
email: {
label: "유저 이메일",
type: "email",
placeholder: "user@email.com",
},
password: { label: "패스워드", type: "password" },
},
async authorize(credentials, req) {
const accountArray = [
{
id: "test1",
password: "test1234!",
},
{
id: "test2",
password: "test1234!",
}
];
const matchedAccount = accountArray.find(
(account) =>
account.id === credentials.email &&
account.password === credentials.password
);
if (matchedAccount) {
const user = [
{
id: 1,
name: "테스터1",
email: "test1",
},
{
id: 2,
name: "테스터2",
email: "test2",
}
];
const matchedUser = user.find((x) => x.email === matchedAccount.id);
if (matchedUser) {
return Promise.resolve(matchedUser);
}
}
return Promise.resolve(null);
},
}),
],
secret: process.env.NEXTAUTH_SECRET,
});
session 기간
secret 다음에 maxAge를 설정하면 된다. 기본 값은 30일 이다.
session: {
maxAge: 24 * 60 * 60, // 24시간
},
NEXTAUTH_SECRET / NEXTAUTH_URL
⭐ 두 개 다 필수로 있어야 함.
NEXTAUTH_SECRET은 무작위 문자열을 넣어주면 된다.
OpenSSL 설치되어 있는 경우
$ openssl rand -base64 32
Node.js를 사용하는 경우
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
명령 프롬프트(CMD)를 열고 명령어를 입력하면 무작위 문자열이 나온다.
Get-Random을 사용하여 PowerShell에서 무작위 바이트를 생성
[Convert]::ToBase64String((Get-Random -Count 32 -InputObject ([byte[]]@(0..255))))
NEXTAUTH_URL은 development, production 분리해서 url를 다르게 설정해줘야 한다.
.env.development
NEXTAUTH_SECRET=...
NEXTAUTH_URL=http://localhost:3000
.env.production
NEXTAUTH_SECRET=...
NEXTAUTH_URL=https://.... (배포 주소)
next.js에 nextauth 연결
client에서 session 정보를 불러올 수 있는 useSession 훅을 사용하기 위해 앱의 가장 최 상단(_app.tsx)에 SessionProvider를 넣어준다.
기본형
// pages/_app.jsx
import { SessionProvider } from "next-auth/react"
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)
}
비 로그인 시 홈 접근 방지
권한 여부에 대한 스타일, 페이지 차이
auth 여부에 따라 보여줄 수 있는 페이지 와 스타일 차이를 Component.auth 를 통해 처리 했다.
그래서 각 페이지마다 login.tsx 하단에 Login.auth = false; index.tsx 하단에 Home.auth = true; 와 같이 넣어 해당 조건에 맞는 스타일과 접근 권한 여부를 부여한다.
⭐ Auth
Component.auth true인 페이지를 불러와서 조건을 통과하면 status에 따른 스타일, 행동을 줄 수 있다. useSession 훅에서 required: true를 설정하는 것은 세션이 반드시 필요한지 여부를 나타냅니다. 이 옵션을 사용하면 세션이 없는 경우에 대한 처리를 지정할 수 있습니다.
하위 코드에서는 useSession 훅을 사용하여 세션 상태를 확인하고, onUnauthenticated 이면 로그인 페이지로 이동 시키고 세션이 로딩 상태이면 컴포넌트를 렌더링한다.
function Auth({ children }: PropsWithChildren) {
const router = useRouter();
const { status } = useSession({
required: true,
onUnauthenticated() {
router.push(`/login`);
},
});
if (status === "loading") {
return (
<>
<Global styles={loadingReset} />
<Loading />
</>
);
}
return <>{children}</>;
}
Login.auth = false 예시
export default function Login() {
...
return (
<>
...
</>
);
}
Login.auth = false;
⭐ 타입 스크립트 사용 시
auth 에 대한 타입을 추가해줘야 한다.
type CustomAppProps = AppProps & {
Component: NextComponentType & { auth?: boolean }; // add auth type
};
export default function App({
Component,
pageProps: { session, ...pageProps },
...appProps
}: CustomAppProps) {
...
}
⭐ 특정 페이지 레이아웃 스타일 분리
appProps 를 통해 pathname이 /login 이면 LayoutComponent 가 스타일이 없는 React.Fragment가 되게 했다.
export default function App({
Component,
pageProps: { session, ...pageProps },
...appProps
}: CustomAppProps) {
const withoutLayout = appProps.router.pathname === "/login";
const LayoutComponent = withoutLayout ? React.Fragment : Layout;
return (
<SessionProvider session={session}>
{Component.auth ? (
<Auth>
<LayoutComponent>
<Global styles={[reset, SCoreDreamFont]} />
<Component {...pageProps} />
</LayoutComponent>
</Auth>
) : (
<LayoutComponent>
<Global styles={[loginReset, SCoreDreamFont]} />
<Component {...pageProps} />
</LayoutComponent>
)}
</SessionProvider>
);
}
전체 코드
// _app.tsx
import React, { useEffect } from "react";
import { SessionProvider, useSession } from "next-auth/react";
import Layout from "@/components/Layout";
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
import { PropsWithChildren } from "react";
import { Global } from "@emotion/react";
import { loadingReset, loginReset, reset } from "@/styles/Global";
import Loading from "@/components/ScreenStructure/Loading";
import type { NextComponentType } from "next";
import {
Hydrate,
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import { SCoreDreamFont } from "@/styles/Fonts";
//Add custom appProp type then use union to add it
type CustomAppProps = AppProps & {
Component: NextComponentType & { auth?: boolean }; // add auth type
};
export default function App({
Component,
pageProps: { session, ...pageProps },
...appProps
}: CustomAppProps) {
const withoutLayout = appProps.router.pathname === "/login";
const LayoutComponent = withoutLayout ? React.Fragment : Layout;
const [queryClient] = React.useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<SessionProvider session={session}>
{Component.auth ? (
// 권한이 필요한 경우
<Auth>
<LayoutComponent>
<Hydrate state={pageProps.dehydratedState}>
<Global styles={[reset, SCoreDreamFont]} />
<Component {...pageProps} />
</Hydrate>
</LayoutComponent>
</Auth>
) : (
// 권한이 필요하지 않은 경우
<LayoutComponent>
<Global styles={[loginReset, SCoreDreamFont]} />
<Component {...pageProps} />
</LayoutComponent>
)}
</SessionProvider>
</QueryClientProvider>
);
}
function Auth({ children }: PropsWithChildren) {
const router = useRouter();
const { status } = useSession({
required: true,
onUnauthenticated() {
router.push(`/login`);
},
});
if (status === "loading") {
return (
<>
<Global styles={loadingReset} />
<Loading />
</>
);
}
return <>{children}</>;
}
로그인 되어 있을 때 로그인 페이지 접근 방지
next.config.js
로그인 되어 있는 상태에서 url에 "/login" 입력 시 로그인 페이지에 진입을 방지하기 위해 "/login" 입력하면 “/” 으로 가는 redirects를 추가한다.
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
async redirects() {
return [
{
source: "/login",
destination: "/",
permanent: true,
},
{
source: "/error",
destination: "/",
permanent: true,
},
];
},
};
module.exports = nextConfig;
user 정보 가져오기 & 로그아웃은 다음 글 이어서…
'Next.js' 카테고리의 다른 글
Next.js14 정리(2) (0) | 2024.08.28 |
---|---|
Next.js 14 정리(1) (0) | 2024.08.28 |
Next Auth(2) (0) | 2024.08.26 |
Next.js Image blurDataURL (0) | 2024.08.23 |
<img/>태그 이미지 원본 크기+next.js (0) | 2024.08.23 |