19.3 JWT 身份验证
19.3 JWT 身份验证
现代Web应用的身份验证解决方案中,JSON Web Tokens (JWT) 已成为主流选择。本节将深入讲解如何在前端应用中安全地实现JWT身份验证流程。
JWT 核心概念
1. JWT 结构解析
头部.载荷.签名
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- Header:算法和token类型
- Payload:包含声明(claims)的数据
- Signature:防止篡改的加密签名
2. 前端JWT生命周期管理
class AuthService {
constructor() {
this.token = null;
this.refreshToken = null;
this.tokenExpiration = null;
this.refreshTimeout = null;
}
async login(credentials) {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
if (!response.ok) throw new Error('登录失败');
const { token, refreshToken, expiresIn } = await response.json();
this.setTokens(token, refreshToken, expiresIn);
}
setTokens(token, refreshToken, expiresIn) {
this.token = token;
this.refreshToken = refreshToken;
this.tokenExpiration = Date.now() + expiresIn * 1000;
// 设置token自动刷新
this.scheduleRefresh();
// 存储到安全的HttpOnly cookie或内存
if (this.shouldPersist()) {
localStorage.setItem('refreshToken', refreshToken);
}
}
scheduleRefresh() {
// 在token过期前5分钟刷新
const refreshDelay = this.tokenExpiration - Date.now() - 300000;
clearTimeout(this.refreshTimeout);
this.refreshTimeout = setTimeout(
() => this.refresh(),
Math.max(0, refreshDelay)
);
}
async refresh() {
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.refreshToken}`
}
});
if (!response.ok) throw new Error('刷新失败');
const { token, expiresIn } = await response.json();
this.setTokens(token, this.refreshToken, expiresIn);
} catch (err) {
this.logout();
throw err;
}
}
logout() {
clearTimeout(this.refreshTimeout);
this.token = this.refreshToken = this.tokenExpiration = null;
localStorage.removeItem('refreshToken');
// 调用后端注销端点
fetch('/api/auth/logout', {
credentials: 'include'
});
}
}
React 集成方案
1. 认证上下文提供
// contexts/AuthContext.js
import React, { createContext, useContext, useEffect, useState } from 'react';
import AuthService from '../services/AuthService';
const authService = new AuthService();
const AuthContext = createContext();
export function AuthProvider({ children, persist = false }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function loadUser() {
try {
if (authService.token) {
const user = await fetchCurrentUser();
setUser(user);
} else if (persist && localStorage.getItem('refreshToken')) {
await authService.refresh();
const user = await fetchCurrentUser();
setUser(user);
}
} catch (err) {
console.error('认证加载失败:', err);
} finally {
setIsLoading(false);
}
}
loadUser();
}, [persist]);
const value = {
user,
isLoading,
login: async (credentials) => {
await authService.login(credentials);
const user = await fetchCurrentUser();
setUser(user);
},
logout: () => {
authService.logout();
setUser(null);
}
};
return (
<AuthContext.Provider value={value}>
{!isLoading && children}
</AuthContext.Provider>
);
}
export function useAuth() {
return useContext(AuthContext);
}
async function fetchCurrentUser() {
const response = await fetch('/api/auth/me', {
headers: {
'Authorization': `Bearer ${authService.token}`
}
});
return response.json();
}
2. 保护路由组件
// components/ProtectedRoute.jsx
import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
export function ProtectedRoute({ children, roles = [] }) {
const { user, isLoading } = useAuth();
const location = useLocation();
if (isLoading) {
return <div>加载中...</div>;
}
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (roles.length > 0 && !roles.some(role => user.roles.includes(role))) {
return <Navigate to="/unauthorized" replace />;
}
return children;
}
// 使用示例
<Route
path="/dashboard"
element={
<ProtectedRoute roles={['admin']}>
<Dashboard />
</ProtectedRoute>
}
/>
Vue 集成方案
1. Pinia 认证存储
// stores/auth.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import router from '../router';
export const useAuthStore = defineStore('auth', () => {
const user = ref(null);
const isLoading = ref(true);
const returnUrl = ref(null);
async function login(username, password) {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
if (!response.ok) throw new Error('登录失败');
const { token, user: userData } = await response.json();
user.value = userData;
localStorage.setItem('token', token);
router.push(returnUrl.value || '/');
}
async function logout() {
await fetch('/api/auth/logout');
user.value = null;
localStorage.removeItem('token');
router.push('/login');
}
async function checkAuth() {
try {
const token = localStorage.getItem('token');
if (token) {
const response = await fetch('/api/auth/me', {
headers: { 'Authorization': `Bearer ${token}` }
});
user.value = await response.json();
}
} catch (err) {
console.error('认证检查失败:', err);
} finally {
isLoading.value = false;
}
}
const isAuthenticated = computed(() => !!user.value);
return {
user,
isLoading,
returnUrl,
isAuthenticated,
login,
logout,
checkAuth
};
});
2. 路由守卫实现
// router.js
import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from './stores/auth';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue'),
meta: { requiresAuth: true }
},
{ path: '/login', component: () => import('./views/Login.vue') }
]
});
router.beforeEach(async (to, from) => {
const auth = useAuthStore();
if (to.meta.requiresAuth && !auth.isAuthenticated) {
auth.returnUrl = to.fullPath;
return '/login';
}
if (to.path === '/login' && auth.isAuthenticated) {
return from.path || '/';
}
if (!auth.isLoading && !auth.user) {
await auth.checkAuth();
if (to.meta.requiresAuth && !auth.isAuthenticated) {
return '/login';
}
}
});
export default router;
安全最佳实践
1. Token 存储策略对比
| 存储方式 | 安全性 | 可访问性 | 防XSS | 防CSRF | 实现复杂度 |
|---|---|---|---|---|---|
| localStorage | 中 | 高 | 弱 | 强 | 低 |
| sessionStorage | 中 | 高 | 弱 | 强 | 低 |
| HttpOnly Cookie | 高 | 低 | 强 | 弱 | 中 |
| 内存存储 | 高 | 低 | 强 | 强 | 高 |
2. 增强安全措施
// CSRF保护示例
async function fetchWithAuth(url, options = {}) {
const response = await fetch(url, {
...options,
credentials: 'include',
headers: {
...options.headers,
'Authorization': `Bearer ${authService.token}`,
'X-CSRF-Token': getCSRFToken()
}
});
if (response.status === 401) {
try {
await authService.refresh();
return fetchWithAuth(url, options);
} catch (err) {
authService.logout();
throw err;
}
}
return response;
}
// 双重提交Cookie模式
function getCSRFToken() {
const cookieValue = document.cookie
.split('; ')
.find(row => row.startsWith('XSRF-TOKEN='))
?.split('=')[1];
return cookieValue || '';
}
3. 敏感操作保护
// 关键操作需要重新验证密码
async function confirmPassword(password) {
const response = await fetch('/api/auth/confirm-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authService.token}`
},
body: JSON.stringify({ password })
});
if (!response.ok) {
throw new Error('密码验证失败');
}
return true;
}
// 使用示例
const handleDeleteAccount = async () => {
try {
await confirmPassword(currentPassword);
await deleteAccount();
} catch (err) {
showError(err.message);
}
};
性能优化
1. Token 自动刷新策略
class TokenRefresher {
constructor() {
this.refreshInProgress = false;
this.queue = [];
}
async refreshToken() {
if (this.refreshInProgress) {
return new Promise((resolve) => {
this.queue.push(resolve);
});
}
this.refreshInProgress = true;
try {
const newToken = await authService.refresh();
this.queue.forEach(resolve => resolve(newToken));
this.queue = [];
return newToken;
} catch (err) {
this.queue.forEach(resolve => resolve(Promise.reject(err)));
this.queue = [];
throw err;
} finally {
this.refreshInProgress = false;
}
}
}
2. 请求批处理
// 合并多个并发请求的401错误
const errorHandler = {
activeRefresh: null,
failedRequests: [],
async handleError(error) {
if (error.status !== 401) throw error;
if (!this.activeRefresh) {
this.activeRefresh = authService.refresh()
.finally(() => {
this.activeRefresh = null;
});
}
try {
await this.activeRefresh;
return retryFailedRequests();
} catch (refreshError) {
this.failedRequests = [];
throw refreshError;
}
}
};
常见问题解决方案
1. Token失效处理
// 响应拦截器示例
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
await authService.refresh();
originalRequest.headers.Authorization = `Bearer ${authService.token}`;
return axios(originalRequest);
} catch (refreshError) {
authService.logout();
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
2. 多标签页同步
// 监听storage事件实现多标签页状态同步
window.addEventListener('storage', (event) => {
if (event.key === 'token' && event.newValue !== authService.token) {
if (event.newValue) {
authService.setToken(event.newValue);
} else {
authService.logout();
}
}
});
// 在设置token时触发事件
function setToken(token) {
localStorage.setItem('token', token);
window.dispatchEvent(new Event('storage'));
}
3. 服务端渲染(SSR)处理
// Nuxt.js插件示例
export default defineNuxtPlugin(async (nuxtApp) => {
const auth = useAuthStore();
if (process.server) {
const token = parseTokenFromCookie(nuxtApp.ssrContext.req.headers.cookie);
if (token) {
await auth.fetchUser(token);
}
} else {
await auth.checkAuth();
}
});
// Next.js getServerSideProps
export async function getServerSideProps(context) {
const token = context.req.cookies.token;
let user = null;
if (token) {
user = await fetchUserServerSide(token);
}
return { props: { initialUser: user } };
}
通过以上实现,你可以构建一个安全、高效的JWT认证系统。记住要根据应用的具体需求选择适当的存储策略和安全措施。接下来我们将进入全栈电商平台的开发。
#前端开发
分享于 2025-03-25
上一篇:19.2 使用 React/Vue 前端框架
下一篇:20.1 前后端分离架构