如何在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
相关产品推荐
相关产品推荐

