登录实现
前端登录的常见方案与实现
问题
实现一个完整的前端登录功能,包括登录流程、状态管理和权限控制。
解答
常见登录方案
1. Cookie + Session
┌─────────┐ ┌─────────┐
│ 浏览器 │ │ 服务器 │
└────┬────┘ └────┬────┘
│ 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)避免频繁登录
- 路由守卫:前端权限控制只是体验优化,真正的权限校验在后端
目录