登录实现

前端登录的常见方案与实现

问题

实现一个完整的前端登录功能,包括登录流程、状态管理和权限控制。

解答

常见登录方案

┌─────────┐                    ┌─────────┐
│  浏览器  │                    │  服务器  │
└────┬────┘                    └────┬────┘
     │  1. 发送用户名/密码          │
     │ ──────────────────────────> │
     │                             │ 2. 验证,创建 Session
     │  3. Set-Cookie: sessionId   │
     │ <────────────────────────── │
     │                             │
     │  4. 请求携带 Cookie          │
     │ ──────────────────────────> │
     │                             │ 5. 根据 sessionId 查找用户

2. JWT Token

┌─────────┐                    ┌─────────┐
│  浏览器  │                    │  服务器  │
└────┬────┘                    └────┬────┘
     │  1. 发送用户名/密码          │
     │ ──────────────────────────> │
     │                             │ 2. 验证,生成 JWT
     │  3. 返回 token              │
     │ <────────────────────────── │
     │                             │
     │  4. Authorization: Bearer token
     │ ──────────────────────────> │
     │                             │ 5. 验证 token 签名

前端实现

登录表单

// login.js
async function login(username, password) {
  try {
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password })
    })
    
    if (!response.ok) {
      throw new Error('登录失败')
    }
    
    const { token, user } = await response.json()
    
    // 存储 token
    localStorage.setItem('token', token)
    // 存储用户信息
    localStorage.setItem('user', JSON.stringify(user))
    
    return user
  } catch (error) {
    console.error(error)
    throw error
  }
}

请求拦截器(Axios)

// request.js
import axios from 'axios'

const instance = axios.create({
  baseURL: '/api',
  timeout: 10000
})

// 请求拦截:添加 token
instance.interceptors.request.use(config => {
  const token = localStorage.getItem('token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

// 响应拦截:处理 token 过期
instance.interceptors.response.use(
  response => response.data,
  error => {
    if (error.response?.status === 401) {
      // token 过期,清除登录状态
      localStorage.removeItem('token')
      localStorage.removeItem('user')
      // 跳转登录页
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)

export default instance

路由守卫(Vue Router)

// router.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/login', component: Login, meta: { public: true } },
    { path: '/dashboard', component: Dashboard },
    { path: '/profile', component: Profile }
  ]
})

// 全局前置守卫
router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('token')
  
  // 公开页面直接放行
  if (to.meta.public) {
    return next()
  }
  
  // 未登录跳转登录页
  if (!token) {
    return next({ path: '/login', query: { redirect: to.fullPath } })
  }
  
  next()
})

export default router

Token 刷新

// auth.js
let isRefreshing = false
let refreshSubscribers = []

// 添加等待刷新的请求
function subscribeTokenRefresh(callback) {
  refreshSubscribers.push(callback)
}

// 刷新完成后执行所有等待的请求
function onTokenRefreshed(newToken) {
  refreshSubscribers.forEach(callback => callback(newToken))
  refreshSubscribers = []
}

async function refreshToken() {
  const refreshToken = localStorage.getItem('refreshToken')
  const response = await fetch('/api/refresh', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ refreshToken })
  })
  const { token } = await response.json()
  localStorage.setItem('token', token)
  return token
}

// 在响应拦截器中使用
instance.interceptors.response.use(
  response => response.data,
  async error => {
    const originalRequest = error.config
    
    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        // 正在刷新,等待新 token
        return new Promise(resolve => {
          subscribeTokenRefresh(token => {
            originalRequest.headers.Authorization = `Bearer ${token}`
            resolve(instance(originalRequest))
          })
        })
      }
      
      originalRequest._retry = true
      isRefreshing = true
      
      try {
        const newToken = await refreshToken()
        onTokenRefreshed(newToken)
        originalRequest.headers.Authorization = `Bearer ${newToken}`
        return instance(originalRequest)
      } catch (err) {
        // 刷新失败,退出登录
        localStorage.clear()
        window.location.href = '/login'
      } finally {
        isRefreshing = false
      }
    }
    
    return Promise.reject(error)
  }
)

Token 存储位置对比

存储方式XSS 风险CSRF 风险适用场景
localStorage一般应用
sessionStorage单标签页应用
Cookie (httpOnly)安全要求高
内存高安全要求

关键点

  • Cookie + Session:服务端存储状态,适合传统应用,有 CSRF 风险
  • JWT:无状态,适合分布式系统,注意 token 泄露风险
  • 存储选择:localStorage 方便但有 XSS 风险,httpOnly Cookie 更安全
  • Token 刷新:使用双 token 机制(access + refresh)避免频繁登录
  • 路由守卫:前端权限控制只是体验优化,真正的权限校验在后端