那里有几个身份服务提供商,将开发商板的复杂性和安全考虑因素带走。 Next.js也有身份验证库,例如Next-auth 。但是有时您想为不涉及敏感用户数据的简单Web应用程序实现非常基本的身份验证。
在本教程中,我们将引导您完成Next.js中创建基本身份验证提供商的过程。无论您是经验丰富的开发人员还是刚从Next.js开始,您都会发现本教程有助于建立基本的身份验证提供商。所以让我们开始吧!
验证提供者的解剖学
我们的身份验证流将由两个部分组成:
- 一个反应上下文提供商和
useAuth
钩 - 一个身份验证和服务用户数据的后端API
这是我们的身份验证流的运作方式:
- 用户试图访问受保护的页面
-
useAuth
检查是否有登录cookie,如果不是登录页面 - 用户输入登录和密码
- 后端API接收登录数据,并在DB中查找用户
- 生成了独特的令牌,并将其返回给客户端
-
useAuth
设置当前用户状态并设置一个包含登录令牌的cookie - 随后刷新的用户数据来自登录cookie数据
- 登录cookie数据已针对背景中的新鲜API数据进行验证
设置我们的项目
可以在页面底部找到本教程的演示站点和回购。该演示是用npx create-next-app@latest
与打字稿和尾风CSS创建的。
依赖项:
软件包名称 | 目的 |
---|---|
cookie | 包装cookie从请求数据 |
cookie-next | 用于在Next.js | 中管理cookie的软件包
bcryptjs | 用于哈希和比较密码的软件包 |
安装类型:
npm i -D @types/bcryptjs @types/cookie
创建我们的上下文提供商
我们将使用React的useContext
钩创建我们的身份验证上下文:
/**
* Basic Authentication Provider
*
* @author Jay Simons
*
*/
import React, { useState, useEffect, useContext, createContext, ReactNode } from "react";
import { getCookie, setCookie, deleteCookie } from 'cookies-next';
import { useRouter } from "next/router";
import { User, LoginToken, LoginCookie, LoginProps, AuthContextType } from '@/interfaces';
const COOKIE_KEY = process.env.NEXT_PUBLIC_AUTH_COOKIE_KEY || '';
const authContext = createContext<AuthContextType>({
user: null,
login: async () => false,
logout: async () => { },
reload: () => { },
sendPasswordResetEmail: () => { },
setLoginCookie: () => { },
});
// Auth provider context
export function AuthProvider({ children }: { children: ReactNode }) {
const auth = useAuthProvider();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}
// Hook for child components to get the auth object ...
// ... and re-render when it changes.
export const useAuth = () => {
return useContext(authContext);
};
// Provider hook that creates auth object and handles state
function useAuthProvider() {
const [user, setUser] = useState<User | null>(null);
const cookieData = getCookie(COOKIE_KEY);
const router = useRouter();
const login = async (props: LoginProps) => {
const result = await fetch("/api/auth/login", {
method: 'POST',
body: JSON.stringify(props),
headers: {
'Content-Type': 'application/json'
}
});
if (result.ok) {
const data = await result.json();
setUser(data.user);
setLoginCookie(data);
return true;
} else {
const error = await result.text();
const errMess = result.status === 401 ? "Invalid username or password" : 'Login failed';
console.error(error);
throw new Error(errMess);
}
};
// Tell the API we're loggin out and delete out login cookie
const logout = async () => {
const result = await fetch('/api/auth/logout');
if (!result.ok) throw new Error("Log out failed");
deleteLoginCookie();
setUser(null);
return;
};
// Store login token / user data in cookie
const setLoginCookie = (data: LoginCookie) => {
const expires = new Date(data.token.expires);
setCookie(COOKIE_KEY, data, { path: '/', expires: expires });
}
// Delete login cookie
const deleteLoginCookie = () => {
deleteCookie(COOKIE_KEY);
}
// Pull user data from API
const loadUser = async () => {
console.log("Pulling user data...");
async function fetchUser(cookieParsed: LoginCookie) {
try {
const result = await fetch("/api/auth/me");
if (result.ok) {
const user = await result.json();
console.log("Done fetching user!");
setUser(user);
cookieParsed.user = user;
setLoginCookie(cookieParsed);
} else {
setUser(null);
deleteLoginCookie();
}
} catch (err) {
console.error(err);
}
}
if (typeof cookieData === 'string') {
const cookieParsed = JSON.parse(cookieData);
setUser(cookieParsed.user);
fetchUser(cookieParsed);
} else {
setUser(null);
}
}
// Reload user data
const reload = () => {
setTimeout(() => {
loadUser();
}, 200);
}
// Send password reset email
const sendPasswordResetEmail = (email: string) => {
/* TODO */
};
// Subscribe to user on mount
useEffect(() => {
loadUser();
}, []); //eslint-disable-line
// Return the user object and auth methods
return {
user,
login,
logout,
reload,
sendPasswordResetEmail,
setLoginCookie,
};
}
为提供商创建authContext
并定义AuthProvider
和useAuth
函数:
本节定义了authContext
和两个函数:
-
AuthProvider
:包装应用程序并为其子零件提供authContext
的组件。 -
useAuth
:一个由子组件用来访问auth
对象的钩子。
定义useAuthProvider
钩
本节定义了useAuthProvider
钩,这是该代码的核心功能。它管理用户状态,处理服务器的身份验证请求,并将用户数据存储在cookie中。 useAuthProvider
中的功能是:
-
login
:将邮政请求发送给用户凭据,并设置用户状态和cookie数据,如果成功。 -
logout
:向服务器发送请求以注销用户并删除用户状态和cookie数据。 -
setLoginCookie
:在cookie中存储登录令牌和用户数据。 -
deleteLoginCookie
:从cookie中删除登录令牌和用户数据。 -
loadUser
:如果请求成功,请从服务器检索用户数据,并设置用户状态和cookie数据。 -
reload
:从服务器重新加载用户数据的新副本。 -
sendPasswordResetEmail
:发送密码重置电子邮件(目前未实施)。
导出AuthProvider
和useAuth
函数:
本节导出了AuthProvider
和useAuth
函数,以便可以在其他组件中使用。
在我们的应用中实施
首先,我们需要打开_app.tsx
并在我们的AuthProvider
上下文中包装所有内容:
import '@/styles/globals.css'
import type { AppProps } from 'next/app'
import { Inter } from 'next/font/google'
import { AuthProvider } from '@/hooks/useAuth';
const inter = Inter({ subsets: ['latin'] })
export default function App({ Component, pageProps }: AppProps) {
return (
<AuthProvider>
<main className={inter.className}>
<Component {...pageProps} />
</main>
</AuthProvider>
)
}
接下来,我们需要一种处理受保护和未保护的页面的方法。实现此目的的最佳方法是在布局高阶组件中进行操作:
import React, { ReactNode } from 'react'
import Head from 'next/head'
import Footer from './Footer'
import LoginWrapper from '../Login'
import { useAuth } from '@/hooks/useAuth'
export default function Layout(props: {
children: ReactNode,
pageTitle?: string,
requireAuth?: boolean
}) {
const auth = useAuth();
const {
children,
pageTitle,
requireAuth = false
} = props;
return (
<div className="min-h-screen flex flex-col bg-gradient-to-b from-slate-600 to-slate-700 text-white">
{
requireAuth && !auth.user
?
<LoginWrapper />
:
<>{children}</>
}
<Footer />
</div>
)
}
此组件将有条件地渲染<LoginWrapper>
或{children}
,具体取决于我们是否设置requireAuth
标志。
这是<LoginWrapper>
:
import React, { useEffect, useState } from 'react'
import Login from './Login'
/**
* This component is used to wrap the login page.
* It delays showing the login page
* to give time for auth.user to load
*
* @author Jay Simons
*/
export default function LoginWrapper() {
const [showLogin, setShowLogin] = useState<boolean>(false);
useEffect(() => {
setTimeout(() => {
setShowLogin(true);
}, 1000);
}, []);
if (showLogin) {
return (
<Login />
)
} else {
return (
<div className="flex h-screen">
<div className="m-auto text-2xl fade-text">Checking Login...</div>
</div>
)
}
}
我们使用useEffect
等一秒钟,然后显示登录组件。这是为了给useAuth
时间从cookie或server获取用户数据。
接下来是我们的登录表单组件:
import React, { useState } from 'react'
import Link from 'next/link'
import { useAuth } from '@/hooks/useAuth'
export default function Login() {
const auth = useAuth();
const [email, setEmail] = useState<string>('joeblow');
const [password, setPassword] = useState<string>('TestPassword4$');
const [error, setError] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const handleResetPassword = () => {
// TODO: Implement
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.name === 'email') {
setEmail(e.target.value);
} else if (e.target.name === 'password') {
setPassword(e.target.value);
}
}
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
try {
await auth.login({
username: email,
password
});
} catch (err) {
if (err instanceof Error) {
setError(err.message);
}
} finally {
setLoading(false);
}
}
return (
<div className="m-auto w-full md:w-[300px] flex flex-col gap-6">
<h1 className="text-center text-2xl font-medium">Please Log In</h1>
{error && <p className="text-center text-red-500">{error}</p>}
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<input
name="email"
className="border border-gray-300 p-2 rounded-md text-gray-800"
type="text"
placeholder="Username or Email"
onChange={handleInputChange}
defaultValue="joeblow"
/>
<input
name="password"
className="border border-gray-300 p-2 rounded-md text-gray-800"
type="password"
placeholder="Password"
onChange={handleInputChange}
defaultValue="TestPassword4$"
/>
<button
className={`btn-base ${loading ? 'bg-gray-500 opacity-50' : 'bg-blue-500 hover:bg-blue-600'}`}
type="submit"
disabled={loading}
>
{loading ? <span className="fade-text">Loading...</span> : 'Log In'}
</button>
</form>
<p className="text-center">
Don't have an account?{' '}
<Link className="link" href="/login">Register</Link>
</p>
<p className="text-center">
Forgot your password?{' '}
<button
className="link"
onClick={handleResetPassword}
>Reset</button>
</p>
</div>
)
}
非常直接。我们的handleSubmit
功能只需使用提供的登录ID和密码调用auth.login
。请注意,登录ID可以是用户名或电子邮件地址。
处理后端
我们有三个端点要定义:
- /api/auth/login-负责身份验证用户名和密码
- /api/auth/login-负责从用户记录中删除登录令牌
- /api/auth/me-负责基于登录cookie 从服务器提供新的用户数据
这是我们的登录端点:
import fakeDb from "@/util/fakeDb";
import { compareSync } from "bcryptjs";
import sanitizeUser from "@/util/sanitizeUser";
// Use edge runtime to improve performance.
export const config = {
runtime: 'edge'
}
export async function unauth() {
return new Response('Unauthorized', { status: 401 });
}
export default async function handler(request: Request) {
try {
const data = await request.json();
let user;
// Fetch user from database
try {
user = await fakeDb(data.username);
} catch (err) {
return unauth();
}
// Check if user exists
if (!user) return unauth();
// Verify password
if (!compareSync(data.password, user.password)) {
return unauth();
}
// Generate token
const token = user.tokens[0]; // Normally we would generate one
// Return user and token
return new Response(JSON.stringify({
user: sanitizeUser(user),
token
}), {
headers: {
'Content-Type': 'application/json'
}
});
} catch (err) {
if (err instanceof Error) {
return new Response(err.message, { status: 500 });
} else {
return new Response('Unknown error', { status: 500 });
}
}
}
在我的示例中,我正在使用Edge运行时进行超快速速度,但是如果您喜欢
我创建了一个fakeDb
函数,该功能模拟了对数据库的调用,以稍微延迟获取用户数据。您可以在下面的存储库上找到该代码。
最后,这是我们的/api/auth/me端点的代码:
import fakeDb from "@/util/fakeDb";
import sanitizeUser from "@/util/sanitizeUser";
import { parse } from "cookie";
// Use edge runtime to improve performance.
export const config = {
runtime: 'edge'
}
export async function unauth() {
return new Response('Unauthorized', { status: 401 });
}
export default async function handler(request: Request) {
try {
// Fetch login cookie
const cookies = parse(request.headers.get('Cookie') || '');
const cookie = cookies[process.env.NEXT_PUBLIC_AUTH_COOKIE_KEY || ''];
if (!cookie) return unauth();
const cookieData = JSON.parse(cookie);
// Check if cookie is valid
if (!cookieData.token || !cookieData.token.token || !cookieData.token.expires) return unauth();
if (new Date(cookieData.token.expires) < new Date()) return unauth();
// Fetch user from database
let user;
try {
user = await fakeDb(cookieData.token.token);
} catch (err) {
return unauth();
}
// Check if user exists
if (!user) return unauth();
// Return user and token
return new Response(JSON.stringify(sanitizeUser(user)), {
headers: {
'Content-Type': 'application/json'
}
});
} catch (err) {
if (err instanceof Error) {
return new Response(err.message, { status: 500 });
} else {
return new Response('Unknown error', { status: 500 });
}
}
}
与登录端点非常相似,只是我们从登录cookie中的存储令牌中标识了用户,而不是提供的登录ID和密码。我们还希望确保检查令牌是否未过期。通常,您会在DB服务器上运行的清理CRON作业,以检查已过期的令牌,但我们不想仅依靠它。
我离开了注销端点的代码,因为它只是返回空白200响应。在生产环境中,您通常会包含代码以从数据库中的用户记录中删除登录令牌。在此演示中,登录时仅删除客户端cookie。
链接
感谢您抽出宝贵的时间阅读我的文章,希望您发现它很有用(或至少很有趣)。有关Web开发,系统管理和云计算的更多信息,请阅读Designly Blog。另外,请留下您的评论!我喜欢听读者的想法。
我使用Hostinger托管我的客户网站。您可以获得一个可以以$ 3.99/mo的价格托管100个网站的业务帐户,您最多可以锁定48个月!这是镇上最好的交易。服务包括PHP托管(带有扩展),MySQL,WordPress和电子邮件服务。
寻找网络开发人员?我可以租用!要查询,请填写一个contact form。