本文皆为个人原创,请尊重创作,未经许可不得转载。
前几周在学习Next框架,用它编写一个宠物主题的博客,中间遇到登录和获取用户Session信息相关的问题,跟着官方教程仍然有很多坑,中途去搜索了很多文档、Github issue和博客内容,相关内容都是英文,故留存本篇文章给正在使用React18、Next 14 需要引入最新的NextAuth.js(Auth.js)(5.0)的朋友进行参考。
(注:最新未必最好,最适合的才是)
此demo已上传Github托管。
依赖:next 14、next-auth 5.0、typescript 5、react18、mysql2、dotenv、zod...可见下文或Demo中Readme.md
项目创建
创建一个最新的Next项目。
npx create-next-app@latest nextjs-authjs-demo --use-npm
目录结构一览:
引入依赖
npm install next-auth@5.0.0-beta.15 mysql2@3.9.2 dotenv@16.4.5 zod@3.22.4 bcrypt@5.1.1
至此,我们的项目依赖如下:
本文皆为个人原创,请尊重创作,未经许可不得转载。
数据准备
定义用户类型、数据格式
在App目录下,创建lib目录并新建definitions.ts,存放我们项目需要的数据类型。
创建数据库链接
在lib下建立返回mysql连接池的js,需要获取数据时使用。
创建.env文件
在顶级目录同级创建.env文件,配置mysql连接信息、AUTH配置信息
MYSQL_HOST=localhost
MYSQL_USER=root
MYSQL_PASSWORD=123456
MYSQL_DATABASE=test
MYSQL_PORT=3306
AUTH_SECRET=YspLQi1R+sO2AIOV2eDoREe4GWz9OFls47VNzTKtPaE=
AUTH_URL=http://localhost:3000/api/auth
配置Next-auth(重点)
在项目app目录同级(如果你的目录是src/app/...,则在src下创建),创建auth.config.ts、auth.ts、middleware.ts三个文件。
auth.config.ts
配置Next-Auth,涉及选项:1必须 0可选
- providers 1
- secret 1 (实际上是.env文件里的AUTH_SECRET)
- session 0
- jwt 0
- pages 0
- callbacks 0
- events 0
- adapter 0
- debug 0
- logger 0
- theme 0
import type { NextAuthConfig } from "next-auth";
export const authConfig = {
pages: { // 定义signIn使用的URL地址
signIn: "/login",
},
// auth.ts authorize进入的回调方法
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnDashboard = nextUrl.pathname.startsWith("/home");
if (isOnDashboard) {
if (isLoggedIn) {
return true;
}
return false; // Redirect unauthenticated users to login page
} else if (isLoggedIn) {
return Response.redirect(new URL("/home", nextUrl));
}
return true;
},
session(params) { // 保存session内容
return {
...params.token,
...params.session,
user: {
...params.session.user,
id: params.session.user.id as string,
},
status: "cool",
};
},
},
providers: [], // 官方提供了OAuth Provider(Github, Twitter, Google),我们这里主要是自定义CredentialsProvider
} satisfies NextAuthConfig;
本文皆为个人原创,请尊重创作,未经许可不得转载。
auth.ts
实际上auth.ts是auth.config把Providers配置拆分出来的,你可以把两个文件进行合并,Next.js官方教程中在这里实现从数据库获取用户信息和校验的过程。
import NextAuth from "next-auth";
import { authConfig } from "./auth.config";
import Credentials from "next-auth/providers/credentials";
import { z } from "zod";
import { User } from "../app/lib/definitions";
import pool from "../app/lib/db";
import bcrypt from "bcrypt";
// 根据用户邮箱获取用户是否存在
async function getUser(email: string): Promise<User | undefined> {
try {
const connect = await pool.getConnection();
const data: any = await connect.query(
`SELECT * FROM user WHERE email='${email}'`,
);
connect.release();
return data[0][0] as User;
} catch (error) {
console.error("Failed to fetch user:", error);
throw new Error("Failed to fetch user.");
}
}
export const { handlers, auth, signIn, signOut } = NextAuth({
...authConfig,
providers: [ // 定义一个Credentials,并实现authorize方法
Credentials({
async authorize(credentials) {
// 使用zod进行数据合法校验
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,
id: user.id
};
}
}
return null;
},
}),
],
});
middleware
顾名思义,定义一个中间件,下列代码实现用户访问前必须登录,没有登录会自动拦截跳转到登录页面。
import NextAuth from "next-auth";
import { authConfig } from "./auth.config";
export default NextAuth(authConfig).auth;
export const config = {
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"],
};
配置Next-Auth路由
在app目录下,创建api/auth目录,创建文件[...nextauth.ts],创建session/route.ts文件
[...nextauth.ts]
import NextAuth from "next-auth";
import { authConfig } from "../../../auth.config";
const handler = NextAuth(authConfig);
export { handler as GET, handler as POST };
route.ts
import { auth } from "../../../../auth";
import { NextResponse } from "next/server";
export async function GET(_request: Request) {
const session = await auth();
if (!session?.user) {
return new NextResponse(
JSON.stringify({ status: "fail", message: "You are not logged in" }),
{ status: 401 },
);
}
return NextResponse.json({
authenticated: !!session,
session,
});
}
SessionProvider
想要在项目中任意一处都能获取,需要将SessionProvider包裹在根目录的html的子元素上。
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
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,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}><SessionProvider>{children}</SessionProvider></body>
</html>
);
}
验证
如果你有留意上文的Auth配置,出现了'/home','/login'地址,依次代表登录后跳转的首页和登录地址。
React18使用文件目录路由,创建好/app/index/page.tsx文件,自动绑定路由地址 '/index'
home页面
'use client'
import { useSession } from "next-auth/react";
export default function Index() {
const { data }: { data: any } = useSession();
const session = data?.session;
return(
<div className="text-center">
<text>hello next-auth, I'm { session ? session.user.name : '' }</text>
</div>
)
};
login页面
'use client'
import { useFormState, useFormStatus } from "react-dom";
import { authenticate } from "../lib/action";
const initState = {
errorMsg: "",
success: false
};
export default function LoginPage() {
const [state, dispatch] = useFormState(authenticate, initState);
const { pending } = useFormStatus();
return(
<div style={{width:300,height:300,margin:'auto'}}>
<form style={{display:"flex",flexDirection:"column",marginTop:50,textAlign:'left'}} action={dispatch}>
<input placeholder="email" name="email"/>
<input placeholder="password" name="password" type="password"/>
<button type="submit" aria-disabled={pending}>Login</button>
{!state.success && state.errorMsg && (
<>
<p className="text-sm text-red-500">{state.errorMsg}</p>
</>
)}
</form>
</div>
)
}
建表
CREATE TABLE `user` (
`id` varchar(255) NOT NULL,
`email` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
INSERT INTO `test`.`user` (`id`, `email`, `name`, `password`) VALUES ('410544b2-4001-4271-9855-fec4b6a6442a', 'test@email.com', 'youKnowWho', '$2b$10$xupd3MaYf1fA2wMjaNl6AuTyBE2ifOGd2xbUVpdV1fgqYyBi9XiRy');
middleware拦截
未登录前,在浏览器地址栏输入http://localhost:3000/home 并访问,被拦截跳转至login页面
登录认证
输入错误的登录信息
输入正确的信息,认证成功后跳转到home页面并获取到session信息 (文中展示的是client组件获取session信息,server组件使用await auth() 获取)
最终的目录结构
参考(Reference):
Next官方教程: nextjs.org/learn/dashb…
Auth.js: next-auth.js.org/
本文皆为个人原创,请尊重创作,未经许可不得转载。