Next.js 14 引入NextAuth.js,实现登录认证用户信息获取

3,938 阅读4分钟

本文皆为个人原创,请尊重创作,未经许可不得转载。

前几周在学习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

目录结构一览:

image.png

引入依赖

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

至此,我们的项目依赖如下:

image.png

本文皆为个人原创,请尊重创作,未经许可不得转载。

数据准备

定义用户类型、数据格式

在App目录下,创建lib目录并新建definitions.ts,存放我们项目需要的数据类型。

image.png

创建数据库链接

在lib下建立返回mysql连接池的js,需要获取数据时使用。

image.png

创建.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三个文件。

image.png

auth.config.ts

配置Next-Auth,涉及选项:1必须 0可选

  1. providers 1
  2. secret 1 (实际上是.env文件里的AUTH_SECRET)
  3. session 0
  4. jwt 0
  5. pages 0
  6. callbacks 0
  7. events 0
  8. adapter 0
  9. debug 0
  10. logger 0
  11. 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文件

image.png

[...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页面

image.png

登录认证

输入错误的登录信息

image.png

输入正确的信息,认证成功后跳转到home页面并获取到session信息 (文中展示的是client组件获取session信息,server组件使用await auth() 获取)

image.png

最终的目录结构

image.png

参考(Reference):
Next官方教程: nextjs.org/learn/dashb…
Auth.js: next-auth.js.org/

本文皆为个人原创,请尊重创作,未经许可不得转载。