NextAuth.js

NextAuth.js

简介

NextAuth.js 是 Next.js 应用程序的完整开源身份验证解决方案。

使用方式

首先需要安装依赖包:

1
npm install next-auth@beta

注:此处由于 5.0 还没发布所以采用 beta 版本。在部署的时候有一些缓存 bug 可以降级 next.js 版本至 14.1.4 。

然后使用如下命令生成 AUTH_SECRET

1
npx auth secret

编写 .env.local 文件:

1
2
3
AUTH_SECRET=<auth_secret>
AUTH_<OAuth_Provider>_ID=<oauth_client_id>
AUTH_<OAuth_Provider>_SECRET=<oauth_secret>

注:测试服务使用 GitHub 作为服务提供端,服务创建请访问 示例文档 并填写好 .env.local 文件。

编写 auth/index.ts 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import NextAuth, {NextAuthConfig} from "next-auth"

import GitHub from "@auth/core/providers/github";

export const BASE_PATH = "/api/auth"

const authOptions:NextAuthConfig = {
providers: [
GitHub,
],
basePath: BASE_PATH,
secret: process.env.AUTH_SECRET,
debug: process.env.NODE_ENV !== "production"}

export const {handlers, auth, signIn, signOut} = NextAuth(authOptions);

编写 middleware.ts :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { NextResponse } from "next/server";
import { auth, BASE_PATH } from "@/auth";

export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

export default auth((req) => {
const reqUrl = new URL(req.url);
if (!req.auth && reqUrl?.pathname !== "/") {
return NextResponse.redirect(
new URL(
`${BASE_PATH}/signin?callbackUrl=${encodeURIComponent(
reqUrl?.pathname
)}`,
req.url
)
);
}
});

注:此处可以编写若未登录之后的重定向和路由保存逻辑。

编写 api/auth/[...nextauth]/route.ts

1
2
3
import { handlers } from "@/auth";

export const { GET, POST } = handlers;

常用方式

自定义 OAuth 服务器

auth.ts 中修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import {OAuthConfig} from "@auth/core/providers";

const customProvider: OAuthConfig<any> = {
id: 'demo',
name: 'demo',
type: 'oauth',
authorization: {
url: "http://<host>/oauth2/authorize",
params: { scope: "profile" },
},
issuer: "http://<host>",
token: 'http://<host>/oauth2/token',
userinfo: 'http://<host>/oauth2/userinfo',
clientId: process.env.CUSTOM_CLIENT_ID,
clientSecret: process.env.CUSTOM_SECRET,
profile(profile) {
return {
id: profile.sub,
name: profile.name
};
},
}

自定义 OIDC 服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {OIDCConfig} from "@auth/core/providers";

const customProvider: OIDCConfig<any> = {
id: 'oidc-client',
name: 'oidc-client',
type: 'oidc',
authorization: {
url: "http://<host>/oauth2/authorize",
params: { scope: "openid" },
},
issuer: "http://<host>",
token: 'http://<host>/oauth2/token',
userinfo: 'http://<host>/oauth2/userinfo',
clientId: process.env.CUSTOM_CLIENT_ID,
clientSecret: process.env.CUSTOM_SECRET,
}

独立服务简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const config = {
providers: [
Credentials({
name: "Credentials",
credentials: {
username: {
label: "Username:",
type: "text",
placeholder: "your-cool-username"
},
password: {
label: "Password:",
type: "password",
placeholder: "your-awesome-password"
}
},
async authorize(credentials) {
// This is where you need to retrieve user data
// to verify with credentials
// Docs: https://next-auth.js.org/configuration/providers/credentials
const user = { id: "42", name: "Dave", password: "nextauth" }
if (credentials?.username === user.name && credentials?.password === user.password) {
return user
} else {
return null
}
}
})
],
} satisfies NextAuthConfig

无页面跳转登录

编写 auth/helpers.ts 文件

1
2
3
4
5
6
7
8
9
10
"use server";
import {signIn as naSignIn, signOut as naSignOut} from ".";

export async function signIn(prop:any) {
await naSignIn(prop);
}

export async function signOut() {
await naSignOut();
}

注:此处可以传入提供方ID,实现快速登录,跳过登录页。之后如果是 oauth2 登录还需要在 oauth2 处登出。

编写 app/AuthButton.client.tsx 文件:

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"use client"

import {useSession} from "next-auth/react";

import {Button} from "@mui/material"

import {signIn, signOut} from "@/auth/helpers";

export default function AuthButton() {
const session = useSession();
return session?.data?.user? (
<Button onClick={async ()=> await signOut()}>
{session.data?.user?.name}: Sign Out
</Button>
) : (
<Button onClick={async ()=> await signIn('oidc-client')}>
Sign In
</Button>
);
}

编写 app/AuthButton.server.tsx 文件

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {SessionProvider} from "next-auth/react";
import {BASE_PATH, auth} from "@/auth";

import AuthButtonClient from "@/app/AuthButton.client";

export default async function AuthButton() {
const session = await auth();
if (session && session.user) {
session.user = {
name: session.user.name,
email: session.user.email
};
}

return (
<SessionProvider basePath={BASE_PATH} session={session}>
<AuthButtonClient/>
</SessionProvider>
)
}

修改 app/page.tsx 文件

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {auth} from "@/auth";
import AuthButton from "@/app/AuthButton.server";

export default async function Home() {
const session = await auth();
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<h1 className="text-3xl font-bold underline">
Hello world!
</h1>
<pre>{JSON.stringify(session, null, 2)}</pre>
<AuthButton/>
</main>
);
}

服务端和客户端获取用户信息

编写 WhoAmIServerAction.tsx

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"use client";

import {useEffect, useState} from "react";

export default function WhoAmIServerAction({
onGetUserAction,
}: {
onGetUserAction: () => Promise<string | null>;
}) {
const [user, setUser] = useState<string | null>();

useEffect(() => {
onGetUserAction().then((user) => setUser(user));
}, []);
return <div>Who Am I (server action): {user}</div>;
}

编写 page.tsx

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {auth} from "@/auth";

import WhoAmIServerAction from "./WhoAmIServerAction";

export default async function TestRoute() {
const session = await auth();

async function onGetUserAction() {
"use server";
const session = await auth();
return session?.user?.name ?? null;
}

return (
<main>
<h1>Test Route</h1>
<div>User: {session?.user?.name}</div>
<WhoAmIServerAction onGetUserAction={onGetUserAction}/>
</main>
)
}

参考资料

官方文档

示例项目

视频教程


NextAuth.js
https://wangqian0306.github.io/2024/next-auth/
作者
WangQian
发布于
2024年5月24日
许可协议