
https://next-auth.js.org/ ๊ณต์ ํํ์ด์ง
next-auth๋ Google, Facebook, GitHub, Twitter ๋ฑ ๋ค์ํ OAuth ์ ๊ณต์์์ ํตํฉ์ ์ง์ํฉ๋๋ค.next-auth๋ ์ฌ์ฉ์์ ๋ก๊ทธ์ธ ์ํ๋ฅผ ์ธ์
์ผ๋ก ๊ด๋ฆฌํ๋ฉฐ, ์๋ฒ ์ธก ๋ฐ ํด๋ผ์ด์ธํธ ์ธก์์// pages/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import GithubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
export default NextAuth({
providers: [
Providers.GitHub({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}),
// ๋ค๋ฅธ ์ ๊ณต์๋ ์ถ๊ฐ ๊ฐ๋ฅ
],
callbacks: {
async session(session, token) {
session.user.id = token.id;
return session;
},
async jwt(token, user) {
if (user) {
token.id = user.id;
}
return token;
},
},
});
next-auth๋ Next.jsd์์ ์ธ์ฆ ๊ธฐ๋ฅ์ ์ฝ๊ฒ ์ถ๊ฐํ๊ณ ํ์ฅํ ์ ์๋ ๊ฐ๋ ฅํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค. OAuth ์ ๊ณต์์ ์ด๋ฉ์ผ ์ธ์ฆ์ ํฌํจํ ๋ค์ํ ์ธ์ฆ ๋ฐฉ์์ ์ง์ํ๋ฉฐ, ์ฌ์ฉ์ ์ ์๊ฐ ์ฉ์ดํฉ๋๋ค. โฉ ์ฌ์ฉ์ ์ธ์ฆ๊ณผ ๊ด๋ จ๋ ๋ณต์กํ ์์ ์ ๊ฐ๋จํ๊ฒ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
next-auth์์ ์ ๊ณตํ๋ Credentials ์ธ์ฆ์ ์ฌ์ฉ์๊ฐ ์
๋ ฅํ ์์ด๋/๋น๋ฐ๋ฒํธ๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์ ์ ์ ๋ก๊ทธ์ธ ์์คํ
์ ๊ตฌํOAuth์ ๊ฐ์ ์ธ๋ถ ํ๋ก๋ฐ์ด๋ ์์ด ์ฌ์ฉ์ ์๊ฒฉ ์ฆ๋ช
์ ์ฒ๋ฆฌCredentialsProvider ๋ ์ด๋ฉ์ผ/๋น๋ฐ๋ฒํธ, ์ฌ์ฉ์ ์ด๋ฆ/๋น๋ฐ๋ฒํธ, ๋๋ ๊ทธ ์ธ ํ์ํ ์ฌ์ฉ์ ์ ์ ๋ฐ์ดํฐ๋ก ๋ก๊ทธ์ธํ ์ ์์credentials ์ค์ ์์ ์ด๋ฌํ ํ๋๋ฅผ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.CredentialsProvider ์ค์ next-auth์ NextAuth ์ค์ ์์ CredentialsProvider ๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์ ์ ์ ์ธ์ฆ ์ค์ app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
const handler = NextAuth({
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "Email", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
// ์ฌ์ฉ์ ์ธ์ฆ ์ฒ๋ฆฌํ๋ ๋ถ๋ถ (DB์์ ์ฌ์ฉ์ ์ ๋ณด ๊ฒ์ฆ)
const params = {
email: credentials?.email,
password: credentials?.password
};
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/login/`, {
method: 'POST',
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(params)
});
const res = await response.json();
if (res.message === "OK") {
return res;
} else {
throw new Error(res.message);
}
},
}),
],
// ์ถ๊ฐ ์ต์
์ค์ (์: ์ธ์
๊ด๋ฆฌ)
});
export { handler as GET, handler as POST }
- `credentials` ๊ฐ์ฒด์ ํค๋ ๋ก๊ทธ์ธ ์ฐฝ์์ ๋ณด๋ธ `input` ํ๋์ `name` ์์ฑ๊ณผ ์ผ์นํด์ผ ํจ
- `email`๊ณผ `password`๋ฅผ ๋ณด๋ธ๋ค๋ฉด, `CredentialsProvider`์์๋ `email`๊ณผ `password`๋ก ๋ฐ์์ผ ํจ
credentials์ด ์ ํจํ์ง ๊ฒ์ฆapp/api/login/route.ts
import { connectToDatabase } from '@/app/lib/mongodb';
import { NextRequest, NextResponse } from 'next/server';
import { Users, UsersType } from '../models/user';
// 1. ์ฌ์ฉ์๊ฐ ์
๋ ฅํ ์์ด๋ -> DB ์ ๊ณ์ ์กฐํ
// 2. ๊ณ์ ์ด ์กด์ฌํ๋์ง ์ฒดํฌ
// 2-1. ๊ณ์ ์กด์ฌํ์ง ์๋ค๋ฉด ๊ฒฐ๊ณผ ๋ฆฌํด
// 2-2. ๊ณ์ ์ด ์กด์ฌํ๋ค๋ฉด ์
๋ ฅ๋ ํจ์ค์๋๋ฅผ bcrypt.compare ํจ์๋ก ํจ์ค์๋๋ฅผ ๋น๊ตํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๋ฆฌํด
const bcrypt = require("bcrypt");
export async function POST(request: NextRequest) {
try {
// ์์ฒญ ๋ณธ๋ฌธ(body) ๊ฐ์ ธ์ค๊ธฐ
const body = await request.json();
// ํด๋ผ์ด์ธํธ์์ ๋ณด๋ธ ๋ฐ์ดํฐ ์ถ๋ ฅ
console.log('Login Server Received data:', body);
const dbName = process.env.DB_NAME_CHICKEN;
const collectionName = process.env.COLLECTION_USERS;
console.log('@@@ Before connecting to database');
const db = await connectToDatabase(dbName as string);
const collection = db.collection(collectionName as string);
console.log(`-- Database connected`);
console.log(`-- Connected to databse: ${db.databaseName}`);
const inputPassword: String = body.password;
const userInfo: UsersType = await Users.findOne({ email: body.email}).exec();
if (userInfo === null) {
return Response.json({
message: "๊ณ์ ์ด ์กด์ฌํ์ง ์์ต๋๋ค.",
result: ""
})
}
const isMatched: boolean = await bcrypt.compare(inputPassword, userInfo.password);
return NextResponse.json({
message: isMatched ? 'OK !' : '์์ด๋ ํน์ ๋น๋ฐ๋ฒํธ๋ฅผ ํ์ธํด์ฃผ์ธ์.'
});
} catch (error) {
console.error('Error handling request:', error);
return NextResponse.json({ message: 'Error processing request' }, { status: 500 });
}
}
signIn ํจ์๋ฅผ ์ฌ์ฉํด ๋ก๊ทธ์ธ ์์ฒญ์ ๋ณด๋ธ๋ค.NextAuth.js์์ ์ ๊ณตํ๋ ํด๋ผ์ด์ธํธ ์ธก ํจ์๋ก, ์ฌ์ฉ์๊ฐ ์ธ์ฆ์ ์๋ํ ๋ ํธ์ถํ๋ค. ์ด ํจ์๋ ํ๋ก๋ฏธ์ค๋ฅผ ๋ฐํํ๋ฉฐ, ์ฑ๊ณต ์ฌ๋ถ ๋ฐ ์ถ๊ฐ ์ ๋ณด๋ฅผ ํฌํจํ ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ค.credentials ์ธ์ฆ๊ณผ ํจ๊ป ์ฌ์ฉ์๊ฐ ์
๋ ฅํ ๋ฐ์ดํฐ๋ฅผ ์๋ฒ๋ก ์ ์กsignIn ํจ์์ ์๋ต ๊ฐconst result = await signIn(provider, options);
result.ok: (boolean) ๋ก๊ทธ์ธ ์๋๊ฐ ์ฑ๊ณตํ๋์ง ์ฌ๋ถ. true์ผ ๊ฒฝ์ฐ ์ฑ๊ณต, false์ผ ๊ฒฝ์ฐ ์คํจ.result.error: (string | undefined) ๋ก๊ทธ์ธ ์คํจ ์ ์๋ฌ ๋ฉ์์ง๋ฅผ ํฌํจ. ์ฑ๊ณต ์์๋ undefined.result.status: (number | undefined) HTTP ์๋ต ์ํ ์ฝ๋. ์ฑ๊ณต ์์๋ undefined์ผ ์ ์์ผ๋ฉฐ, ์คํจ ์ HTTP ์ํ ์ฝ๋๊ฐ ํฌํจ๋ ์ ์์.result.url: (string | null | undefined) ๋ฆฌ๋๋ ์
๋ URL. redirect: false๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ ์ด ์์ฑ์ด ํฌํจ๋ฉ๋๋ค.์ค์ result json ๊ฐ
๋ก๊ทธ์ธ ์ฑ๊ณตํ ๊ฒฝ์ฐ -> {"error":null,"status":200,"ok":true,"url":"http://localhost:3000"}
์คํจํ ๊ฒฝ์ฐ -> {"error":"CredentialsSignin","status":401,"ok":false,"url":null}
function Login() {
// ๋ก๊ทธ์ธ ์ ๋ณด
const [formData, setFormData] = useState({
email: '',
password: ''
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target) return;
const { name, value } = e.target;
setFormData({
...formData,
[name]: value
});
};
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// ๋ก๊ทธ์ธ ์ฒ๋ฆฌ
console.log(`๋ก๊ทธ์ธ => email: ${formData.email}, password: ${formData.password}`);
try {
const res = await signIn("credentials", {
redirect: false,
email: formData.email,
password: formData.password,
callbackUrl: "/",
}).then((result) => {
console.log(result!.error);
if (result?.ok) {
console.log('์ธ์ฆ์ ์ฑ๊ณตํ์์ต๋๋ค.');
router.push("/"); // ์ธ์ฆ ์ฑ๊ณต ํ ๋ฆฌ๋ค์ด๋ ํธ. ํ์ผ๋ก ์ค์
}
else {
showToast.error(result?.error ?? '๋ก๊ทธ์ธ์ ์คํจํ์ต๋๋ค.');
// return;
}
});
} catch (error) {
console.error(`Network error: ${error}`);
}
};
return (
<form onSubmit={handleSubmit}>
<input type="text" name="email" className="grow" placeholder="์ด๋ฉ์ผ์ ์
๋ ฅํด์ฃผ์ธ์."
onChange={handleChange} />
<input
type={showPassword ? 'text' : 'password'}
name="password"
className="grow"
placeholder="๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์."
onChange={handleChange}
/>
<button type="submit" className={'btn my-6 ylw w-full'}>๋ก๊ทธ์ธ</button>
</form>
);
}
export default Login;
๋น๋ฐ๋ฒํธ ๋ณด์:
์ธ์ ๊ด๋ฆฌ:
next-auth๋ ์ธ์
๊ด๋ฆฌ๋ฅผ ์๋์ผ๋ก ์ฒ๋ฆฌํ๋ฉฐ, ์ฌ์ฉ์๋ ๋ก๊ทธ์ธ ํ ์ธ์ฆ๋ ์ธ์
์ ์ ์งํ ์ ์์ต๋๋ค.์๋ฌ ์ฒ๋ฆฌ:
signIn ํจ์์ ๊ฒฐ๊ณผ์์ ์ค๋ฅ ๋ฉ์์ง๋ฅผ ํ์ธํ๊ณ , ์ฌ์ฉ์์๊ฒ ์ ์ ํ ํผ๋๋ฐฑ์ ์ ๊ณตํ ์ ์์ต๋๋ค.
next-auth์Credentials์ธ์ฆ์ ์ฌ์ฉ์ ์ ์ ์ธ์ฆ ์์คํ ์ ๊ตฌํํ ์ ์๋ ๊ฐ๋ ฅํ๊ณ ์ ์ฐํ ๋๊ตฌ. ์ฌ์ฉ์ ์ ๋ ฅ ํ๋๋ฅผ ์ค์ ํ๊ณ , ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋๋ ์ธ๋ถ API์ ์ฐ๋ํ์ฌ ์ฌ์ฉ์ ์ธ์ฆ์ ์ฒ๋ฆฌํ๋ฉฐ,authorize๋ฉ์๋๋ฅผ ํตํด ์ธ๋ถ์ ์ธ ์ธ์ฆ ๋ก์ง์ ์์ฑํ ์ ์์
authorize ํจ์์์ ์ฌ์ฉ์๊ฐ ์
๋ ฅํ ์๊ฒฉ ์ฆ๋ช
(์: ์ด๋ฉ์ผ๊ณผ ๋น๋ฐ๋ฒํธ)์ ๊ฒ์ฆํฉ๋๋ค.null์ ๋ฐํํ๊ฑฐ๋ ์๋ฌ๋ฅผ ๋ฐ์์ํต๋๋ค.authorize ํจ์๊ฐ ๋ฐํํ ์ฌ์ฉ์ ๊ฐ์ฒด๋ next-auth์ ์ํด ์๋์ผ๋ก ์ธ์
์ ์ ์ฅ๋ฉ๋๋ค.id, name, email)๊ฐ ์ธ์
์ ์ ์ฅ๋๋ฉฐ, ์ด๋ฅผ JWT์ ํฌํจํ๊ฑฐ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ ์ ์์ต๋๋ค.callbacks ์ต์
์ jwt ๋ฐ session ์ฝ๋ฐฑ์ ํตํด ์ธ์
์ ์ ์ฅ๋๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ปค์คํฐ๋ง์ด์งํ ์ ์์ต๋๋ค.const handler = NextAuth({
<... ์ ์ฝ๋ ์ฐธ๊ณ >
pages: {
signIn: "/login"
},
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60, //30์ผ๋์ ์ธ์
์ ์ง
updateAge: 24 * 60 * 60, // 24์๊ฐ๋ง๋ค ์ธ์
๊ฐฑ์
},
callbacks: {
async jwt({ token, user}) {
// ์ฌ์ฉ์ ์ธ์ฆ ํ JWT์ ์ฌ์ฉ์ ์ ๋ณด ์ ์ฅ
if (user) {
token.email = user.email;
token.name = user.name;
}
return token;
},
async session({ session, token }) {
// ์ธ์
๊ฐ์ฒด์ ์ฌ์ฉ์ ์ ๋ณด ์ถ๊ฐ
if (session.user) {
session.user.email = token.email;
session.user.name = token.name;
}
return session;
}
},
jwt ์ฝ๋ฐฑ:
- ์ฌ์ฉ์ ์ธ์ฆ ํ JWT ํ ํฐ์ ์ ์ฅํ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ค์
- ์ฌ์ฉ์ ๊ฐ์ฒด๊ฐ ์์ ๋๋ง JWT ํ ํฐ์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ถ๊ฐ
session ์ฝ๋ฐฑ:
- ํด๋ผ์ด์ธํธ ์ธก์์ ์ ๊ทผํ ์ ์๋ ์ธ์
๊ฐ์ฒด์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ถ๊ฐ
- jwt ์ฝ๋ฐฑ์์ ์ค์ ํ ํ ํฐ ์ ๋ณด๋ฅผ ์ธ์
์ ๋ณต์ฌํ์ฌ ํด๋ผ์ด์ธํธ์์ ์ฌ์ฉํ ์ ์๋๋ก ํจ
CredentialsProvider์์ ์ธ์ฆ์ ์ฑ๊ณตํ๋ฉด ์ฌ์ฉ์ ์ ๋ณด๊ฐ ์๋์ผ๋ก ์ธ์ ์ ์ ์ฅ๋ฉ๋๋ค.
jwt์session์ฝ๋ฐฑ์ ์ฌ์ฉํ์ฌ ์ธ์ ์ ์ ์ฅ๋๋ ์ ๋ณด๋ฅผ ์ปค์คํฐ๋ง์ด์งํ ์ ์์ต๋๋ค.
ํด๋ผ์ด์ธํธ๋useSessionํ ์ด๋getSessionํจ์๋ฅผ ํตํด ์ธ์ ์ ๋ณด๋ฅผ ์ฝ๊ฒ ์ ๊ทผํ ์ ์์ต๋๋ค.