Next.js 14 TS 中使用jwt 和 App Router 进行管理

server/2025/2/5 6:08:02/

        jwt是一个很基础的工作。但是因为架构不一样,就算是相同的架构,版本不一样,加jwt都会有一定的差别。现在我们的项目是Next.js 14 TS 的 App Router项目(就是没有pages那种),添加jwt的步骤:

1、安装所需的依赖:

npm install jsonwebtoken bcryptjs
npm install -D @types/jsonwebtoken @types/bcryptjs


2、配置环境变量

//在项目根目录(package.json所在目录)下创建一个.env.local文件,用于存储环境变量,例如我们的 //JWT 秘密密钥:
JWT_SECRET=my_super_secret_key
JWT_EXPIRES_IN=1h


3、我们在 app/api 文件夹中创建两个 API 路由:一个用于登录,一个用于保护的数据获取。

1. 登录 API (app/api/login/route.ts)
为实现登录功能,我们需要处理用户输入的用户名和密码,验证它们,创建 JWT 并返回给客户端。import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';const users = [{ id: 1, username: '1', password: await bcrypt.hash('1', 10) },{ id: 2, username: '2', password: await bcrypt.hash('2', 10) },
];export async function POST(request: NextRequest) {const { username, password } = await request.json();const user = users.find(u => u.username === username);if (!user || !(await bcrypt.compare(password, user.password))) {return NextResponse.json({ error: 'Invalid username or password' }, { status: 401 });}const token = jwt.sign({ userId: user.id, username: user.username }, process.env.JWT_SECRET!, { expiresIn: process.env.JWT_EXPIRES_IN });return NextResponse.json({ token });
}
2. 受保护的 API (app/api/protected/route.ts)
这个路由将在请求时检查并验证 JWT,并返回受保护的数据。
import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';export function GET(request: NextRequest) {const authHeader = request.headers.get('authorization');if (!authHeader || !authHeader.startsWith('Bearer ')) {return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });}const token = authHeader.split(' ')[1];try {const decodedToken = jwt.verify(token, process.env.JWT_SECRET!);return NextResponse.json({ message: 'This is protected data', user: decodedToken });} catch (err) {return NextResponse.json({ error: 'Invalid or expired token' }, { status: 401 });}
}


4、配置中间件

如果有多个受保护的路由,建议使用中间件来验证 JWT。这可以避免在每个受保护的路由中重复相同的验证逻辑。
在 app/middleware.ts 文件中:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import jwt from 'jsonwebtoken';export const middleware = (req: NextRequest) => {const token = req.cookies.get('token');if (req.nextUrl.pathname.startsWith('/api/protected')) {if (!token) {return NextResponse.json({ message: 'Authorization token missing' }, { status: 401 });}try {jwt.verify(token, process.env.JWT_SECRET!);return NextResponse.next();} catch (error) {return NextResponse.json({ message: 'Invalid token' }, { status: 401 });}}if (req.nextUrl.pathname.startsWith('/app/test')) {if (!token) {return NextResponse.redirect(new URL('/login', req.url));}try {jwt.verify(token, process.env.JWT_SECRET!);return NextResponse.next();} catch (error) {return NextResponse.redirect(new URL('/login', req.url));}}return NextResponse.next();
};export const config = {matcher: ['/api/protected/:path*', '/app/test/:path*'],
};


5、创建 AuthGuard 组件

创建 AuthGuard 组件 (app/components/AuthGuard.tsx)
我们使用一个高阶组件来实现路由保护逻辑。
'use client';// app/components/AuthGuard.tsx
import { ReactNode, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '../context/auth';const AuthGuard = ({ children }: { children: ReactNode }) => {const router = useRouter();const { isAuthenticated } = useAuth();useEffect(() => {if (!isAuthenticated) {router.push('/login');}}, [isAuthenticated, router]);if (!isAuthenticated) {return null; // 或者一个加载动画}return <>{children}</>;
};export default AuthGuard;


6、高阶组件将封装所有重复的逻辑:

创建 AuthGuardwithAuth高阶组件 (app/components/AuthGuardwithAuth.tsx)
// app/components/withAuth.tsx
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '../context/auth';
import AuthGuard from './AuthGuard';const withAuth = (WrappedComponent: React.ComponentType<any>) => {return (props: any) => {const [message, setMessage] = useState('');const { isAuthenticated, logout } = useAuth();const router = useRouter();//console.log("withAuth启动了");useEffect(() => {const fetchData = async () => {const token = localStorage.getItem('token');const res = await fetch('/api/protected', {headers: {'Authorization': `Bearer ${token}`,},});const data = await res.json();if (res.ok) {setMessage(data.message);} else {setMessage(data.error);logout(); // 如果token无效,注销用户router.push('/login');}};if (isAuthenticated) {fetchData();}}, [isAuthenticated, logout, router]);return (<AuthGuard><WrappedComponent {...props} message={message} /></AuthGuard>);};
};export default withAuth;

7、使用 React Context 管理登录状态 

使用 React Context 管理登录状态 (app/context/auth.tsx)
"use client"
// app/context/auth.tsx
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';interface AuthContextType {isAuthenticated: boolean;login: (token: string) => void;logout: () => void;
}const AuthContext = createContext<AuthContextType | undefined>(undefined);export const AuthProvider = ({ children }: { children: ReactNode }) => {const [isAuthenticated, setIsAuthenticated] = useState(false);useEffect(() => {const token = localStorage.getItem('token');if (token) {// Optionally, you can verify the token on the client side heresetIsAuthenticated(true);}}, []);const login = (token: string) => {localStorage.setItem('token', token);setIsAuthenticated(true);};const logout = () => {localStorage.removeItem('token');setIsAuthenticated(false);};return (<AuthContext.Provider value={{ isAuthenticated, login, logout }}>{children}</AuthContext.Provider>);
};export const useAuth = () => {const context = useContext(AuthContext);if (context === undefined) {throw new Error('useAuth must be used within an AuthProvider');}return context;
};

8、配置全局 AuthProvider

配置全局 AuthProvider (app/layout.tsx)
接下来,在 app/layout.tsx 中配置全局的 AuthProvider:
// src/app/layout.tsx
import type { Metadata } from 'next';
import { Roboto } from 'next/font/google';
import '@progress/kendo-theme-bootstrap/dist/all.css';
import './globals.css';
import IntlProviderWrapper from './IntlProviderWrapper';
import { AuthProvider } from './context/auth';export const metadata: Metadata = {title: 'Create Next App',description: 'Generated by create next app',
};const roboto = Roboto({weight: '400',subsets: ['latin'],
});export default function RootLayout({children,
}: {children: React.ReactNode
}) {return (<html lang="en"><body className={roboto.className}><AuthProvider><IntlProviderWrapper>{children}</IntlProviderWrapper></AuthProvider></body></html>);
}

9、登录页示例

登录页示例 (app/login/page.tsx)
'use client';import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '../context/auth';export default function Login() {const [username, setUsername] = useState('');const [password, setPassword] = useState('');const [error, setError] = useState('');const { login } = useAuth();const router = useRouter();const handleLogin = async () => {setError(''); // Reset error messageconst res = await fetch('/api/login', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({ username, password }),});const data = await res.json();if (res.ok) {login(data.token); // 使用 context 中的 login 方法router.push('/protected');} else {setError(data.error);}};return (<div><h1>Login</h1><inputtype="text"placeholder="Username"value={username}onChange={(e) => setUsername(e.target.value)}/><inputtype="password"placeholder="Password"value={password}onChange={(e) => setPassword(e.target.value)}/><button onClick={handleLogin}>Login</button>{error && <p style={{ color: 'red' }}>{error}</p>}</div>);
}

10、受保护页

import withAuth from '../components/AuthGuardwithAuth';
export default  withAuth(App)


http://www.ppmy.cn/server/165086.html

相关文章

PostgreSQL 插入、选择、更新、删除数据

&#xff08;1&#xff09;插入与选择操作 向表中插入数据主要是使用insert语句&#xff0c;结构如下所示&#xff1a; INSERT INTO 表名 (列名) VALUES &#xff08;具体值);这里需要注意的是&#xff1a;要使用单引号&#xff0c;单引号在 SQL 中用于定义字符串字面量&…

git进阶--1---HEAD、工作树和索引之间的区别与联系

git进阶–1—HEAD、工作树和索引之间的区别与联系 1. HEAD git中可以有很多的分支&#xff0c;本质都是一个指向commit对象的可变指针&#xff0c;HEAD是一个特别的指针&#xff0c;始终指向正在工作中的本地分支的指针&#xff0c;一句话&#xff0c;你当前处在哪个分支&…

人工智能搜索的层级发展趋势:从信息检索到智能决策

##引言 随着信息爆炸时代的来临&#xff0c;人们对搜索的需求不再仅仅停留在简单的关键词匹配。 人工智能&#xff08;AI&#xff09;技术的进步为搜索领域带来了革命性的变革&#xff0c;基于AI的搜索方式能够更智能地理解用户意图&#xff0c;提供更精准、更高效的搜索结果…

ZK-ALU-在有限域上实现左移

先看在实数域上实现左移, 再看在有限域上的实现 左移-整数 计算机中的左移计算&#xff08;<< 操作&#xff09;通常由处理器的硬件电路直接支持&#xff0c;因此效率非常高。在编程语言中&#xff0c;左移操作可以通过位移运算符&#xff08;例如 C/C 中的 <<&a…

ros 创建Topic

1、确定话题名称和消息类型 自定义话题名称&#xff0c;消息类型根据发送消息需要从std_msgs中查找确定 2、在main函数中通过NodeHander发布话题 // 创建一个NodeHandle对象&#xff0c;用于与ROS系统进行交互ros::NodeHandle nh;// 创建一个Publisher对象&#xff0c;用于发…

【算法】动态规划专题② ——LIS(最长递增子序列) python

目录 前置知识问题描述DP解法小试牛刀举一反三实战演练总结 前置知识 【算法】动态规划专题① ——线性DP python 问题描述 题目是说&#xff1a; 给定一个整数数组&#xff0c;找到其中最长的严格递增子序列的长度。&#xff08;子序列不要求连续&#xff09; 比如说&#xf…

数据结构——并查集

一、并查集原理 再一些应用问题中&#xff0c;需要将n个不同的元素划分成一些不相交的集合。开始时&#xff0c;每个元素自成一个单元素集合&#xff0c;然后按一定的规律将归于同一组元素的集合合并。在此过程中要反复用到查询某一个元素归属于哪个集合。适合这种问题的抽象…

javascript-es6 (一)

作用域&#xff08;scope&#xff09; 规定了变量能够被访问的“范围”&#xff0c;离开了这个“范围”变量便不能被访问 局部作用域 函数作用域&#xff1a; 在函数内部声明的变量只能在函数内部被访问&#xff0c;外部无法直接访问 function getSum(){ //函数内部是函数作用…