You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Next.js与DRF间基于JWT实现基础身份验证?

Django + Next.js JWT 身份验证实现方案

一、Django 后端配置(JWT 认证)

使用djangorestframework-simplejwt实现稳定的JWT认证,无需额外依赖社交登录组件。

1. 安装依赖

pip install djangorestframework djangorestframework-simplejwt

2. 配置 settings.py

添加必要的APP并配置JWT认证规则:

INSTALLED_APPS = [
    # 原有APP
    'rest_framework',
    'rest_framework_simplejwt',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
}

# 自定义JWT过期时间
from datetime import timedelta
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(hours=2),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
}

3. 自定义登录视图

返回JWT令牌的同时,附带前端需要的会员姓名、职位信息(敏感信息仅在后端接口返回):

from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework.response import Response
from rest_framework import status
from django.contrib.auth import authenticate
from .serializers import MemberSerializer  # 仅包含name、position字段的序列化器

class CustomTokenObtainPairView(TokenObtainPairView):
    def post(self, request, *args, **kwargs):
        username = request.data.get('username')
        password = request.data.get('password')
        user = authenticate(username=username, password=password)
        
        if not user:
            return Response({'error': '无效的登录凭据'}, status=status.HTTP_401_UNAUTHORIZED)
        
        # 生成JWT令牌
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        tokens = serializer.validated_data
        
        # 返回令牌+前端所需会员信息
        member_data = MemberSerializer(user).data
        return Response({
            'access': tokens['access'],
            'refresh': tokens['refresh'],
            'member': member_data
        })

4. 配置路由

添加登录和令牌刷新接口:

from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView
from .views import CustomTokenObtainPairView

urlpatterns = [
    path('api/token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    # 后端敏感信息接口(需认证)
    path('api/member-full-info/', get_member_full_info, name='member_full_info'),
]

5. 保护敏感信息接口

仅允许已认证用户访问邮箱、手机号等敏感数据:

from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.authentication import JWTAuthentication
from .serializers import MemberFullSerializer  # 包含email、phone的序列化器

@api_view(['GET'])
@authentication_classes([JWTAuthentication])
@permission_classes([IsAuthenticated])
def get_member_full_info(request):
    serializer = MemberFullSerializer(request.user)
    return Response(serializer.data)

二、Next.js(TypeScript + Tailwind)前端配置

1. 登录页实现

创建app/login/page.tsx,实现表单提交并存储JWT令牌:

'use client';

import { useState } from 'react';

export default function LoginPage() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');

    try {
      const res = await fetch('http://你的Django域名/api/token/', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password }),
      });

      if (!res.ok) {
        const data = await res.json();
        throw new Error(data.error || '登录失败');
      }

      const data = await res.json();
      // 存储令牌到Cookie(比localStorage更安全)
      document.cookie = `access_token=${data.access}; path=/; max-age=7200; secure; SameSite=Strict`;
      document.cookie = `refresh_token=${data.refresh}; path=/; max-age=604800; secure; SameSite=Strict`;
      
      // 存储会员信息到localStorage
      localStorage.setItem('member', JSON.stringify(data.member));
      
      // 跳转首页
      window.location.href = '/';
    } catch (err) {
      setError(err instanceof Error ? err.message : '未知错误');
    }
  };

  return (
    <div className="flex items-center justify-center min-h-screen bg-gray-100">
      <div className="w-full max-w-md p-8 bg-white rounded-lg shadow-md">
        <h2 className="text-2xl font-bold text-center mb-6">会员登录</h2>
        {error && <p className="text-red-500 text-center mb-4">{error}</p>}
        <form onSubmit={handleLogin}>
          <div className="mb-4">
            <label className="block text-gray-700 mb-2" htmlFor="username">用户名</label>
            <input
              type="text"
              id="username"
              className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
              value={username}
              onChange={(e) => setUsername(e.target.value)}
              required
            />
          </div>
          <div className="mb-6">
            <label className="block text-gray-700 mb-2" htmlFor="password">密码</label>
            <input
              type="password"
              id="password"
              className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              required
            />
          </div>
          <button
            type="submit"
            className="w-full py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
          >
            登录
          </button>
        </form>
      </div>
    </div>
  );
}

2. 请求封装与令牌自动刷新

创建utils/api.ts,自动处理令牌携带和过期刷新:

import Cookies from 'js-cookie';

const API_BASE_URL = 'http://你的Django域名/api';

export const apiRequest = async (url: string, options: RequestInit = {}) => {
  let accessToken = Cookies.get('access_token');
  const refreshToken = Cookies.get('refresh_token');

  const headers = {
    'Content-Type': 'application/json',
    ...options.headers,
  };

  if (accessToken) {
    headers['Authorization'] = `Bearer ${accessToken}`;
  }

  let res = await fetch(`${API_BASE_URL}${url}`, { ...options, headers });

  // 处理令牌过期
  if (res.status === 401 && refreshToken) {
    const refreshRes = await fetch(`${API_BASE_URL}/token/refresh/`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refresh: refreshToken }),
    });

    if (refreshRes.ok) {
      const data = await refreshRes.json();
      Cookies.set('access_token', data.access, { expires: 2/24, secure: true, sameSite: 'Strict' });
      // 重新发起原请求
      headers['Authorization'] = `Bearer ${data.access}`;
      res = await fetch(`${API_BASE_URL}${url}`, { ...options, headers });
    } else {
      // 刷新失败,清空令牌并跳转登录页
      Cookies.remove('access_token');
      Cookies.remove('refresh_token');
      localStorage.removeItem('member');
      window.location.href = '/login';
      return Promise.reject(new Error('登录已过期'));
    }
  }

  return res;
};

3. 路由保护

创建middleware.ts,拦截未登录用户访问受保护页面:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const accessToken = request.cookies.get('access_token')?.value;

  // 未登录且访问非登录页,重定向到登录
  if (!accessToken && request.nextUrl.pathname !== '/login') {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // 已登录且访问登录页,重定向到首页
  if (accessToken && request.nextUrl.pathname === '/login') {
    return NextResponse.redirect(new URL('/', request.url));
  }

  return NextResponse.next();
}

// 指定需要保护的路由
export const config = {
  matcher: ['/', '/dashboard/:path*'],
};

4. 展示会员信息与敏感数据

在首页展示姓名和职位:

'use client';

import { useEffect, useState } from 'react';
interface Member { name: string; position: string; }

export default function Home() {
  const [member, setMember] = useState<Member | null>(null);

  useEffect(() => {
    const storedMember = localStorage.getItem('member');
    if (storedMember) setMember(JSON.parse(storedMember));
  }, []);

  if (!member) return <div className="container mx-auto p-4">加载中...</div>;

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-4">欢迎回来,{member.name}</h1>
      <p className="text-lg">你的职位:{member.position}</p>
      <a href="/dashboard" className="mt-4 inline-block px-4 py-2 bg-gray-200 rounded hover:bg-gray-300">
        查看个人详情
      </a>
    </div>
  );
}

在app/dashboard/page.tsx获取敏感信息:

'use client';

import { useEffect, useState } from 'react';
import { apiRequest } from '@/utils/api';
interface MemberFull { name: string; position: string; email: string; phone: string; }

export default function Dashboard() {
  const [memberInfo, setMemberInfo] = useState<MemberFull | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  useEffect(() => {
    const fetchInfo = async () => {
      try {
        const res = await apiRequest('/member-full-info/');
        if (!res.ok) throw new Error('获取信息失败');
        setMemberInfo(await res.json());
      } catch (err) {
        setError(err instanceof Error ? err.message : '未知错误');
      } finally {
        setLoading(false);
      }
    };
    fetchInfo();
  }, []);

  if (loading) return <div className="container mx-auto p-4">加载中...</div>;
  if (error) return <div className="container mx-auto p-4 text-red-500">{error}</div>;

  return (
    <div className="container mx-auto p-4">
      <h2 className="text-2xl font-bold mb-4">个人详情</h2>
      <div className="space-y-2">
        <p><strong>姓名:</strong>{memberInfo?.name}</p>
        <p><strong>职位:</strong>{memberInfo?.position}</p>
        <p><strong>邮箱:</strong>{memberInfo?.email}</p>
        <p><strong>手机号:</strong>{memberInfo?.phone}</p>
      </div>
    </div>
  );
}

内容的提问来源于stack exchange,提问作者TheIgnitedWizard

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 19:22:42