предисловие
Когда я недавно работал над проектом, он включал единый вход, то есть страницу входа в проект с использованием страницы входа, совместно используемой компанией, и унифицированную логику обработки на этой странице. Наконец, пользователям нужно войти в систему только один раз, и они могут получить доступ ко всем веб-сайтам компании в состоянии входа в систему.
❝Единый вход (Single Sign On, сокращенно SSO) — одно из наиболее популярных решений для бизнес-интеграции предприятия.Он используется между несколькими прикладными системами.Пользователям нужно войти в систему только один раз, чтобы получить доступ ко всем взаимно доверенным прикладным системам.
❞
Эта статья о том, как управлять после входа в системуaccess_token
а такжеrefresh_token
, в основном для инкапсуляции перехватчика axios, который здесь записан.
нужно
- Предварительный сценарий
-
Перейти на страницу проекта
http://xxxx.project.com/profile
Если вам нужно войти в систему, перейдите на платформу входа SSO, не входя в систему. В настоящее время URL-адрес URL входаhttp://xxxxx.com/login?app_id=project_name_id&redirect_url=http://xxxx.project.com/profile
,вapp_id
Это определяется соглашением на заднем плане.redirect_url
адрес обратного вызова, указанный после успешной авторизации. -
После ввода пароля учетной записи и правильного, он будет перенаправлен обратно на страницу, которую вы только что ввели, с параметром в адресной строке.
?code=XXXXX
, то естьhttp://xxxx.project.com/profile?code=XXXXXX
, значение кода недействительно после одного использования и истекает в течение 10 минут -
Немедленно получите это кодовое значение, а затем запросите API
/access_token/authenticate
, несущие параметры{ verify_code: code }
, и API уже поставляется сapp_id
а такжеapp_secret
Два параметра с фиксированным значением, через которые можно запросить авторизованный API и получить возвращаемое значение после успешного запроса.{ access_token: "xxxxxxx", refresh_token: "xxxxxxxx", expires_in: xxxxxxxx }
, спастиaccess_token
а такжеrefresh_token
в файл cookie (localStorage также может), и в это время пользователь успешно вошел в систему. -
access_token
Он в стандартном формате JWT и является токеном авторизации. Можно понять, что он проверяет личность пользователя. Это параметр, который приложение должно передать при вызове API для доступа и изменения пользовательских данных (в заголовках файла заголовок запроса), и срок его действия истекает через 2 часа. То есть после выполнения первых трех шагов вы можете вызывать API, которые требуют от пользователей входа в систему для их использования; но если вы ничего не сделаете и запросите эти API после двух часов молчания, вы получите сообщениеaccess_token
истек, вызов не удался. -
Тогда вы не можете позволить пользователю выйти из системы через 2 часа Решение состоит в том, чтобы взять просроченный через два часа.
access_token
а такжеrefresh_token
(refresh_token
Срок действия обычно больше, например, один месяц или более), чтобы запросить/refresh
API, возвращаемый результат{ access_token: "xxxxx", expires_in: xxxxx }
, в обмен на новыйaccess_token
,новыйaccess_token
Время истечения тоже 2 часа, и оно повторно сохраняется в куке, а цикл продолжает авторизоваться и вызывать API пользователя.refresh_token
В течение ограниченного времени истечения (например, недели или месяца и т. д.) вы можете продолжить обмен на новый в следующий раз.access_token
, но по истечении ограниченного времени, даже если реальный смысл истечет, необходимо повторно ввести пароль учетной записи для входа в систему.
Время истечения логина на сайте компании всего два часа (время истечения токена), но мы хотим, чтобы часто активные пользователи в течение месяца не заходили повторно, поэтому есть такая необходимость запретить пользователям вводить свой аккаунт и пароль еще раз, чтобы войти.
Зачем использовать спец.refresh_token
обновитьaccess_token
Шерстяная ткань? первыйaccess_token
Это будет связано с определенными правами пользователя.Если авторизация пользователя изменена, этоaccess_token
Его также необходимо обновить, чтобы связать новые разрешения, если нет.refresh_token
, вы также можете обновитьaccess_token
, но каждое обновление требует от пользователя ввода имени пользователя и пароля для входа в систему, что очень проблематично. имеютrefresh_ token
, может уменьшить эту проблему, клиент напрямую используетrefresh_token
обновитьaccess_token
, никаких дополнительных действий со стороны пользователя не требуется.
Сказав так много, некоторые люди могут пожаловаться, используется логинaccess_token
Ладно, добавимrefresh_token
сделать это таким хлопотным, или некоторые компанииrefresh_token
Он устроен в фоновом режиме и не требует предварительной обработки. Однако предварительный сценарий есть, и требования основаны на этом сценарии.
- нужно
-
когда
access_token
Когда он истечет, используйтеrefresh_token
запросить новыйaccess_token
, интерфейс должен быть обновлен без восприятия пользователемaccess_token
. Например, когда пользователь инициирует запрос, если решениеaccess_token
Срок действия истек, то вы должны сначала вызвать интерфейс токена обновления, чтобы получить новыйaccess_token
, а затем повторно инициируйте запрос пользователя. -
Если несколько пользовательских запросов инициированы одновременно, первый пользователь запрашивает вызов интерфейса токена обновления. Если интерфейс не вернулся, остальные запросы пользователя по-прежнему инициируют запрос интерфейса токена обновления, что приведет к множественным запросам. Как справиться с этими запросами, содержание нашей статьи.
идеи
Вариант первый
Прописано в перехватчике запроса, перед запросом использовать поля, возвращаемые исходным запросомexpires_in
поле для сужденияaccess_token
Независимо от того, истек ли срок его действия, если он истек, запрос будет приостановлен и сначала обновленaccess_token
Затем продолжайте запрашивать.
- Преимущества: можно сохранять http-запросы
- Недостатки: поскольку используется оценка местного времени, если местное время изменено, существует риск сбоя проверки.
Вариант 2
Написано в ответ на перехватчики, перехватить возвращенные данные. Сначала инициируйте запрос пользователя, если интерфейс возвращаетaccess_token
просрочено, сначала обновитеaccess_token
, и попробуй еще раз.
- Плюсы: не нужно оценивать время
- Недостаток: потребуется еще один http-запрос.
Здесь я выбираю второй вариант.
выполнить
Здесь используется Axios, который должен перехватывать после запроса, поэтому используется перехватчик ответа axios.axios.interceptors.response.use()
метод
Введение метода
- @utils/auth.js
import Cookies from 'js-cookie'
const TOKEN_KEY = 'access_token'
const REGRESH_TOKEN_KEY = 'refresh_token'
export const getToken = () => Cookies.get(TOKEN_KEY)
export const setToken = (token, params = {}) => {
Cookies.set(TOKEN_KEY, token, params)
}
export const setRefreshToken = (token) => {
Cookies.set(REGRESH_TOKEN_KEY, token)
}
- request.js
import axios from 'axios'
import { getToken, setToken, getRefreshToken } from '@utils/auth'
// 刷新 access_token 的接口
const refreshToken = () => {
return instance.post('/auth/refresh', { refresh_token: getRefreshToken() }, true)
}
// 创建 axios 实例
const instance = axios.create({
baseURL: process.env.GATSBY_API_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
}
})
instance.interceptors.response.use(response => {
return response
}, error => {
if (!error.response) {
return Promise.reject(error)
}
// token 过期或无效,返回 401 状态码,在此处理逻辑
return Promise.reject(error)
})
// 给请求头添加 access_token
const setHeaderToken = (isNeedToken) => {
const accessToken = isNeedToken ? getToken() : null
if (isNeedToken) { // api 请求需要携带 access_token
if (!accessToken) {
console.log('不存在 access_token 则跳转回登录页')
}
instance.defaults.headers.common.Authorization = `Bearer ${accessToken}`
}
}
// 有些 api 并不需要用户授权使用,则不携带 access_token;默认不携带,需要传则设置第三个参数为 true
export const get = (url, params = {}, isNeedToken = false) => {
setHeaderToken(isNeedToken)
return instance({
method: 'get',
url,
params,
})
}
export const post = (url, params = {}, isNeedToken = false) => {
setHeaderToken(isNeedToken)
return instance({
method: 'post',
url,
data: params,
})
}
Затем преобразуйте перехватчик ответов axios в request.js.
instance.interceptors.response.use(response => {
return response
}, error => {
if (!error.response) {
return Promise.reject(error)
}
if (error.response.status === 401) {
const { config } = error
return refreshToken().then(res=> {
const { access_token } = res.data
setToken(access_token)
config.headers.Authorization = `Bearer ${access_token}`
return instance(config)
}).catch(err => {
console.log('抱歉,您的登录状态已失效,请重新登录!')
return Promise.reject(err)
})
}
return Promise.reject(error)
})
Согласовано возвращать код состояния 401, чтобы указатьaccess_token
Просрочено или недействительно, если пользователь инициирует запрос и возвращает результатaccess_token
истек, запросите обновлениеaccess_token
Интерфейс. Введите, если запрос выполнен успешноthen
Внутри сбросьте конфигурацию и обновитеaccess_token
и повторите исходный запрос.
но еслиrefresh_token
Также просрочен, запрос также возвращает 401. На этом этапе отладка обнаружит, что функция не может быть введенаrefreshToken()
изcatch
внутри, потому чтоrefreshToken()
Этот же метод используется и внутри методаinstance
Пример, повторение логики обработки перехватчика ответа 401, но сама функция является обновлениемaccess_token
, поэтому интерфейс нужно исключить, а именно:
if (error.response.status === 401 && !error.config.url.includes('/auth/refresh')) {}
Приведенный выше код уже достиг бессмысленного обновленияaccess_token
, когдаaccess_token
Если он не истекает, он вернется в обычном режиме; когда он истечет, axios обновит токен один раз, а затем повторно инициирует исходный запрос.
оптимизация
Запретить несколько токенов обновления
Если токен истек, запросите обновлениеaccess_token
Также существует определенный интервал времени для возврата интерфейса, если в это время будут отправлены другие запросы, то обновление будет выполнено снова.access_token
интерфейс, это приведет к многократному обновлениюaccess_token
. Поэтому нам нужно принять решение и определить метку, чтобы определить, обновляется ли она в данный момент.access_token
состояние, если оно находится в состоянии обновления, другим запросам больше не разрешается вызывать этот интерфейс.
let isRefreshing = false // 标记是否正在刷新 token
instance.interceptors.response.use(response => {
return response
}, error => {
if (!error.response) {
return Promise.reject(error)
}
if (error.response.status === 401 && !error.config.url.includes('/auth/refresh')) {
const { config } = error
if (!isRefreshing) {
isRefreshing = true
return refreshToken().then(res=> {
const { access_token } = res.data
setToken(access_token)
config.headers.Authorization = `Bearer ${access_token}`
return instance(config)
}).catch(err => {
console.log('抱歉,您的登录状态已失效,请重新登录!')
return Promise.reject(err)
}).finally(() => {
isRefreshing = false
})
}
}
return Promise.reject(error)
})
Обработка нескольких запросов одновременно
Вышеупомянутого подхода недостаточно, потому что, если несколько запросов инициируются одновременно, по истечении срока действия токена первый запрос входит в метод обновления токена, затем другие запросы входят без какой-либо логической обработки, просто возвращают ошибку и, наконец, только выполняется первый запрос. , что явно необоснованно.
Например, если три запроса инициированы одновременно, первый запрос входит в процесс обновления токена, второй и третий запросы необходимо сохранить, а запрос будет перевыпущен после обновления токена.
Здесь мы определяем массивrequests
, используемый для сохранения ожидающего запроса, а затем возвратаPromise
, пока вы не позвонитеresolve
метод, запрос будет в состоянии ожидания, и вы можете знать, что массив фактически хранит функцию; при обновлении токена функция выполняется через цикл массива, то есть выполняется разрешение по одному на повторную отправку запрос.
let isRefreshing = false // 标记是否正在刷新 token
let requests = [] // 存储待重发请求的数组
instance.interceptors.response.use(response => {
return response
}, error => {
if (!error.response) {
return Promise.reject(error)
}
if (error.response.status === 401 && !error.config.url.includes('/auth/refresh')) {
const { config } = error
if (!isRefreshing) {
isRefreshing = true
return refreshToken().then(res=> {
const { access_token } = res.data
setToken(access_token)
config.headers.Authorization = `Bearer ${access_token}`
// token 刷新后将数组的方法重新执行
requests.forEach((cb) => cb(access_token))
requests = [] // 重新请求完清空
return instance(config)
}).catch(err => {
console.log('抱歉,您的登录状态已失效,请重新登录!')
return Promise.reject(err)
}).finally(() => {
isRefreshing = false
})
} else {
// 返回未执行 resolve 的 Promise
return new Promise(resolve => {
// 用函数形式将 resolve 存入,等待刷新后再执行
requests.push(token => {
config.headers.Authorization = `Bearer ${token}`
resolve(instance(config))
})
})
}
}
return Promise.reject(error)
})
Окончательный код request.js
import axios from 'axios'
import { getToken, setToken, getRefreshToken } from '@utils/auth'
// 刷新 access_token 的接口
const refreshToken = () => {
return instance.post('/auth/refresh', { refresh_token: getRefreshToken() }, true)
}
// 创建 axios 实例
const instance = axios.create({
baseURL: process.env.GATSBY_API_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
}
})
let isRefreshing = false // 标记是否正在刷新 token
let requests = [] // 存储待重发请求的数组
instance.interceptors.response.use(response => {
return response
}, error => {
if (!error.response) {
return Promise.reject(error)
}
if (error.response.status === 401 && !error.config.url.includes('/auth/refresh')) {
const { config } = error
if (!isRefreshing) {
isRefreshing = true
return refreshToken().then(res=> {
const { access_token } = res.data
setToken(access_token)
config.headers.Authorization = `Bearer ${access_token}`
// token 刷新后将数组的方法重新执行
requests.forEach((cb) => cb(access_token))
requests = [] // 重新请求完清空
return instance(config)
}).catch(err => {
console.log('抱歉,您的登录状态已失效,请重新登录!')
return Promise.reject(err)
}).finally(() => {
isRefreshing = false
})
} else {
// 返回未执行 resolve 的 Promise
return new Promise(resolve => {
// 用函数形式将 resolve 存入,等待刷新后再执行
requests.push(token => {
config.headers.Authorization = `Bearer ${token}`
resolve(instance(config))
})
})
}
}
return Promise.reject(error)
})
// 给请求头添加 access_token
const setHeaderToken = (isNeedToken) => {
const accessToken = isNeedToken ? getToken() : null
if (isNeedToken) { // api 请求需要携带 access_token
if (!accessToken) {
console.log('不存在 access_token 则跳转回登录页')
}
instance.defaults.headers.common.Authorization = `Bearer ${accessToken}`
}
}
// 有些 api 并不需要用户授权使用,则无需携带 access_token;默认不携带,需要传则设置第三个参数为 true
export const get = (url, params = {}, isNeedToken = false) => {
setHeaderToken(isNeedToken)
return instance({
method: 'get',
url,
params,
})
}
export const post = (url, params = {}, isNeedToken = false) => {
setHeaderToken(isNeedToken)
return instance({
method: 'post',
url,
data: params,
})
}
Справочная статья:
- пс:Личный технический блог, репозиторий Github, если вы считаете, что это хорошо, добро пожаловать в звездочку, поддержите меня, чтобы я продолжал писать~