Next.js 14 TS 中使用jwt 和 App Router 进行管理
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 here
setIsAuthenticated(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 message
const 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>
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="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)