javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > 前端无感刷新

前端实现无感刷新的详细方案

作者:北辰alk

无感刷新(Silent Refresh)是指在用户无感知的情况下,通过技术手段自动更新身份凭证(如Token),维持用户登录状态的技术方案,本文给大家介绍了前端实现无感刷新的详细方案,需要的朋友可以参考下

一、什么是无感刷新?

1.1 核心概念

无感刷新(Silent Refresh)是指在用户无感知的情况下,通过技术手段自动更新身份凭证(如Token),维持用户登录状态的技术方案。主要解决以下痛点:

1.2 典型应用场景

场景说明
JWT认证Access Token过期自动刷新
OAuth2.0使用Refresh Token获取新凭证
敏感操作维持长时间操作不中断

二、实现原理与方案对比

2.1 技术方案对比

方案优点缺点适用场景
定时检测实现简单时间误差大短期会话
请求拦截精确控制需要全局处理常规Web应用
Web Worker不阻塞主线程复杂度高大型应用
Service Worker离线可用需要HTTPSPWA应用

2.2 核心实现流程

三、基础版实现(Axios拦截器方案)

3.1 创建Axios实例

// src/utils/request.js
import axios from 'axios'

const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 10000
})

3.2 添加请求拦截器

// 请求拦截器
service.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('access_token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

3.3 响应拦截器处理逻辑

// 响应拦截器
let isRefreshing = false
let requests = []

service.interceptors.response.use(
  (response) => {
    return response.data
  },
  async (error) => {
    const { config, response } = error
    
    // Token过期处理
    if (response.status === 401 && !config._retry) {
      
      // 存储待重试请求
      if (!isRefreshing) {
        isRefreshing = true
        
        try {
          // 刷新Token
          const newToken = await refreshToken()
          
          // 存储新Token
          localStorage.setItem('access_token', newToken)
          
          // 重试队列
          requests.forEach(cb => cb(newToken))
          requests = []
          
          // 重试原请求
          config.headers.Authorization = `Bearer ${newToken}`
          return service(config)
        } catch (refreshError) {
          // 刷新失败处理
          localStorage.clear()
          window.location.href = '/login'
          return Promise.reject(refreshError)
        } finally {
          isRefreshing = false
        }
      }
      
      // 将未完成的请求加入队列
      return new Promise((resolve) => {
        requests.push((token) => {
          config.headers.Authorization = `Bearer ${token}`
          resolve(service(config))
        })
      })
    }
    
    return Promise.reject(error)
  }
)

3.4 Token刷新函数

async function refreshToken() {
  const refreshToken = localStorage.getItem('refresh_token')
  if (!refreshToken) {
    throw new Error('缺少刷新令牌')
  }
  
  try {
    const { data } = await axios.post('/api/auth/refresh', {
      refresh_token: refreshToken
    })
    
    return data.access_token
  } catch (error) {
    throw new Error('令牌刷新失败')
  }
}

四、进阶优化方案

4.1 并发请求控制

class TokenRefreshManager {
  constructor() {
    this.subscribers = []
    this.isRefreshing = false
  }

  subscribe(callback) {
    this.subscribers.push(callback)
  }

  onRefreshed(token) {
    this.subscribers.forEach(callback => callback(token))
    this.subscribers = []
  }

  async refresh() {
    if (this.isRefreshing) {
      return new Promise(resolve => {
        this.subscribe(resolve)
      })
    }
    
    this.isRefreshing = true
    try {
      const newToken = await refreshToken()
      this.onRefreshed(newToken)
      return newToken
    } finally {
      this.isRefreshing = false
    }
  }
}

export const tokenManager = new TokenRefreshManager()

4.2 定时检测策略

// Token有效期检测
function setupTokenCheck() {
  const checkInterval = setInterval(() => {
    const token = localStorage.getItem('access_token')
    if (token && isTokenExpired(token)) {
      tokenManager.refresh().catch(() => {
        clearInterval(checkInterval)
      })
    }
  }, 60 * 1000) // 每分钟检查一次
}

// JWT解码示例
function isTokenExpired(token) {
  const payload = JSON.parse(atob(token.split('.')[1]))
  const exp = payload.exp * 1000
  const now = Date.now()
  return now > exp - 5 * 60 * 1000 // 提前5分钟刷新
}

4.3 Web Worker实现

// worker.js
self.addEventListener('message', async (e) => {
  if (e.data.type === 'refreshToken') {
    try {
      const response = await fetch('/api/refresh', {
        method: 'POST',
        body: JSON.stringify({
          refresh_token: e.data.refreshToken
        })
      })
      const data = await response.json()
      self.postMessage({ success: true, token: data.access_token })
    } catch (error) {
      self.postMessage({ success: false, error })
    }
  }
})

// 主线程调用
const worker = new Worker('./worker.js')

function refreshWithWorker() {
  return new Promise((resolve, reject) => {
    worker.postMessage({
      type: 'refreshToken',
      refreshToken: localStorage.getItem('refresh_token')
    })
    
    worker.onmessage = (e) => {
      if (e.data.success) {
        resolve(e.data.token)
      } else {
        reject(e.data.error)
      }
    }
  })
}

五、安全增强措施

5.1 安全存储方案

// 安全存储类
class SecureStorage {
  private encryptionKey: string
  
  constructor(key: string) {
    this.encryptionKey = key
  }

  setItem(key: string, value: string) {
    const encrypted = CryptoJS.AES.encrypt(value, this.encryptionKey)
    localStorage.setItem(key, encrypted.toString())
  }

  getItem(key: string) {
    const encrypted = localStorage.getItem(key)
    if (!encrypted) return null
    
    return CryptoJS.AES.decrypt(encrypted, this.encryptionKey)
      .toString(CryptoJS.enc.Utf8)
  }
}

// 初始化实例
const storage = new SecureStorage('your-secret-key')
storage.setItem('refresh_token', 'your-refresh-token')

5.2 双Token校验流程

5.3 防御措施

// 防止CSRF攻击示例
function addCsrfProtection(config) {
  const csrfToken = getCsrfToken() // 从Cookie获取
  if (csrfToken) {
    config.headers['X-CSRF-TOKEN'] = csrfToken
  }
  return config
}

// 速率限制
let refreshCount = 0
setInterval(() => {
  refreshCount = Math.max(0, refreshCount - 2)
}, 60 * 1000)

async function safeRefresh() {
  if (refreshCount > 5) {
    throw new Error('刷新过于频繁')
  }
  refreshCount++
  return refreshToken()
}

六、多框架适配实现

6.1 Vue3 Composition API实现

<script setup>
import { ref } from 'vue'
import { useAxios } from '@vueuse/integrations/useAxios'

const { execute } = useAxios(
  '/api/data',
  { method: 'GET' },
  {
    immediate: false,
    onError: async (error) => {
      if (error.response?.status === 401) {
        await refreshToken()
        execute() // 自动重试
      }
    }
  }
)
</script>

6.2 React Hooks实现

import { useEffect } from 'react'
import axios from 'axios'

function useSilentRefresh() {
  useEffect(() => {
    const interceptor = axios.interceptors.response.use(
      response => response,
      async error => {
        if (error.response.status === 401) {
          await refreshToken()
          return axios.request(error.config)
        }
        return Promise.reject(error)
      }
    )
    
    return () => {
      axios.interceptors.response.eject(interceptor)
    }
  }, [])
}

6.3 Angular拦截器实现

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private auth: AuthService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    return next.handle(req).pipe(
      catchError(error => {
        if (error.status === 401) {
          return this.auth.refresh().pipe(
            switchMap(() => {
              const authReq = req.clone({
                setHeaders: { Authorization: `Bearer ${this.auth.token}` }
              })
              return next.handle(authReq)
            })
          )
        }
        return throwError(error)
      })
    )
  }
}

七、性能优化方案

7.1 请求队列管理

class RequestQueue {
  constructor() {
    this.queue = []
    this.isProcessing = false
  }

  add(request) {
    return new Promise((resolve, reject) => {
      this.queue.push({ request, resolve, reject })
      if (!this.isProcessing) this.process()
    })
  }

  async process() {
    this.isProcessing = true
    while (this.queue.length) {
      const { request, resolve, reject } = this.queue.shift()
      try {
        const response = await request()
        resolve(response)
      } catch (error) {
        reject(error)
      }
    }
    this.isProcessing = false
  }
}

7.2 内存缓存优化

const tokenCache = {
  accessToken: null,
  refreshToken: null,
  expiresAt: 0,

  get access() {
    if (Date.now() < this.expiresAt) {
      return this.accessToken
    }
    return null
  },

  async refresh() {
    const { access_token, expires_in } = await refreshToken()
    this.accessToken = access_token
    this.expiresAt = Date.now() + expires_in * 1000
    return access_token
  }
}

7.3 指数退避重试

async function retryWithBackoff(fn, retries = 3, delay = 1000) {
  try {
    return await fn()
  } catch (error) {
    if (retries <= 0) throw error
    await new Promise(resolve => setTimeout(resolve, delay))
    return retryWithBackoff(fn, retries - 1, delay * 2)
  }
}

八、生产环境注意事项

8.1 安全规范

  1. HTTPS必须启用:防止中间人攻击
  2. 设置合理有效期
    • Access Token:15-30分钟
    • Refresh Token:7-30天 
  3. 权限分离:Refresh Token仅用于获取新Access Token

8.2 监控指标

指标监控方式报警阈值
刷新成功率日志统计<95%
并发请求数性能监控>100/秒
Token泄露次数安全扫描>0次

8.3 灾备方案

九、完整实现流程图

十、常见问题解答

Q1:如何防止Refresh Token被盗用?

Q2:移动端实现有何不同?

Q3:如何处理多标签页场景?

// 使用BroadcastChannel同步状态
const channel = new BroadcastChannel('auth')

channel.addEventListener('message', (event) => {
  if (event.data.type === 'token_refreshed') {
    localStorage.setItem('access_token', event.data.token)
  }
})

function broadcastNewToken(token) {
  channel.postMessage({ type: 'token_refreshed', token })
}

十一、总结与展望

11.1 技术总结

11.2 未来趋势

  1. 无密码认证:WebAuthn标准普及
  2. 零信任架构:持续身份验证
  3. 区块链身份:去中心化认证

以上就是前端实现无感刷新的详细方案的详细内容,更多关于前端无感刷新的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文