인증(Authentication)과 인가(Authorization)의 차이점
- 인증(Authentication): 인증은 사용자가 누구인지를 확인하는 것이다. 사용자는 아이디와 비밀번호를 통해 자신의 신원을 증명한다.
- 인가(Authorization): 사용자의 신원이 확인되면, 인가는 해당 사용자가 애플리케이션의 어떤 부분에 접근 가능한지를 결정한다.
NextAuth.js란?
NextAuth.js
는 세션 관리, 로그인 및 로그아웃 등 인증 과정을 단순화하여 Next.js 애플리케이션에서 인증을 위한 통합 솔루션을 제공한다.
NextAuth.js 설치
npm install next-auth@beta bcrypt
- Next.js 14에서 사용할 수 있는
NextAuth.js
베타 버전과 비밀번호 해쉬화를 위한 bcrypt
라이브러리를 설치한다.
AUTH_SECRET=your-secret-key
AUTH_URL=http://localhost:3000/api/auth
- npm 설치 후
.env
파일에 openssl rand -base64 32
로 생성한 시크릿 키와 URL을 추가한다.
Next.js Middleware로 라우팅 보호하기
import type { NextAuthConfig } from 'next-auth';
export const authConfig = {
providers: [],
pages: {
signIn: '/login',
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
if (isOnDashboard) {
if (isLoggedIn) return true;
return false;
} else if (isLoggedIn) {
return Response.redirect(new URL('/dashboard', nextUrl));
}
return true;
},
},
} satisfies NextAuthConfig;
auth.config.ts
파일을 루드 디렉토리에 생성하여 authConfig
를 설정한다.
pages
옵션으로 로그인 할 때 NextAuth.js
에서 기본적으로 제공하는 로그인 페이지로 이동하는 것이 아닌 커스텀 로그인 페이지로 이동하도록 설정한다.
authorized
콜백 함수를 이용해서 로그인 요청에 Next.js Middleware
를 통해 페이지에 접근할 수 있는 권한을 부여받았는지 확인한다.
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
export default NextAuth(authConfig).auth;
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.png).*)'],
};
- 루트 디렉토리에
middleware.ts
파일을 생성한다.
authConfig
객체로 NextAuth.js
룰 초기화하고 이를 export 한다. 또한 matcher
옵션을 추가해서 미들웨어가 특정 경로에서만 실행되도록 설정할 수 있다.
- 미들웨어를 사용하면 미들웨어가 인증을 확인할 때까지 보호된 경로의 렌더링이 시작되지 않아서 애플리케이션의 보안과 성능이 모두 향상된다는 장점이 있다.
- 미들웨어 파일 생성 후 이미지가 엑박으로 뜰 경우 다음 게시물을 참고해주세요! 링크
로그인 기능 추가하기
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import { authConfig } from './auth.config';
import { sql } from '@vercel/postgres';
import { z } from 'zod';
import type { User } from '@/app/lib/definitions';
import bcrypt from 'bcrypt';
async function getUser(email: string): Promise<User | undefined> {
try {
const user = await sql<User>`SELECT * from USERS where email=${email}`;
return user.rows[0];
} catch (error) {
console.error('Failed to fetch user:', error);
throw new Error('Failed to fetch user.');
}
}
export const { auth, signIn, signOut } = NextAuth({
...authConfig,
providers: [
Credentials({
async authorize(credentials) {
const parsedCredentials = z
.object({ email: z.string().email(), password: z.string().min(6) })
.safeParse(credentials);
if (parsedCredentials.success) {
const { email, password } = parsedCredentials.data;
const user = await getUser(email);
if (!user) return null;
const passwordsMatch = await bcrypt.compare(password, user.password);
if (passwordsMatch) return user;
}
console.log('Invalid credentials');
return null;
},
}),
],
});
authorize
함수를 사용해서 인증 로직을 관리할 수 있다.
zod
를 사용해서 사용자가 DB에 존재하는지 확인하기 전에 이메일과 비밀번호의 유효성 검사를 수행한다.
- 유효성 검사를 마치면
getUser
함수를 실행해 DB에서 해당하는 사용자의 데이터를 찾고, bcrypt.compare
를 실행해서 사용자가 입력한 비밀번호와 DB에 해쉬화되어 저장된 비밀번호가 일치하는지 확인한다.
- 비밀번호까지 일치한다면 사용자의 정보를 반환하고, 일치하지 않는다면 사용자가 로그인 할 수 없도록
null
을 반환한다.
'use client'
import { useFormState } from 'react-dom';
import { authenticate } from '@/app/lib/actions';
export default function LoginForm() {
const [code, action] = useFormState(authenticate, undefined);
return (
<form action={action}>
{code === 'CredentialSignin' && (
<p aria-live="polite" className="text-sm text-red-500">
Invalid credentials
</p>
)}
)
- 리액트에서 제공하는
useFormState
훅을 사용해서 서버 액션을 호출하고 에러를 핸들링 할 수 있다.
import { useFormStatus } from 'react-dom';
function LoginButton() {
const { pending } = useFormStatus();
return (
<Button className="mt-4 w-full" aria-disabled={pending}>
Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
</Button>
);
}
- 또한
useFormStatus
훅에서 반환하는 pending
값을 사용해서 로그인 중인 경우 로그인 버튼에 disabled
설정을 할 수 있다.
로그아웃 기능 구현하기
import { signOut } from '@/auth';
export default function SideNav() {
return (
<form
action={async () => {
'use server';
await signOut();
}}
>
);
}
auth.ts
파일에서 export 한 signOut
함수를 이용해서 로그아웃 기능을 구현한다.
메타데이터란?
- 메타데이터는 웹 페이지에 추가적인 정보를 전달하며 보통 페이지의
<head>
태그 안에 위치한다.
- 메타데이터는 웹 페이지의 SEO를 향상시키는 데 중요한 역할을 하며, 검색 엔진과 소셜 미디어 플랫폼에 대한 접근성과 이해도를 높인다.
- 적절한 메타데이터는 검색 엔진이 웹 페이지를 효율적으로 인덱싱해서 검색 결과 순위를 향상시키는 데 도움을 준다.
- 또한 오픈그래프와 같은 메타데이터는 소셜 미디어에서 링크를 공유할 때 사용자에게 더욱 매력적이고 유익한 컨텐츠를 제공한다.
메타데이터의 종류
<title>Page Title</title>
<meta name="description" content="A brief description of the page content." />
<meta name="keywords" content="keyword1, keyword2, keyword3" />
<meta property="og:title" content="Title Here" />
<meta property="og:description" content="Description Here" />
<meta property="og:image" content="image_url_here" />
<link rel="icon" href="path/to/favicon.ico" />
- Title: 브라우저 탭에 나타나는 웹 페이지의 제목을 나타낸다. 검색 엔진이 해당 웹 페이지를 파악하는 데 도움을 주기 때문에 SEO에 매우 중요하다.
- Description: 웹 페이지의 내용에 대한 간략한 개요를 제공하며 검색 엔진 결과에 표시된다.
- Keyword: 웹 페이지의 내용과 관련된 키워드를 포함하여 검색 엔진이 페이지를 인덱싱하는 데 도움을 준다.
- Open Graph: 제목, 상세 내용, 미리보기 이미지 등을 제공함으로써 웹 페이지가 소셜 미디어에서 공유될 때 표현되는 방식을 향상시긴다.
- Favicon: 브라우저의 주소 표시줄이나 탭에서 표시되는 파비콘(작은 아이콘)을 설정한다.
메타데이터 추가하기
파비콘과 오픈 그래프
/app
폴더의 루트 디렉토리에 favicon.ico
과 opengraph-image.jpg
파일을 추가한다.
- Next.js는 이 파일들을 자동으로 식별하여 파비콘과 오픈 그래프 이미지로 사용한다.
페이지 title과 description
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Acme Dashboard',
description: 'The official Next.js Course Dashboard, built with App Router.',
};
layout.js
혹은 page.js
파일에 title, description 등의 정보를 담은 메타데이터 객체를 포함하면 Next.js는 자동으로 애플리케이션에 메타데이터를 추가한다.
import { Metadata } from 'next';
export const metadata: Metadata = {
title: {
template: '%s | Acme Dashboard',
default: 'Acme Dashboard',
},
description: 'The official Next.js Learn Dashboard built with App Router.',
};
export const metadata: Metadata = {
title: 'Invoices',
};
- 만약에 페이지마다 커스텀 타이틀을 추가하고 싶다면 해당
page.tsx
에 메타데이터 객체를 추가하면 된다.
- 하지만 타이틀 중에서도 회사 이름과 같이 고정적인 부분이 있다면
title.template
필드에 %s
를 설정해서 해당 부분의 타이틀만 변경되도록 할 수 있다.
- 에를 들면 상단의 예시 코드에서
invoices
페이지의 타이틀은 Invoices | Acme Dashboard
가 될 것이다.