前端自动刷新Token与超时安全退出攻略

开源商城源码 2024-07-08 12:03:01 阅读 80

一、token的作用

因为http请求是无状态的,是一次性的,请求之间没有任何关系,服务端无法知道请求者的身份,所以需要鉴权,来验证当前用户是否有访问系统的权限。

以oauth2.0授权码模式为例:

89e4a202403071133361293.png

每次请求资源服务器时都会在请求头中添加 Authorization: Bearer access_token 资源服务器会先判断token是否有效,如果无效或过期则响应 401 Unauthorize。此时用户处于操作状态,应该自动刷新token保证用户的行为正常进行。

刷新token:使用refresh_token获取新的access_token,使用新的access_token重新发起失败的请求。

二、无感知刷新token方案

2.1 刷新方案

当请求出现状态码为 401 时表明token失效或过期,拦截响应,刷新token,使用新的token重新发起该请求。

如果刷新token的过程中,还有其他的请求,则应该将其他请求也保存下来,等token刷新完成,按顺序重新发起所有请求。

2.2 原生AJAX请求

2.2.1 http工厂函数

<code>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;

}

2.2.2 无感知刷新token

// 是否正在刷新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' })

2.3 Axios 无感知刷新token

// 是否正在刷新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

}

})

三、长时间无操作超时自动退出

当用户登录之后,长时间不操作应该做自动退出功能,提高用户数据的安全性。

3.1 操作事件

操作事件:用户操作事件主要包含鼠标点击、移动、滚动事件和键盘事件等。

特殊事件:某些耗时的功能,比如上传、下载等。

3.2 方案

用户在登录页面之后,可以复制成多个标签,在某一个标签有操作,其他标签也不应该自动退出。所以需要标签页之间共享操作信息。这里我们使用 localStorage 来实现跨标签页共享数据。

在 localStorage 存入两个字段:

名称 类型说明 说明
lastActiveTime string 最后一次触发操作事件的时间戳
activeEvents string[ ] 特殊事件名称数组

当有操作事件时,将当前时间戳存入 lastActiveTime。

当有特殊事件时,将特殊事件名称存入 activeEvents ,等特殊事件结束后,将该事件移除。

设置定时器,每1分钟获取一次 localStorage 这两个字段,优先判断 activeEvents 是否为空,若不为空则更新 lastActiveTime 为当前时间,若为空,则使用当前时间减去 lastActiveTime 得到的值与规定值(假设为1h)做比较,大于 1h 则退出登录。

3.3 代码实现

<code>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'))

}



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。