前言
在系統中要實作身份驗證是一件比較麻煩的事情,比如內建郵箱登入,手機号登入,以及其他第三方登入等,但是有了NextAuth.js,一切就變得簡單。正如官網說的添加身份驗證,隻要幾分鐘就可以實作。本文将實作郵箱登入、 Github 授權登入,以及密碼登入。那麼,一起來看看吧!
Next.js 應用接入 NextAuth
NextAuth.js 是 Next.js 應用程式的完整開源身份驗證解決方案,專門為 Next.js 設計,NextAuth 的特點:
- 靈活且易于使用,支援 OAuth1.0 OAuth2.0 和 OpenId 連結;
- 靈活資料管理,可以不使用資料庫,也可以選擇使用 MySQL, MariaDB, Postgres, SQL Server, MongoDB 以及 SQLite。
- 預設安全,預設 Cookie 機制,可開啟 JSON Web Token;
- NextAuth 推進無密碼的登入機制
- 支援 serverless 部署
安裝
首先我們使用 yarn 安裝 NextAuth.js
yarn add next-auth
授權 api
要通過 NextAuth.js 獲得授權, 需要先建立一個pages/api/auth/[...nextauth].ts 檔案,它包含了所有全局 NextAuth.js 配置。
import NextAuth from "next-auth"
import GithubProvider from "next-auth/providers/github"
export const authOptions = {
// 在 providers 中配置更多授權服務
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
// ...add more providers here
],
}
export default NextAuth(authOptions)
我們先添加一種授權登入方式,首先是使用 GITHUB 登入
Github 授權流程
圖源:網際網路
我之前使用過 Nodejs 內建 Github OAuth 流程,大緻要分為以上 6 個步驟,需要寫不少代碼和接口,但使用了 Next-auth.js, 就可以非常輕松的內建到我們的應用中,幾乎不用寫代碼。
注冊 GitHub OAuth Application
環境變量可以在 Github 開發者中申請,點選注冊一個新 OAuth Application:
圖源:網際網路
回調位址填http://localhost:3000/api/auth/callback/github
位址可以先填開發環境位址,待上前線前可以修改為正式域名位址,或者開發環境和生産環境單獨申請。
圖源:網際網路
注冊成功過後,在頁面上複制 Client ID 和 Client secrets 到 .env 檔案中
GITHUB_ID=你注冊的 GITHUB_ID
GITHUB_SECRET=你注冊的 GITHUB_SECRET
配置 pages/_app.ts
為了讓所有頁面能夠擷取到 Session, 我們需要在 pages/_app.ts 外層加SessionProvider
import { SessionProvider } from "next-auth/react"
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)
}
用戶端擷取登入資訊
然後我們就可以建立一個登入元件components/login-btn.tsx。
import { useSession, signIn, signOut } from "next-auth/react"
export default function Component() {
const { data: session } = useSession()
if (session) {
return (
<>
<span className="mr-1">session.user.email</span>
<button onClick={() => signOut()}>登出</button>
</>
)
}
return (
<button onClick={() => signIn()}>登入</button>
)
}
在首頁引用登入元件,就可以使用 GITHUB 來登入了,一起看來看看效果吧。
圖源:網際網路
注意:有時候會因為網絡問題, GitHub 無法登入。我們可以設定 NextAuthOptions 的 debug 為 true,會在控制台看到以下錯誤資訊:
圖源:網際網路
原因是通路 GitHub 需要代理,需要将代理設定為全局模式,并且設定請求 timeout 時間,将逾時時間延長。
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
httpOptions: {
timeout: 50000,
},
}),
登入成功後,我們看下頁面列印出來的資料,包含 GitHub 登入賬戶的基本資訊。
通過控制台我們可以發現,useSession 其實就是通路了http://localhost:3000/api/auth/session接口擷取資訊,這部分是在用戶端實作的,那麼在服務端可以擷取到使用者授權資訊嗎?
SSR 頁面擷取登入資訊
回到我們要開發的網站,還缺少個人管理頁面,這個頁面必須是目前登入使用者才能通路,沒授權,是不能通路的。
建立pages/me.tsx,用于使用者管理自己釋出的東西。
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { unstable_getServerSession } from "next-auth/next";
export default function Page() {
return <div>個人中心</div>;
}
export async function getServerSideProps(context) {
const session = await unstable_getServerSession(
context.req,
context.res,
authOptions
);
if (!session) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
return {
props: {
session,
},
};
}
此時通路 http://localhost:3000/me 若沒有授權登入,則将自動跳轉到首頁。
看列印出的session值,其中沒有 User 的 id,而我們的視訊表關聯的是 UserId ,是以我們需要将使用者的授權資訊同步到我們的資料表中。
Prisma 适配
next-auth.js 為 prisma 提供了擴充卡,我們隻需要按官網給出的步驟依次執行
- 安裝 prisma 擴充卡
yarn add @next-auth/prisma-adapter
- 在 NextAuth.js 配置 prisma 擴充卡
import NextAuth, { NextAuthOptions } from "next-auth";
import EmailProvider from "next-auth/providers/email";
import GithubProvider from "next-auth/providers/github";
import prisma from "@/lib/prisma";
+ import { PrismaAdapter } from "@next-auth/prisma-adapter";
export const authOptions: NextAuthOptions = {
//debug: true,
+ adapter: PrismaAdapter(prisma),
providers: [
// OAuth authentication providers...
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
+ callbacks: {
+ session: async ({ session, token, user }) => {
+ if (session?.user) {
+ session.user.id = user.id;
+ }
+ return session;
+ },
+ },
};
export default NextAuth(authOptions);
添加 callbacks 函數,将 user id 指派給 session 中的 user id,友善後面接口中可以直接擷取使用者 id。
- 添加 prisma Schema 中添加模型
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
當我們将這些模型粘貼到 Schema 後,會看到 VSCode 中有錯誤提示
圖源:網際網路
原因是我們之前設計的使用者表 id 是 Int 類型,跟目前的 Sring 類型不比對,解決辦法是将 Int 改成 String,最好的做法是所有表中的 id 類型改成統一。
- 遷移 Schema,生成表
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
httpOptions: {
timeout: 50000,
},
}),
執行完成後,我們重新整理頁面,重新登入頁面,來看下效果
圖源:網際網路
session 中已經有了 id,這裡我測試了下,将我 Github 預設郵箱改成另一個,也不會影響注冊使用者表中的資訊,因為 Account 表中的唯一值是provider + providerAccountId。
服務端渲染我的視訊
在 session 中可以擷取 userId,那麼我們就可以在 getServerSideProps 擷取目前使用者的資料了。
export async function getServerSideProps(context) {
const session = await unstable_getServerSession(
context.req,
context.res,
authOptions
);
if (!session) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
const data = await prisma.video.findMany({
where: {
authorId: session.user.id,
},
include: { author: true },
});
return {
props: {
session,
data: makeSerializable(data),
},
};
}
這裡有個問題,當我們擷取 user.id 的時候, typescript 會提示錯誤,因為預設的 User 類型中是不包含 id
圖源:網際網路
是以我們需要重寫下 next-auth 中 Session 的接口,建立 types/next-auth.d.ts 輸入以下代碼,就可以繼承預設的 Session TS 類型接口了
import NextAuth, { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
} & DefaultSession["user"];
}
}
添加完成後,在頁面中使用 useSession, unstable_getServerSession 等擷取到的 Session 不會 TS 類型報錯了。
郵箱授權登入
有了 Github 授權登入,并且關聯了資料庫,那要加上郵箱授權登入,便是輕而易舉。
首先安裝 nodemailer,用于 Node.js 發送郵件
yarn add nodemailer
然後在 pages/api/auth/[...nextauth].ts引入并且配置 EmailProvider
import EmailProvider from "next-auth/providers/email";
export const authOptions: NextAuthOptions = {
//debug: true,
adapter: PrismaAdapter(prisma),
providers: [
EmailProvider({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
//maxAge: 24 * 60 * 60, // 設定郵箱連結失效時間,預設24小時
}),
// OAuth authentication providers...
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
// ...
}
然後在 .env 檔案中配置環境變量
EMAIL_SERVER=smtp://username:[email protected]:587
EMAIL_FROM=NextAuth <[email protected]>
這裡的 EMAIL_SERVER 中的 username 就是發件郵箱的賬号,而 password 并不是郵箱密碼,需要在郵箱設定中開啟,這裡我以 163 郵箱為例
圖源:網際網路
登入郵箱後,在郵箱設定中開啟 POP3/SMTP/IMAP 服務,點選開啟,這裡會需要短信驗證,驗證會有一個授權密碼,這個授權碼就是 password, 最後面的服務位址和端口需要根據你最終選擇的 POP3/SMTP/IMAP 服務來配置,下圖是 126 郵箱的伺服器配置。
圖源:網際網路
配置完成後重新整理浏覽器就可以使用郵箱來完成登入了,登入的郵箱賬号不能是發送郵件服務的賬号,比如我設定的是發送郵件服務是 163 郵箱,那我注冊的時候使用 QQ 郵箱。
圖源:網際網路
點選 “sign in with Email” 後,你就會收到如下郵件,在郵箱中點選連結,便會自動授權登入成功。
登入成功後的,Session 中的資訊跟我 Github 賬号登入的資訊是一緻的,因為在資料庫中,郵箱位址是唯一值。
圖源:網際網路
更改郵件模闆
有些同學會說,發送的郵件主題太醜了,我們可以定制嗎?
放心,Next-auth 幫我們考慮到了 , EmailProvider 支援自定義模闆,我們需要配置 sendVerificationRequest 函數
import EmailProvider from "next-auth/providers/email";
...
providers: [
EmailProvider({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
sendVerificationRequest({
identifier: email,
url,
provider: { server, from },
}) {
/* your function */
},
}),
]
郵件模闆函數可能會很大,可以将 sendVerificationRequest 提取為單獨檔案,然後再引入;
import { createTransport } from "nodemailer";
import { SendVerificationRequestParams } from "next-auth/providers/email";
import { Theme } from "next-auth";
export async function sendVerificationRequest(
params: SendVerificationRequestParams
) {
const { identifier, url, provider, theme } = params;
const { host } = new URL(url);
const transport = createTransport(provider.server);
const result = await transport.sendMail({
to: identifier,
from: provider.from,
subject: `${host} 注冊認證`,
text: text({ url, host }),
html: html({ url, host, theme }),
});
const failed = result.rejected.concat(result.pending).filter(Boolean);
if (failed.length) {
throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`);
}
}
/**
*使用HTML body 代替正文内容
*/
function html(params: { url: string; host: string; theme: Theme }) {
const { url, host, theme } = params;
//由于使用
const escapedHost = host.replace(/\./g, ".");
return `
<body>
<div style="background:#f2f5f7;display: flex;justify-content: center;align-items: center; height:200px">歡迎注冊${escapedHost},點選<a href="${url}" target="_blank">登入</a></div>
</body>
`;
}
/** 不支援HTML 的郵件用戶端會顯示下面的文本資訊 */
function text({ url, host }: { url: string; host: string }) {
return `歡迎注冊 ${host}\n點選${url}登入\n\n`;
}
當然這裡我簡化了模闆代碼, 在真實場景中,我們也可以替換 HTML 檔案來實作。
密碼登入
密碼登入 Next-auth 是不鼓勵使用的,因為與密碼相關的固有安全風險以及與支援使用者名和密碼具有額外複雜性。
使用密碼登入需要使用 CredentialsProvider
import NextAuth, { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import prisma from "@/lib/prisma";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
export const authOptions: NextAuthOptions = {
//debug: true,
adapter: PrismaAdapter(prisma),
providers: [
CredentialsProvider({
// 登入按鈕顯示 (e.g. "Sign in with Credentials")
name: "Credentials",
// credentials 用于配置登入頁面的表單
credentials: {
email: {
label: "郵箱",
type: "text",
placeholder: "請輸入郵箱",
},
password: {
label: "密碼",
type: "password",
placeholder: "請輸入密碼",
},
},
async authorize(credentials, req) {
console.log(credentials);
// TODO
// const maybeUser= await prisma.user.findFirst({where:{
// email: credentials.email,
// }})
// 根據 credentials 我們查詢資料庫中的資訊
const user = {
id: "1",
name: "mayun",
email: "[email protected]",
};
if (user) {
// 傳回的對象将儲存才JWT 的使用者屬性中
return user;
} else {
// 如果傳回null,則會顯示一個錯誤,建議使用者檢查其詳細資訊。
return null;
// 跳轉到錯誤頁面,并且攜帶錯誤資訊 http://localhost:3000/api/auth/error?error=使用者名或密碼錯誤
//throw new Error("使用者名或密碼錯誤");
}
},
}),
],
session: {
strategy: "jwt",
},
jwt: {
secret: "test",
},
callbacks: {
async jwt({ token, user, account, profile, isNewUser }) {
if (user) {
token.id = user.id;
}
return token;
},
session: async ({ session, token, user }) => {
if (session?.user && token) {
session.user.id = token.id as string;
}
return session;
},
},
};
export default NextAuth(authOptions);
上面代碼中,我們首先需要開啟 JWT 模式,在 authorize方法中我們可以根據使用者所填的表單資訊進行資料庫查詢,由于我們的資料庫中沒有密碼字段,是以上面的代碼中直接傳回了一個固定 user 資訊,那真實的流程應該是:郵箱登入——> 設定密碼——>密碼登入
實作效果:
圖源:網際網路
自定義登入頁面
有同學會說,這個頁面怎麼這麼醜,既有中文也有英文呢?顯然在國内是不合适的, Next-auth 幫我們考慮到了,它支援配置自定義頁面。
在 pages/api/auth/[...nextauth].ts 添加 pages 參數就可以實作自定義
pages: {
signIn: '/auth/login',
},
自定義界面 ,可配置 signIn,signOut,error,verifyRequest 和 newUser,在這裡,我們隻配置登入頁面。
登入頁面的 dom 結構可以參考預設的 dom 結構, 直接複制出來就可以了。
圖源:網際網路
我們可以看到 form 表單中,有個預設的隐藏域,送出了 csrfToken 的值,那麼這個值該如何擷取呢?
import { getCsrfToken } from "next-auth/react"
export default function SignIn({ csrfToken }) {
return (
<form method="post" action="/api/auth/signin/email">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
<label>
Email address
<input type="email" id="email" name="email" />
</label>
<button type="submit">Sign in with Email</button>
</form>
)
}
export async function getServerSideProps(context) {
const csrfToken = await getCsrfToken(context)
return {
props: { csrfToken },
}
}
csrfToken 可以通過導出的 getCsrfToken 方法擷取,并且指派給隐藏域 csrfToken,在送出表單的時候,就會自動送出該值。
最後我們來看下實作效果:
圖源:網際網路
是不是有國内 App 的風格了呢?這裡我使用了 @chakra-ui/react 實作代碼也很簡單,這裡就不貼了,感興趣的小夥伴可以直接看我的 github。
還有些小夥伴會問,登入頁面能否能做成彈窗呢?當然也可以。
import { signIn } from "next-auth/react";
export default function Login() {
return (
<button
onClick={() =>
signIn("credentials", {
email: "[email protected]",
password: "1234",
})
}
>
登入
</button>
);
}
界面我們可以完全自定義,寫成一個元件,隻需要調用内置的 signIn 方法即可,它會幫我們自動添加 csrfToken值。
小結
本文通過 NextAuth.js, 給我們網站實作了郵箱登入、 Github 授權登入,以及密碼登入。你學會了嗎?若對你有幫助,記得幫我點點關注。