我们的React Native Redux应用程序使用JWT令牌进行身份验证。有许多动作需要这样的令牌,并且许多动作同时被派遣,例如当app加载时。
E.g。
componentDidMount() {
dispath(loadProfile());
dispatch(loadAssets());
...
}
loadProfile
和loadAssets
都需要JWT。我们将令牌保存在状态AsyncStorage
中。我的问题是如何处理令牌过期。
最初我打算使用中间件来处理令牌过期
// jwt-middleware.js
export function refreshJWTToken({ dispatch, getState }) {
return (next) => (action) => {
if (isExpired(getState().auth.token)) {
return dispatch(refreshToken())
.then(() => next(action))
.catch(e => console.log('error refreshing token', e));
}
return next(action);
};
}
我遇到的问题是,loadProfile
和loadAssets
操作都会刷新令牌,因为在发送令牌时,令牌将会过期。理想情况下,我想“暂停”需要身份验证的操作,直到刷新令牌。有没有办法用中间件做到这一点?
答案 0 :(得分:28)
我找到了解决这个问题的方法。我不确定这是否是最佳实践方法,并且可能会对其进行一些改进。
我最初的想法仍然存在:JWT刷新是在中间件中。如果使用thunk
,那么中间件必须在thunk
之前出现。
...
const createStoreWithMiddleware = applyMiddleware(jwt, thunk)(createStore);
然后在中间件代码中,我们检查令牌是否在任何异步操作之前过期。如果它已过期,我们还会检查我们是否已经刷新了令牌 - 为了能够进行此类检查,我们会向该州添加新令牌的承诺。
import { refreshToken } from '../actions/auth';
export function jwt({ dispatch, getState }) {
return (next) => (action) => {
// only worry about expiring token for async actions
if (typeof action === 'function') {
if (getState().auth && getState().auth.token) {
// decode jwt so that we know if and when it expires
var tokenExpiration = jwtDecode(getState().auth.token).<your field for expiration>;
if (tokenExpiration && (moment(tokenExpiration) - moment(Date.now()) < 5000)) {
// make sure we are not already refreshing the token
if (!getState().auth.freshTokenPromise) {
return refreshToken(dispatch).then(() => next(action));
} else {
return getState().auth.freshTokenPromise.then(() => next(action));
}
}
}
}
return next(action);
};
}
最重要的部分是refreshToken
功能。该函数需要在刷新令牌时调度操作,以便状态将包含新令牌的承诺。这样,如果我们同时调度使用令牌身份验证的多个异步操作,则令牌只会刷新一次。
export function refreshToken(dispatch) {
var freshTokenPromise = fetchJWTToken()
.then(t => {
dispatch({
type: DONE_REFRESHING_TOKEN
});
dispatch(saveAppToken(t.token));
return t.token ? Promise.resolve(t.token) : Promise.reject({
message: 'could not refresh token'
});
})
.catch(e => {
console.log('error refreshing token', e);
dispatch({
type: DONE_REFRESHING_TOKEN
});
return Promise.reject(e);
});
dispatch({
type: REFRESHING_TOKEN,
// we want to keep track of token promise in the state so that we don't try to refresh
// the token again while refreshing is in process
freshTokenPromise
});
return freshTokenPromise;
}
我意识到这很复杂。我也有点担心在refreshToken
中调度行动本身并不是行动。请告诉我您知道的任何其他方法,以处理使用redux到期的JWT令牌。
答案 1 :(得分:18)
而不是&#34;等待&#34;要完成某个操作,您可以保留一个商店变量,以便知道您是否还在提取令牌:
样品减速器
export function loadThings() {
return (dispatch, getState) => {
const { auth, isLoading } = getState();
if (!isExpired(auth.token)) {
dispatch({ type: 'LOAD_FETCHING', fetching: false })
dispatch(loadProfile());
dispatch(loadAssets());
} else {
dispatch({ type: 'LOAD_FETCHING', fetching: true })
dispatch(refreshToken());
}
};
}
现在是动作创建者:
fetching
安装组件时会调用此方法。如果auth键是陈旧的,它将调度一个动作以将componentDidMount() {
dispath(loadThings());
// ...
}
componentWillReceiveProps(newProps) {
const { fetching, token } = newProps; // bound from store
// assuming you have the current token stored somewhere
if (token === storedToken) {
return; // exit early
}
if (!fetching) {
loadThings()
}
}
设置为true并刷新令牌。请注意,我们尚未加载配置文件或资产。
新组件:
fetching
请注意,现在您尝试在挂载时加载您的东西,但在接收道具时也会在某些条件下加载(这将在商店更改时调用,以便我们可以保留refreshToken
)当初始提取失败时,它将会触发componentWillReceiveProps
。完成后,它会在商店中设置新令牌,更新组件,从而调用{{1}}。如果它仍未提取(不确定是否需要进行此检查),则会加载内容。
答案 2 :(得分:5)
我在redux-api-middleware
周围做了一个简单的包装,推迟了操作并刷新了访问令牌。
middleware.js
import { isRSAA, apiMiddleware } from 'redux-api-middleware';
import { TOKEN_RECEIVED, refreshAccessToken } from './actions/auth'
import { refreshToken, isAccessTokenExpired } from './reducers'
export function createApiMiddleware() {
const postponedRSAAs = []
return ({ dispatch, getState }) => {
const rsaaMiddleware = apiMiddleware({dispatch, getState})
return (next) => (action) => {
const nextCheckPostoned = (nextAction) => {
// Run postponed actions after token refresh
if (nextAction.type === TOKEN_RECEIVED) {
next(nextAction);
postponedRSAAs.forEach((postponed) => {
rsaaMiddleware(next)(postponed)
})
} else {
next(nextAction)
}
}
if(isRSAA(action)) {
const state = getState(),
token = refreshToken(state)
if(token && isAccessTokenExpired(state)) {
postponedRSAAs.push(action)
if(postponedRSAAs.length === 1) {
return rsaaMiddleware(nextCheckPostoned)(refreshAccessToken(token))
} else {
return
}
}
return rsaaMiddleware(next)(action);
}
return next(action);
}
}
}
export default createApiMiddleware();
我在状态中保留令牌,并使用简单的帮助程序将Acess令牌注入请求标头
export function withAuth(headers={}) {
return (state) => ({
...headers,
'Authorization': `Bearer ${accessToken(state)}`
})
}
所以redux-api-middleware
动作几乎保持不变
export const echo = (message) => ({
[RSAA]: {
endpoint: '/api/echo/',
method: 'POST',
body: JSON.stringify({message: message}),
headers: withAuth({ 'Content-Type': 'application/json' }),
types: [
ECHO_REQUEST, ECHO_SUCCESS, ECHO_FAILURE
]
}
})
我写了article并分享了project example,它显示了JWT刷新令牌工作流程
答案 3 :(得分:0)
我认为 redux 不是强制执行令牌刷新原子性的正确工具。
相反,我可以为您提供一个可以从任何地方调用的原子函数,并确保您始终获得有效的令牌:
/*
The non-atomic refresh function
*/
const refreshToken = async () => {
// Do whatever you need to do here ...
}
/*
Promise locking-queueing structure
*/
var promiesCallbacks = [];
const resolveQueue = value => {
promiesCallbacks.forEach(x => x.resolve(value));
promiesCallbacks = [];
};
const rejectQueue = value => {
promiesCallbacks.forEach(x => x.reject(value));
promiesCallbacks = [];
};
const enqueuePromise = () => {
return new Promise((resolve, reject) => {
promiesCallbacks.push({resolve, reject});
});
};
/*
The atomic function!
*/
var actionInProgress = false;
const refreshTokenAtomically = () => {
if (actionInProgress) {
return enqueuePromise();
}
actionInProgress = true;
return refreshToken()
.then(({ access }) => {
resolveQueue(access);
return access;
})
.catch((error) => {
rejectQueue(error);
throw error;
})
.finally(() => {
actionInProgress = false;
});
};