ReactネイティブReduxアプリはJWTトークンを認証に使用します。このようなトークンを必要とするアクションは多数あり、それらの多くはアプリのロード時などに同時にディスパッチされます。
例えば。
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
アクションの両方でトークンの更新が発生することでした。なぜなら、それらがディスパッチされるとトークンが期限切れになるためです。理想的には、トークンが更新されるまで認証を必要とするアクションを「一時停止」したいと思います。ミドルウェアでそれを行う方法はありますか?
これを解決する方法を見つけました。これがベストプラクティスのアプローチであるかどうかはわかりませんが、おそらくいくつかの改善が行われる可能性があります。
私の元々のアイデアは変わりません: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
でアクションをディスパッチすることも少し心配です。 JWTトークンの期限切れをreduxで処理する他の方法を教えてください。
アクションが完了するのを「待つ」代わりに、ストア変数を保持して、トークンをまだフェッチしているかどうかを知ることができます。
サンプルレデューサー
const initialState = {
fetching: false,
};
export function reducer(state = initialState, action) {
switch(action.type) {
case 'LOAD_FETCHING':
return {
...state,
fetching: action.fetching,
}
}
}
アクション作成者:
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
をtrueに設定するアクションをディスパッチし、トークンを更新します。まだプロファイルまたはアセットをロードしないことに注意してください。
新しいコンポーネント:
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()
}
}
マウントに物をロードしようとしますが、小道具を受け取ったときに特定の条件下でロードしようとすることに注意してください(ストアが変更されると呼び出され、fetching
を保持できます)。最初のフェッチが失敗すると、 refreshToken
。それが完了すると、ストアに新しいトークンを設定し、コンポーネントを更新してcomponentWillReceiveProps
を呼び出します。まだフェッチしていない場合(このチェックが必要かどうかわからない)、ロードします。
アクションを延期し、アクセストークンを更新するために、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();
トークンを状態に保ち、単純なヘルパーを使用してアクセストークンをリクエストヘッダーに挿入します
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
]
}
})