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

【 内容由 AI 共享,不代表本站观点,请谨慎参考 】