React native Refresh 有效,但下一次调用仍使用最后一个标记
React native Refresh works but next call still uses the last token
我使用以下中间件在令牌过期时刷新我的令牌:
import {AsyncStorage} from 'react-native';
import moment from 'moment';
import fetch from "../components/Fetch";
import jwt_decode from 'jwt-decode';
/**
* This middleware is meant to be the refresher of the authentication token, on each request to the API,
* it will first call refresh token endpoint
* @returns {function(*=): Function}
* @param store
*/
const tokenMiddleware = store => next => async action => {
if (typeof action === 'object' && action.type !== "FETCHING_TEMPLATES_FAILED") {
let eToken = await AsyncStorage.getItem('eToken');
if (isExpired(eToken)) {
let rToken = await AsyncStorage.getItem('rToken');
let formData = new FormData();
formData.append("refresh_token", rToken);
await fetch('/token/refresh',
{
method: 'POST',
body: formData
})
.then(response => response.json())
.then(async (data) => {
let decoded = jwt_decode(data.token);
console.log({"refreshed": data.token});
return await Promise.all([
await AsyncStorage.setItem('token', data.token).then(() => {return AsyncStorage.getItem('token')}),
await AsyncStorage.setItem('rToken', data.refresh_token).then(() => {return AsyncStorage.getItem('rToken')}),
await AsyncStorage.setItem('eToken', decoded.exp.toString()).then(() => {return AsyncStorage.getItem('eToken')}),
]).then((values) => {
return next(action);
});
}).catch((err) => {
console.log(err);
});
return next(action);
} else {
return next(action);
}
}
function isExpired(expiresIn) {
// We refresh the token 3.5 hours before it expires(12600 seconds) (lifetime on server 25200seconds)
return moment.unix(expiresIn).diff(moment(), 'seconds') < 10;
}
};
export default tokenMiddleware;
获取助手:
import { AsyncStorage } from 'react-native';
import GLOBALS from '../constants/Globals';
import {toast} from "./Toast";
import I18n from "../i18n/i18n";
const jsonLdMimeType = 'application/ld+json';
export default async function (url, options = {}, noApi = false) {
if ('undefined' === typeof options.headers) options.headers = new Headers();
if (null === options.headers.get('Accept')) options.headers.set('Accept', jsonLdMimeType);
if ('undefined' !== options.body && !(options.body instanceof FormData) && null === options.headers.get('Content-Type')) {
options.headers.set('Content-Type', jsonLdMimeType);
}
let token = await AsyncStorage.getItem('token');
console.log({"url": url,"new fetch": token});
if (token) {
options.headers.set('Authorization', 'Bearer ' + token);
}
let api = '/api';
if (noApi) {
api = "";
}
const link = GLOBALS.BASE_URL + api + url;
return fetch(link, options).then(response => {
if (response.ok) return response;
return response
.json()
.then(json => {
if (json.code === 401) {
toast(I18n.t(json.message), "danger", 3000);
AsyncStorage.setItem('token', '');
}
const error = json['message'] ? json['message'] : response.statusText;
throw Error(I18n.t(error));
})
.catch(err => {
throw err;
});
})
.catch(err => {
throw err;
});
}
我的问题是:
- 当我做一个动作时,中间件被调用。
- 如果令牌即将过期,则调用刷新令牌方法并更新 AsyncStorage。
- 那么应该调用
next(action)
方法。
- 但是我的
/templates
端点在我的 /token/refresh
端点之前(而不是之后)使用旧的过期令牌调用...
- 那么结果是我当前的屏幕 return 一个错误(未授权)但是如果用户更改屏幕它将再次工作,因为它的令牌已成功刷新。但这样很丑:p
编辑:为了解决这个问题,我修改了我的代码以将其放入一个文件中。
我还放了一些 console.log 来展示这段代码将如何执行
从图中我们可以看出:
- 我的调用(/模板)在我的刷新端点之前执行。刷新令牌的控制台日志在那之后很久就到达了...
请问有什么帮助吗?
编辑直到赏金结束:
从那个问题我试图理解为什么我的方法关于中间件是错误的,因为我在互联网上找到的许多资源都在谈论中间件是实现刷新令牌操作的最佳解决方案。
您遇到了请求竞争条件,并且没有可以完全解决此问题的正确解决方案。下一项可以作为解决这个问题的起点:
- 单独使用令牌刷新,在客户端等待执行,例如发送令牌刷新(类似于 GET /keepalive)以防在会话超时的一半时间内发送任何请求 - 这将导致所有请求都将 100% 授权(我肯定会使用的选项 - 它可以是不仅用于跟踪请求,还用于跟踪事件)
- 收到 401 后清理令牌 - 假设在边界情况下删除有效令牌是肯定的情况(解决方案易于实施),重新加载后您将看不到工作应用程序
- 重复收到 401 的查询,但有一些延迟(实际上不是最佳选择)
强制令牌更新比超时更频繁 - 在超时的 50-75% 处更改它们将减少失败请求的数量(但如果用户在整个会话时间内都处于空闲状态,它们仍然会持续存在) .因此,任何有效请求都将 return 新的有效令牌代替旧令牌。
实施令牌延长期,当旧令牌可以被计算为在转移期间有效时 - 旧令牌被延长一段时间以绕过问题(听起来不是很好,但它是一个选项至少)
我的处理设置略有不同。我没有在中间件中处理刷新令牌逻辑,而是将其定义为辅助函数。这样我就可以在我认为合适的任何网络请求之前进行所有令牌验证,并且任何不涉及网络请求的 redux 操作都不需要此功能
export const refreshToken = async () => {
let valid = true;
if (!validateAccessToken()) {
try {
//logic to refresh token
valid = true;
} catch (err) {
valid = false;
}
return valid;
}
return valid;
};
const validateAccessToken = () => {
const currentTime = new Date();
if (
moment(currentTime).add(10, 'm') <
moment(jwtDecode(token).exp * 1000)
) {
return true;
}
return false;
};
现在我们有了这个辅助函数,我为所有需要的 redux 操作调用它
const shouldRefreshToken = await refreshToken();
if (!shouldRefreshToken) {
dispatch({
type: OPERATION_FAILED,
payload: apiErrorGenerator({ err: { response: { status: 401 } } })
});
} else {
//...
}
在您的中间件中,您使 store.dispatch
异步,但 store.dispatch
的原始签名是同步的。这可能会产生严重的副作用。
让我们考虑一个简单的中间件,它记录应用程序中发生的每个操作,以及在它之后计算的状态:
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
编写上述中间件实质上是在做以下事情:
const next = store.dispatch // you take current version of store.dispatch
store.dispatch = function dispatchAndLog(action) { // you change it to meet your needs
console.log('dispatching', action)
let result = next(action) // and you return whatever the current version is supposed to return
console.log('next state', store.getState())
return result
}
考虑将 3 个这样的中间件链接在一起的示例:
const {
createStore,
applyMiddleware,
combineReducers,
compose
} = window.Redux;
const counterReducer = (state = 0, action) => {
switch (action.type) {
case "INCREMENT":
return state + 1;
default:
return state;
}
};
const rootReducer = combineReducers({
counter: counterReducer
});
const logger = store => next => action => {
console.log("dispatching", action);
let result = next(action);
console.log("next state", store.getState());
return result;
};
const logger2 = store => next => action => {
console.log("dispatching 2", action);
let result = next(action);
console.log("next state 2", store.getState());
return result;
};
const logger3 = store => next => action => {
console.log("dispatching 3", action);
let result = next(action);
console.log("next state 3", store.getState());
return result;
};
const middlewareEnhancer = applyMiddleware(logger, logger2, logger3);
const store = createStore(rootReducer, middlewareEnhancer);
store.dispatch({
type: "INCREMENT"
});
console.log('current state', store.getState());
<script src="https://unpkg.com/redux@4.0.1/dist/redux.js"></script>
首先 logger
获取操作,然后 logger2
,然后 logger3
然后它转到实际的 store.dispatch
& reducer 被调用。 reducer 将状态从 0 更改为 1,logger3
获取更新后的状态并将 return 值(操作)传播回 logger2
,然后 logger
.
现在,让我们考虑将 store.dispatch
更改为链中间某处的异步函数时会发生什么:
const logger2 = store => next => async action => {
function wait(ms) {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, ms);
});
}
await wait(5000).then(v => {
console.log("dispatching 2", action);
let result = next(action);
console.log("next state 2", store.getState());
return result;
});
};
我已经修改了 logger2
,但是 logger
(链上的那个)不知道 next
现在是异步的。它将 return 待定 Promise
并将返回 "unupdated" 状态,因为调度的操作尚未到达减速器。
const {
createStore,
applyMiddleware,
combineReducers,
compose
} = window.Redux;
const counterReducer = (state = 0, action) => {
switch (action.type) {
case "INCREMENT":
return state + 1;
default:
return state;
}
};
const rootReducer = combineReducers({
counter: counterReducer
});
const logger = store => next => action => {
console.log("dispatching", action);
let result = next(action); // will return a pending Promise
console.log("next state", store.getState());
return result;
};
const logger2 = store => next => async action => {
function wait(ms) {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, ms);
});
}
await wait(2000).then(() => {
console.log("dispatching 2", action);
let result = next(action);
console.log("next state 2", store.getState());
return result;
});
};
const logger3 = store => next => action => {
console.log("dispatching 3", action);
let result = next(action);
console.log("next state 3", store.getState());
return result;
};
const middlewareEnhancer = applyMiddleware(logger, logger2, logger3);
const store = createStore(rootReducer, middlewareEnhancer);
store.dispatch({ // console.log of it's return value is too a pending `Promise`
type: "INCREMENT"
});
console.log('current state', store.getState());
<script src="https://unpkg.com/redux@4.0.1/dist/redux.js"></script>
所以 my store.dispatch
returns 立即从带有未决 Promise 的中间件链和 console.log('current state', store.getState());
仍然打印 0。达到原始 store.dispatch
之后减速器 looong。
我不知道你的整个设置,但我猜你的情况就是这样。您假设您的中间件已经做了一些事情并进行了往返,但实际上它还没有完成工作(或者没有人 await
告诉他完成它)。可能您正在调度一个获取 /templates
的操作,并且由于您编写了一个中间件来自动更新不记名令牌,您假设 fetch 助手实用程序将使用全新的令牌调用。但是 dispatch
提前 return 编辑了一个未决的承诺,你的令牌仍然是旧的。
除此之外,只有一件事看起来是错误的:您在中间件中通过 next
:
分派了两次相同的操作
const tokenMiddleware = store => next => async action => {
if (something) {
if (something) {
await fetch('/token/refresh',)
.then(async (data) => {
return await Promise.all([
// ...
]).then((values) => {
return next(action); // First, after the `Promise.all` resolves
});
});
return next(action); // then again after the `fetch` resolves, this one seems redundant & should be removed
} else {
return next(action);
}
}
建议:
- 将您的令牌保存在 redux 存储中,将它们保存在存储中并从存储中重新水化 redux 存储
- 为所有 api 调用编写一个 Async Action Creator,这将在必要时刷新令牌并仅在刷新令牌后异步发送操作。
示例redux thunk:
function apiCallMaker(dispatch, url, actions) {
dispatch({
type: actions[0]
})
return fetch(url)
.then(
response => response.json(),
error => {
dispatch({
type: actions[2],
payload: error
})
}
)
.then(json =>
dispatch({
type: actions[1],
payload: json
})
)
}
}
export function createApiCallingActions(url, actions) {
return function(dispatch, getState) {
const { accessToken, refreshToken } = getState();
if(neededToRefresh) {
return fetch(url)
.then(
response => response.json(),
error => {
dispatch({
type: 'TOKEN_REFRESH_FAILURE',
payload: error
})
}
)
.then(json =>
dispatch({
type: 'TOKEN_REFRESH_SUCCESS',
payload: json
})
apiCallMaker(dispatch, url, actions)
)
} else {
return apiCallMaker(dispatch, url, actions)
}
}
您可以这样使用它:
dispatch(createApiCallingActions('/api/foo', ['FOO FETCH', 'FOO SUCCESS', 'FOO FAILURE'])
dispatch(createApiCallingActions('/api/bar', ['BAR FETCH', 'BAR SUCCESS', 'BAR FAILURE'])
我使用以下中间件在令牌过期时刷新我的令牌:
import {AsyncStorage} from 'react-native';
import moment from 'moment';
import fetch from "../components/Fetch";
import jwt_decode from 'jwt-decode';
/**
* This middleware is meant to be the refresher of the authentication token, on each request to the API,
* it will first call refresh token endpoint
* @returns {function(*=): Function}
* @param store
*/
const tokenMiddleware = store => next => async action => {
if (typeof action === 'object' && action.type !== "FETCHING_TEMPLATES_FAILED") {
let eToken = await AsyncStorage.getItem('eToken');
if (isExpired(eToken)) {
let rToken = await AsyncStorage.getItem('rToken');
let formData = new FormData();
formData.append("refresh_token", rToken);
await fetch('/token/refresh',
{
method: 'POST',
body: formData
})
.then(response => response.json())
.then(async (data) => {
let decoded = jwt_decode(data.token);
console.log({"refreshed": data.token});
return await Promise.all([
await AsyncStorage.setItem('token', data.token).then(() => {return AsyncStorage.getItem('token')}),
await AsyncStorage.setItem('rToken', data.refresh_token).then(() => {return AsyncStorage.getItem('rToken')}),
await AsyncStorage.setItem('eToken', decoded.exp.toString()).then(() => {return AsyncStorage.getItem('eToken')}),
]).then((values) => {
return next(action);
});
}).catch((err) => {
console.log(err);
});
return next(action);
} else {
return next(action);
}
}
function isExpired(expiresIn) {
// We refresh the token 3.5 hours before it expires(12600 seconds) (lifetime on server 25200seconds)
return moment.unix(expiresIn).diff(moment(), 'seconds') < 10;
}
};
export default tokenMiddleware;
获取助手:
import { AsyncStorage } from 'react-native';
import GLOBALS from '../constants/Globals';
import {toast} from "./Toast";
import I18n from "../i18n/i18n";
const jsonLdMimeType = 'application/ld+json';
export default async function (url, options = {}, noApi = false) {
if ('undefined' === typeof options.headers) options.headers = new Headers();
if (null === options.headers.get('Accept')) options.headers.set('Accept', jsonLdMimeType);
if ('undefined' !== options.body && !(options.body instanceof FormData) && null === options.headers.get('Content-Type')) {
options.headers.set('Content-Type', jsonLdMimeType);
}
let token = await AsyncStorage.getItem('token');
console.log({"url": url,"new fetch": token});
if (token) {
options.headers.set('Authorization', 'Bearer ' + token);
}
let api = '/api';
if (noApi) {
api = "";
}
const link = GLOBALS.BASE_URL + api + url;
return fetch(link, options).then(response => {
if (response.ok) return response;
return response
.json()
.then(json => {
if (json.code === 401) {
toast(I18n.t(json.message), "danger", 3000);
AsyncStorage.setItem('token', '');
}
const error = json['message'] ? json['message'] : response.statusText;
throw Error(I18n.t(error));
})
.catch(err => {
throw err;
});
})
.catch(err => {
throw err;
});
}
我的问题是:
- 当我做一个动作时,中间件被调用。
- 如果令牌即将过期,则调用刷新令牌方法并更新 AsyncStorage。
- 那么应该调用
next(action)
方法。 - 但是我的
/templates
端点在我的/token/refresh
端点之前(而不是之后)使用旧的过期令牌调用... - 那么结果是我当前的屏幕 return 一个错误(未授权)但是如果用户更改屏幕它将再次工作,因为它的令牌已成功刷新。但这样很丑:p
编辑:为了解决这个问题,我修改了我的代码以将其放入一个文件中。 我还放了一些 console.log 来展示这段代码将如何执行
从图中我们可以看出:
- 我的调用(/模板)在我的刷新端点之前执行。刷新令牌的控制台日志在那之后很久就到达了...
请问有什么帮助吗?
编辑直到赏金结束:
从那个问题我试图理解为什么我的方法关于中间件是错误的,因为我在互联网上找到的许多资源都在谈论中间件是实现刷新令牌操作的最佳解决方案。
您遇到了请求竞争条件,并且没有可以完全解决此问题的正确解决方案。下一项可以作为解决这个问题的起点:
- 单独使用令牌刷新,在客户端等待执行,例如发送令牌刷新(类似于 GET /keepalive)以防在会话超时的一半时间内发送任何请求 - 这将导致所有请求都将 100% 授权(我肯定会使用的选项 - 它可以是不仅用于跟踪请求,还用于跟踪事件)
- 收到 401 后清理令牌 - 假设在边界情况下删除有效令牌是肯定的情况(解决方案易于实施),重新加载后您将看不到工作应用程序
- 重复收到 401 的查询,但有一些延迟(实际上不是最佳选择)
强制令牌更新比超时更频繁 - 在超时的 50-75% 处更改它们将减少失败请求的数量(但如果用户在整个会话时间内都处于空闲状态,它们仍然会持续存在) .因此,任何有效请求都将 return 新的有效令牌代替旧令牌。
实施令牌延长期,当旧令牌可以被计算为在转移期间有效时 - 旧令牌被延长一段时间以绕过问题(听起来不是很好,但它是一个选项至少)
我的处理设置略有不同。我没有在中间件中处理刷新令牌逻辑,而是将其定义为辅助函数。这样我就可以在我认为合适的任何网络请求之前进行所有令牌验证,并且任何不涉及网络请求的 redux 操作都不需要此功能
export const refreshToken = async () => {
let valid = true;
if (!validateAccessToken()) {
try {
//logic to refresh token
valid = true;
} catch (err) {
valid = false;
}
return valid;
}
return valid;
};
const validateAccessToken = () => {
const currentTime = new Date();
if (
moment(currentTime).add(10, 'm') <
moment(jwtDecode(token).exp * 1000)
) {
return true;
}
return false;
};
现在我们有了这个辅助函数,我为所有需要的 redux 操作调用它
const shouldRefreshToken = await refreshToken();
if (!shouldRefreshToken) {
dispatch({
type: OPERATION_FAILED,
payload: apiErrorGenerator({ err: { response: { status: 401 } } })
});
} else {
//...
}
在您的中间件中,您使 store.dispatch
异步,但 store.dispatch
的原始签名是同步的。这可能会产生严重的副作用。
让我们考虑一个简单的中间件,它记录应用程序中发生的每个操作,以及在它之后计算的状态:
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
编写上述中间件实质上是在做以下事情:
const next = store.dispatch // you take current version of store.dispatch
store.dispatch = function dispatchAndLog(action) { // you change it to meet your needs
console.log('dispatching', action)
let result = next(action) // and you return whatever the current version is supposed to return
console.log('next state', store.getState())
return result
}
考虑将 3 个这样的中间件链接在一起的示例:
const {
createStore,
applyMiddleware,
combineReducers,
compose
} = window.Redux;
const counterReducer = (state = 0, action) => {
switch (action.type) {
case "INCREMENT":
return state + 1;
default:
return state;
}
};
const rootReducer = combineReducers({
counter: counterReducer
});
const logger = store => next => action => {
console.log("dispatching", action);
let result = next(action);
console.log("next state", store.getState());
return result;
};
const logger2 = store => next => action => {
console.log("dispatching 2", action);
let result = next(action);
console.log("next state 2", store.getState());
return result;
};
const logger3 = store => next => action => {
console.log("dispatching 3", action);
let result = next(action);
console.log("next state 3", store.getState());
return result;
};
const middlewareEnhancer = applyMiddleware(logger, logger2, logger3);
const store = createStore(rootReducer, middlewareEnhancer);
store.dispatch({
type: "INCREMENT"
});
console.log('current state', store.getState());
<script src="https://unpkg.com/redux@4.0.1/dist/redux.js"></script>
首先 logger
获取操作,然后 logger2
,然后 logger3
然后它转到实际的 store.dispatch
& reducer 被调用。 reducer 将状态从 0 更改为 1,logger3
获取更新后的状态并将 return 值(操作)传播回 logger2
,然后 logger
.
现在,让我们考虑将 store.dispatch
更改为链中间某处的异步函数时会发生什么:
const logger2 = store => next => async action => {
function wait(ms) {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, ms);
});
}
await wait(5000).then(v => {
console.log("dispatching 2", action);
let result = next(action);
console.log("next state 2", store.getState());
return result;
});
};
我已经修改了 logger2
,但是 logger
(链上的那个)不知道 next
现在是异步的。它将 return 待定 Promise
并将返回 "unupdated" 状态,因为调度的操作尚未到达减速器。
const {
createStore,
applyMiddleware,
combineReducers,
compose
} = window.Redux;
const counterReducer = (state = 0, action) => {
switch (action.type) {
case "INCREMENT":
return state + 1;
default:
return state;
}
};
const rootReducer = combineReducers({
counter: counterReducer
});
const logger = store => next => action => {
console.log("dispatching", action);
let result = next(action); // will return a pending Promise
console.log("next state", store.getState());
return result;
};
const logger2 = store => next => async action => {
function wait(ms) {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, ms);
});
}
await wait(2000).then(() => {
console.log("dispatching 2", action);
let result = next(action);
console.log("next state 2", store.getState());
return result;
});
};
const logger3 = store => next => action => {
console.log("dispatching 3", action);
let result = next(action);
console.log("next state 3", store.getState());
return result;
};
const middlewareEnhancer = applyMiddleware(logger, logger2, logger3);
const store = createStore(rootReducer, middlewareEnhancer);
store.dispatch({ // console.log of it's return value is too a pending `Promise`
type: "INCREMENT"
});
console.log('current state', store.getState());
<script src="https://unpkg.com/redux@4.0.1/dist/redux.js"></script>
所以 my store.dispatch
returns 立即从带有未决 Promise 的中间件链和 console.log('current state', store.getState());
仍然打印 0。达到原始 store.dispatch
之后减速器 looong。
我不知道你的整个设置,但我猜你的情况就是这样。您假设您的中间件已经做了一些事情并进行了往返,但实际上它还没有完成工作(或者没有人 await
告诉他完成它)。可能您正在调度一个获取 /templates
的操作,并且由于您编写了一个中间件来自动更新不记名令牌,您假设 fetch 助手实用程序将使用全新的令牌调用。但是 dispatch
提前 return 编辑了一个未决的承诺,你的令牌仍然是旧的。
除此之外,只有一件事看起来是错误的:您在中间件中通过 next
:
const tokenMiddleware = store => next => async action => {
if (something) {
if (something) {
await fetch('/token/refresh',)
.then(async (data) => {
return await Promise.all([
// ...
]).then((values) => {
return next(action); // First, after the `Promise.all` resolves
});
});
return next(action); // then again after the `fetch` resolves, this one seems redundant & should be removed
} else {
return next(action);
}
}
建议:
- 将您的令牌保存在 redux 存储中,将它们保存在存储中并从存储中重新水化 redux 存储
- 为所有 api 调用编写一个 Async Action Creator,这将在必要时刷新令牌并仅在刷新令牌后异步发送操作。
示例redux thunk:
function apiCallMaker(dispatch, url, actions) {
dispatch({
type: actions[0]
})
return fetch(url)
.then(
response => response.json(),
error => {
dispatch({
type: actions[2],
payload: error
})
}
)
.then(json =>
dispatch({
type: actions[1],
payload: json
})
)
}
}
export function createApiCallingActions(url, actions) {
return function(dispatch, getState) {
const { accessToken, refreshToken } = getState();
if(neededToRefresh) {
return fetch(url)
.then(
response => response.json(),
error => {
dispatch({
type: 'TOKEN_REFRESH_FAILURE',
payload: error
})
}
)
.then(json =>
dispatch({
type: 'TOKEN_REFRESH_SUCCESS',
payload: json
})
apiCallMaker(dispatch, url, actions)
)
} else {
return apiCallMaker(dispatch, url, actions)
}
}
您可以这样使用它:
dispatch(createApiCallingActions('/api/foo', ['FOO FETCH', 'FOO SUCCESS', 'FOO FAILURE'])
dispatch(createApiCallingActions('/api/bar', ['BAR FETCH', 'BAR SUCCESS', 'BAR FAILURE'])