Bootstrap

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)

;