赞
踩
因为http请求是无状态的,是一次性的,请求之间没有任何关系,服务端无法知道请求者的身份,所以需要鉴权,来验证当前用户是否有访问系统的权限。
以oauth2.0授权码模式为例:
每次请求资源服务器时都会在请求头中添加 Authorization: Bearer access_token 资源服务器会先判断token是否有效,如果无效或过期则响应 401 Unauthorize。此时用户处于操作状态,应该自动刷新token保证用户的行为正常进行。
刷新token:使用refresh_token获取新的access_token,使用新的access_token重新发起失败的请求。
当请求出现状态码为 401 时表明token失效或过期,拦截响应,刷新token,使用新的token重新发起该请求。
如果刷新token的过程中,还有其他的请求,则应该将其他请求也保存下来,等token刷新完成,按顺序重新发起所有请求。
- function httpFactory({ method, url, body, headers, readAs, timeout }) {
- const xhr = new XMLHttpRequest()
- xhr.open(method, url)
- xhr.timeout = isNumber(timeout) ? timeout : 1000 * 60
-
- if(headers){
- forEach(headers, (value, name) => value && xhr.setRequestHeader(name, value))
- }
-
- const HTTPPromise = new Promise((resolve, reject) => {
- xhr.onload = function () {
- let response;
-
- if (readAs === 'json') {
- try {
- response = JSONbig.parse(this.responseText || null);
- } catch {
- response = this.responseText || null;
- }
- } else if (readAs === 'xml') {
- response = this.responseXML
- } else {
- response = this.responseText
- }
-
- resolve({ status: xhr.status, response, getResponseHeader: (name) => xhr.getResponseHeader(name) })
- }
-
- xhr.onerror = function () {
- reject(xhr)
- }
- xhr.ontimeout = function () {
- reject({ ...xhr, isTimeout: true })
- }
-
- beforeSend(xhr)
-
- body ? xhr.send(body) : xhr.send()
-
- xhr.onreadystatechange = function () {
- if (xhr.status === 502) {
- reject(xhr)
- }
- }
- })
-
- // 允许HTTP请求中断
- HTTPPromise.abort = () => xhr.abort()
-
- return HTTPPromise;
- }
- // 是否正在刷新token的标记
- let isRefreshing = false
-
- // 存放因token过期而失败的请求
- let requests = []
-
- function httpRequest(config) {
- let abort
- let process = new Promise(async (resolve, reject) => {
- const request = httpFactory({...config, headers: { Authorization: 'Bearer ' + cookie.load('access_token'), ...configs.headers }})
- abort = request.abort
-
- try {
- const { status, response, getResponseHeader } = await request
-
- if(status === 401) {
- try {
- if (!isRefreshing) {
- isRefreshing = true
-
- // 刷新token
- await refreshToken()
-
- // 按顺序重新发起所有失败的请求
- const allRequests = [() => resolve(httpRequest(config)), ...requests]
- allRequests.forEach((cb) => cb())
- } else {
- // 正在刷新token,将请求暂存
- requests = [
- ...requests,
- () => resolve(httpRequest(config)),
- ]
- }
- } catch(err) {
- reject(err)
- } finally {
- isRefreshing = false
- requests = []
- }
- }
- } catch(ex) {
- reject(ex)
- }
- })
-
- process.abort = abort
- return process
- }
-
- // 发起请求
- httpRequest({ method: 'get', url: 'http://127.0.0.1:8000/api/v1/getlist' })
- // 是否正在刷新token的标记
- let isRefreshing = false
-
- let requests: ReadonlyArray<(config: any) => void> = []
-
- // 错误响应拦截
- axiosInstance.interceptors.response.use((res) => res, async (err) => {
- if (err.response && err.response.status === 401) {
- try {
- if (!isRefreshing) {
- isRefreshing = true
- // 刷新token
- const { access_token } = await refreshToken()
-
- if (access_token) {
- axiosInstance.defaults.headers.common.Authorization = `Bearer ${access_token}`;
-
- requests.forEach((cb) => cb(access_token))
- requests = []
-
- return axiosInstance.request({
- ...err.config,
- headers: {
- ...(err.config.headers || {}),
- Authorization: `Bearer ${access_token}`,
- },
- })
- }
-
- throw err
- }
-
- return new Promise((resolve) => {
- // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
- requests = [
- ...requests,
- (token) => resolve(axiosInstance.request({
- ...err.config,
- headers: {
- ...(err.config.headers || {}),
- Authorization: `Bearer ${token}`,
- },
- })),
- ]
- })
- } catch (e) {
- isRefreshing = false
- throw err
- } finally {
- if (!requests.length) {
- isRefreshing = false
- }
- }
- } else {
- throw err
- }
- })
当用户登录之后,长时间不操作应该做自动退出功能,提高用户数据的安全性。
操作事件:用户操作事件主要包含鼠标点击、移动、滚动事件和键盘事件等。
特殊事件:某些耗时的功能,比如上传、下载等。
用户在登录页面之后,可以复制成多个标签,在某一个标签有操作,其他标签也不应该自动退出。所以需要标签页之间共享操作信息。这里我们使用 localStorage 来实现跨标签页共享数据。
在 localStorage 存入两个字段:
名称 | 类型说明 | 说明 |
---|---|---|
lastActiveTime | string | 最后一次触发操作事件的时间戳 |
activeEvents | string[ ] | 特殊事件名称数组 |
当有操作事件时,将当前时间戳存入 lastActiveTime。
当有特殊事件时,将特殊事件名称存入 activeEvents ,等特殊事件结束后,将该事件移除。
设置定时器,每1分钟获取一次 localStorage 这两个字段,优先判断 activeEvents 是否为空,若不为空则更新 lastActiveTime 为当前时间,若为空,则使用当前时间减去 lastActiveTime 得到的值与规定值(假设为1h)做比较,大于 1h 则退出登录。
- const LastTimeKey = 'lastActiveTime'
- const activeEventsKey = 'activeEvents'
- const debounceWaitTime = 2 * 1000
- const IntervalTimeOut = 1 * 60 * 1000
-
- export const updateActivityStatus = debounce(() => {
- localStorage.set(LastTimeKey, new Date().getTime())
- }, debounceWaitTime)
-
- /**
- * 页面超时未有操作事件退出登录
- */
- export function timeout(keepTime = 60) {
- document.addEventListener('mousedown', updateActivityStatus)
- document.addEventListener('mouseover', updateActivityStatus)
- document.addEventListener('wheel', updateActivityStatus)
- document.addEventListener('keydown', updateActivityStatus)
-
- // 定时器
- let timer;
-
- const doTimeout = () => {
- timer && clearTimeout(timer)
- localStorage.remove(LastTimeKey)
- document.removeEventListener('mousedown', updateActivityStatus)
- document.removeEventListener('mouseover', updateActivityStatus)
- document.removeEventListener('wheel', updateActivityStatus)
- document.removeEventListener('keydown', updateActivityStatus)
-
- // 注销token,清空session,回到登录页
- logout()
- }
-
- /**
- * 重置定时器
- */
- function resetTimer() {
- localStorage.set(LastTimeKey, new Date().getTime())
-
- if (timer) {
- clearInterval(timer)
- }
-
- timer = setInterval(() => {
- const isSignin = document.cookie.includes('access_token')
- if (!isSignin) {
- doTimeout()
- return
- }
-
- const activeEvents = localStorage.get(activeEventsKey)
- if(!isEmpty(activeEvents)) {
- localStorage.set(LastTimeKey, new Date().getTime())
- return
- }
-
- const lastTime = Number(localStorage.get(LastTimeKey))
-
- if (!lastTime || Number.isNaN(lastTime)) {
- localStorage.set(LastTimeKey, new Date().getTime())
- return
- }
-
- const now = new Date().getTime()
- const time = now - lastTime
-
- if (time >= keepTime) {
- doTimeout()
- }
- }, IntervalTimeOut)
- }
-
- resetTimer()
- }
-
- // 上传操作
- function upload() {
- const current = JSON.parse(localStorage.get(activeEventsKey))
- localStorage.set(activeEventsKey, [...current, 'upload'])
- ...
- // do upload request
- ...
- const current = JSON.parse(localStorage.get(activeEventsKey))
- localStorage.set(activeEventsKey, Array.isArray(current) ? current.filter((item) => itme !== 'upload'))
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。