Next Auth 的文档大部分都是过时的示例,据我了解,它仍在最终确定中。
实际上,很难找到有关如何执行此操作的信息。 Next Auth 的文档对我来说是真正的痛苦和混乱。我花了很多时间观看视频教程和阅读相关文章,但似乎一切都变化得太快,所有信息都已经过时了。
我实际上是在尝试根据官方网站上的教程调整身份验证系统 Next js 的,但显然我做错了。我收到如下错误:
在 VSCode 前端控制台中,我收到如下错误:
Response from API: {
user: { id: 4, username: 'bob', email: '[email protected]' },
refresh: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTcxMDUyNjUzMiwiaWF0IjoxNzEwNDQwMTMyLCJqdGkiOiJkMzI3MjJkNjM1MTQ0NTE5YTQ3ODEzYmE0YTYzYjc0NiIsInVzZXJfaWQiOjR9.bUEP8vq0yhbRICtXjowThZ98Z77Fh9ArhLHWnflCbaA',
access: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzEwNDQwNDMyLCJpYXQiOjE3MTA0NDAxMzIsImp0aSI6IjBjOTE5MzM5YWNhZTQ4MmVhNzZmMTRjZTFkNWY0YzhlIiwidXNlcl9pZCI6NH0.M0F-yIQ3hitTvbom-JVeWQqlfaEEesYADFPFlDU8Mxw'
}
[auth][error] CallbackRouteError: Read more at https://errors.authjs.dev#callbackrouteerror
[auth][cause]: TypeError: "ikm"" must be an instance of Uint8Array or a string
............
[auth][details]: {
"provider": "credentials"
}
Authentication Error: CallbackRouteError: Read more at https://errors.authjs.dev#callbackrouteerror
at Module.callback (webpack-internal:///(action-browser)/./node_modules/@auth/core/lib/actions/callback/index.js:437:23)
....................
type: 'CallbackRouteError',
kind: 'error',
[cause]: {
err: TypeError: "ikm"" must be an instance of Uint8Array or a string
如有任何帮助,我将不胜感激。
我在Django、DRF上有一个后端,也使用了JWT Simple,但这并不重要。
后端创建端点:
127.0.0.1:8000/auth/login/
127.0.0.1:8000/auth/register/
127.0.0.1:8000/auth/refresh/token
它们都工作正常。使用邮递员检查。
在 Next js 14 的前端: 在项目根目录中,我有 auth.config.ts ,代码如下:
import type { NextAuthConfig } from "next-auth";
export const authConfig = {
pages: {
signIn: "/login",
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isHomepage = nextUrl.pathname.startsWith("/");
if (isHomepage) {
if (isLoggedIn) return true;
return false; // Redirect unauthenticated users to login page
} else if (isLoggedIn) {
return Response.redirect(new URL("/", nextUrl));
}
return true;
},
},
providers: [], // Add providers with an empty array for now
} satisfies NextAuthConfig;
auth.ts:
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { authConfig } from "./auth.config";
import { API_URL } from "@/config";
export const { auth, signIn, signOut } = NextAuth({
...authConfig,
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
username: { label: "Username", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
const res = await fetch(`http://127.0.0.1:4000/auth/login/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: credentials.username,
password: credentials.password,
}),
});
const data = await res.json();
console.log("Ответ от API:", data);
if (res.ok) {
return {
id: data.user.id,
username: data.user.username,
email: data.user.email,
accessToken: data.access,
refreshToken: data.refresh,
} as any;
}
if (!res.ok) {
console.error("API request error:", data);
return null;
}
},
}),
],
});
中间件.ts:
import { getToken } from "next-auth/jwt";
import { NextResponse } from "next/server";
import { AUTH_SECRET } from "@/config";
export async function middleware(req: any) {
const session = await getToken({ req, secret: AUTH_SECRET });
const { pathname } = req.nextUrl;
const publicPaths = [
"/login",
"/register",
"/api/auth",
"/_next/static",
"/_next/image",
];
const isPublicPath = publicPaths.some((path) => pathname.startsWith(path));
if (!isPublicPath && !session) {
const loginUrl = new URL("/login", req.url);
loginUrl.searchParams.set("callbackUrl", req.url);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
export const config = {
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
matcher: ["/", "/terms", "/contact", "/dashboard", "/profile"],
};
应用程序/登录/page.tsx:
import LoginForm from "@/app/ui/login-form";
export default function LoginPage() {
return (
<main className="flex items-center justify-center md:h-screen">
<div className="relative mx-auto flex w-full max-w-[400px] flex-col space-y-2.5 p-4 md:-mt-32">
<div className="flex h-20 w-full items-end rounded-lg bg-blue-500 p-3 md:h-36">
<div className="w-32 text-white md:w-36">Login</div>
</div>
<LoginForm />
</div>
</main>
);
}
登录表单.tsx:
"use client";
import {
UserIcon,
KeyIcon,
ExclamationCircleIcon,
} from "@heroicons/react/24/outline";
import { ArrowRightIcon } from "@heroicons/react/20/solid";
import { Button } from "./button";
import { useFormState, useFormStatus } from "react-dom";
import { authenticate } from "@/app/lib/actions";
export default function LoginForm() {
const [errorMessage, dispatch] = useFormState(authenticate, undefined);
return (
<form action={dispatch} className="space-y-3">
<div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8">
<h1 className="mb-3 text-2xl">Please log in to continue.</h1>
<div className="w-full">
<div>
<label
className="mb-3 mt-5 block text-xs font-medium text-gray-900"
htmlFor="username"
>
Username
</label>
<div className="relative">
<input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
id="username"
type="text"
name="username"
placeholder="Enter your username"
required
/>
<UserIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />{" "}
</div>
</div>
<div className="mt-4">
<label
className="mb-3 mt-5 block text-xs font-medium text-gray-900"
htmlFor="password"
>
Password
</label>
<div className="relative">
<input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
id="password"
type="password"
name="password"
placeholder="Enter password"
required
minLength={6}
/>
<KeyIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
</div>
</div>
</div>
<LoginButton />
<div
className="flex h-8 items-end space-x-1"
aria-live="polite"
aria-atomic="true"
>
{errorMessage && (
<>
<ExclamationCircleIcon className="h-5 w-5 text-red-500" />
<p className="text-sm text-red-500">{errorMessage}</p>
</>
)}
</div>
</div>
</form>
);
}
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>
);
}
app/lib/actions.ts:
"use server";
import { signIn } from "@/auth";
import { AuthError } from "next-auth";
export async function authenticate(
prevState: string | undefined,
formData: FormData
) {
try {
await signIn("credentials", formData);
} catch (error) {
if (error instanceof AuthError) {
console.error("Authentication error:", error);
switch (error.type) {
case "CredentialsSignin":
return "Invalid credentials.";
default:
console.log(error.message);
return "Something got wrong." + error.message;
}
}
throw error;
}
}
是的,这确实很有挑战性。我花了几天时间才成功实施。我会尽力帮助你。请根据您的需要调整代码示例。这是一个真实的工作示例。
DESCRIPTION:在这个文件中,我们管理一些事情,例如登录过程,将未经授权的用户重定向到登录页面,保存刷新令牌,刷新过期的访问令牌等。请仔细观察。
import { cookies, headers } from "next/headers"
import NextAuth from "next-auth"
import type { NextAuthConfig } from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import { privateRoutes } from "@/contains/contants" // an array like ["/", "/account"]
// @ts-ignore
async function refreshAccessToken(token) { // this is our refresh token method
console.log("Now refreshing the expired token...")
try {
// change it with your own endpoint
const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/refresh`, {
method: "POST",
headers: headers(),
body: JSON.stringify({ userID: token.userId })
})
const { success, data } = await res.json() // change it with your own response type
if (!success) {
console.log("The token could not be refreshed!")
throw data
}
console.log("The token has been refreshed successfully.")
// get some data from the new access token such as exp (expiration time)
const decodedAccessToken = JSON.parse(Buffer.from(data.accessToken.split(".")[1], "base64").toString())
return {
...token,
accessToken: data.accessToken,
refreshToken: data.refreshToken ?? token.refreshToken,
idToken: data.idToken,
accessTokenExpires: decodedAccessToken["exp"] * 1000,
error: "",
}
} catch (error) {
console.log(error)
// return an error if somethings goes wrong
return {
...token,
error: "RefreshAccessTokenError",
}
}
}
export const config = {
trustHost: true,
theme: {
logo: "https://next-auth.js.org/img/logo/logo-sm.png",
},
providers: [
// we use credentials provider here
CredentialsProvider({
credentials: {
email: {
label: "email",
type: "email",
placeholder: "[email protected]",
},
password: {
label: "password",
type: "password",
},
},
async authorize(credentials, req) {
const payload = {
email: credentials.email,
password: credentials.password,
}
// external api for users to log in, change it with your own endpoint
const res = await fetch(`${process.env.API_BASE_URL}/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(payload)
})
const user = await res.json()
if (!res.ok) {
throw new Error(user.message)
}
if (res.ok && user) {
const prefix = process.env.NODE_ENV === "development" ? "__Dev-" : ""
// we set http only cookie here to store refresh token information as we will not append it to our session to avoid maximum size warning for the session cookie (4096 bytes)
cookies().set({
name: `${prefix}xxx.refresh-token`,
value: user.refreshToken,
httpOnly: true,
sameSite: "strict",
secure: true,
} as any)
return user
}
return null
}
})
],
// this is required
secret: process.env.AUTH_SECRET,
// our custom login page
pages: {
signIn: "/login",
},
callbacks: {
async jwt({ token, user, account }) {
if (account && user) {
token.id = user.id
token.accessToken = user.accessToken
token.refreshToken = user.refreshToken
token.role = "Unknown" // the user role
const decodedAccessToken = JSON.parse(Buffer.from(user.accessToken.split(".")[1], "base64").toString())
if (decodedAccessToken) {
token.userId = decodedAccessToken["sub"] as string
token.accessTokenExpires = decodedAccessToken["exp"] * 1000
}
// get some info about user from the id token
const decodedIdToken = JSON.parse(Buffer.from(user.idToken.split(".")[1], "base64").toString())
if (decodedIdToken) {
token.email = decodedIdToken["email"]
token.cognitoGroups = decodedIdToken["cognito:groups"]
token.role = decodedIdToken["cognito:groups"].length ? decodedIdToken["cognito:groups"][0] : "Unknown"
}
}
// if our access token has not expired yet, return all information except the refresh token
if (token.accessTokenExpires && (Date.now() < Number(token.accessTokenExpires))) {
const { refreshToken, ...rest } = token
return rest
}
// if our access token has expired, refresh it and return the result
return await refreshAccessToken(token)
},
async session({ session, token }) {
console.log("session => ", session)
return {
...session,
user: {
...session.user,
id: token.id as string,
email: token.email as string,
cognitoGroups: token.cognitoGroups as string[],
accessToken: token.accessToken as string,
accessTokenExpires: token.accessTokenExpires as number,
role: token.role as string
},
error: token.error,
}
},
authorized({ request, auth }) {
const { pathname } = request.nextUrl
// get route name such as "/about"
const searchTerm = request.nextUrl.pathname.split("/").slice(0, 2).join("/")
// if the private routes array includes the search term, we ask authorization here and forward any unauthorized users to the login page
if (privateRoutes.includes(searchTerm)) {
console.log(`${!!auth ? "Can" : "Cannot"} access private route ${searchTerm}`)
return !!auth
// if the pathname starts with one of the routes below and the user is already logged in, forward the user to the home page
} else if (pathname.startsWith("/login") || pathname.startsWith("/forgot-password") || pathname.startsWith("/signup")) {
const isLoggedIn = !!auth
if (isLoggedIn) {
return Response.redirect(new URL("/", request.nextUrl))
}
return true
}
return true
},
},
debug: process.env.NODE_ENV === "development",
} satisfies NextAuthConfig
export const { auth, handlers } = NextAuth(config)
我们的路由处理程序用于刷新我们在上面的文件中调用的访问令牌(路径:app/api/auth/refresh)
import { cookies } from "next/headers"
export async function POST(request: Request) {
const body = await request.json()
const prefix = process.env.NODE_ENV === "development" ? "__Dev-" : ""
const payload = {
refreshToken: cookies().get(`${prefix}xxx.refresh-token` as any)?.value,
userID: body.userID,
}
// change it with your own endpoint
const res = await fetch(`${process.env.API_BASE_URL}/auth/refresh`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(payload)
})
const data = await res.json()
return Response.json({
success: res.ok,
status: res.status,
data,
})
}
为了能够使用我们自己的会话道具,我们需要在项目的根级别添加 type 文件夹并将 next-auth.d.ts 文件放入其中
这是我们的 next-auth.d.ts 文件,如果没有它,打字稿会抱怨
import { DefaultSession } from "next-auth"
declare module "next-auth" {
interface User {
id: string
email: string
cognitoGroups: string[]
accessToken: string
refreshToken: string
idToken: string
exp: number
role: string
}
interface Session {
user: User & DefaultSession["user"]
expires: string
error: string
}
}
import { handlers } from "auth"
export const { GET, POST } = handlers
不要忘记在 tsconfig.json 文件中添加身份验证路径
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"auth": ["./auth"]
}
},
"include": ["process.d.ts", "next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
API_BASE_URL=https://api.yourdomain.com // external api for login, remember?
AUTH_SECRET=ac7uc0598805655a2b015a789ba889e0 // generate your secret here
AUTH_URL=http://localhost:3000/api/auth // important, you will have problems such as wrong redirect url issue without it
NEXT_PUBLIC_BASE_URL=http://localhost:3000
想知道制作过程吗?
API_BASE_URL=https://api.yourdomain.com // external api for login, remember?
AUTH_SECRET=bx7fg0593375655a2b015a789cc089x9 // just another secret
AUTH_URL=https://www.yourdomain.com/api/auth
NEXT_PUBLIC_BASE_URL=https://www.yourdomain.com
不要忘记在 Dockerfile 中添加这些变量(如果您使用的话)
与许多示例一样,您可以将页面的某些部分转换为客户端组件,如下所示,并从服务器组件页面调用它。我添加了客户端组件部分作为示例。
"use client"
import { useSearchParams } from "next/navigation"
import type { SignInResponse } from "next-auth/lib/client"
import { signIn } from "next-auth/react"
import { Route } from "@/routers/types"
export default function LoginForm() {
const searchParams = useSearchParams()
async function login({ email, password }: any) { // whatever your type
const callbackUrl = searchParams.get("callbackUrl")
signIn("credentials", {
email: email,
password: password,
redirect: false,
})
.then((res: SignInResponse | undefined) => {
if (!res) {
alert("No response!")
return
}
if (!res.ok)
alert("Something went wrong!")
else if (res.error) {
console.log(res.error)
if (res.error == "CallbackRouteError")
alert("Could not login! Please check your credentials.")
else
alert(`Internal Server Error: ${res.error}`)
} else {
if (callbackUrl)
router.push(callbackUrl as Route)
else
router.push("/")
}
})
}
return (
<>
// ...
<>
)
}
就是这样。
如果您有任何问题请告诉我。
奖金
我在这里给你一个 HOC 来实现基于角色的访问
/****** withAuth HOC ******
Use this HOC if your page needs authentication to be accessed
- If the page to be accessed requires only authentication (a logged in user),
pass an empty array as allowedRoles
- If the page to be accessed requires both authentication and authorization (a logged in user with a specific role),
pass a string array as allowedRoles
******/
import React from "react"
import { redirect } from "next/navigation"
import { auth } from "auth"
import PageFourOhThree from "@/app/(unauthorized)/four-oh-three/page" // you design it
/* eslint-disable react/display-name */
export default function withAuth(allowedRoles: string[]) {
return function <P extends object>(Component: React.FC<P>) {
return async function (props: P) {
const sess = await auth()
// redirect the user to the login page if not logged in
if (!sess)
redirect("/login")
// show 403 page if the user has not one of the required roles to access the page
if (allowedRoles.length)
if (!allowedRoles.includes(sess.user?.role)) {
return (
<PageFourOhThree />
)
}
return <Component {...(props as P)} />
}
}
}
这就是你如何使用它
import withAuth from "@/hoc/withAuth"
//..
export interface PageAboutProps {}
const PageAbout: FC<PageAboutProps> = () => {
//..
}
export default withAuth(["Administrator"])(PageAbout) // which means only the users with Administrator role can access to this page