2016-04-29 18 views
31

La nostra app React Native Redux utilizza token JWT per l'autenticazione. Esistono molte azioni che richiedono tali token e molti di essi vengono inviati simultaneamente, ad es. quando viene caricata l'app.Come utilizzare Redux per aggiornare il token JWT?

E.g.

componentDidMount() { 
    dispath(loadProfile()); 
    dispatch(loadAssets()); 
    ... 
} 

Sia loadProfile e loadAssets richiedono JWT. Salviamo il token nello stato e AsyncStorage. La mia domanda è come gestire la scadenza del token.

Inizialmente stavo per usare il middleware per la gestione di scadenza del token

}

Il problema che ho incontrato è stato che rinfrescante del token avverrà per entrambi i loadProfile e loadAssets azioni perché al momento in cui vengono spediti il ​​token sarà scaduto. Idealmente mi piacerebbe "mettere in pausa" le azioni che richiedono l'autenticazione finché il token non viene aggiornato. C'è un modo per farlo con il middleware?

+1

Ti suggerisco di guardare una libreria chiamata [redux-saga] (https://github.com/yelouafi/redux-saga) ... Risolve perfettamente questo problema . –

risposta

19

Ho trovato un modo per risolvere questo. Non sono sicuro che questo sia l'approccio delle migliori pratiche e probabilmente ci sono alcuni miglioramenti che potrebbero essere apportati.

La mia idea originale rimane: JWT refresh è nel middleware. Questo middleware deve venire prima di thunk se viene utilizzato thunk.

... 
const createStoreWithMiddleware = applyMiddleware(jwt, thunk)(createStore); 

Poi nel codice middleware, controlliamo per vedere se token è scaduto prima di qualsiasi azione asincrona. Se è scaduto, controlliamo anche se stiamo già aggiornando il token: per essere in grado di avere tale controllo aggiungiamo promessa per token fresco allo stato.

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().then(() => next(action)); 
        } else { 
         return getState().auth.freshTokenPromise.then(() => next(action)); 
        } 
       } 
      } 
     } 
     return next(action); 
    }; 
} 

La parte più importante è la funzione refreshToken. Quella funzione deve inviare un'azione quando il token viene aggiornato in modo che lo stato conterrà la promessa per il token nuovo. In questo modo, se inviamo più azioni asincrone che utilizzano l'autenticazione del token contemporaneamente, il token viene aggiornato solo una volta.

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

Mi rendo conto che questo è piuttosto complicato. Sono anche un po 'preoccupato per le operazioni di spedizione in refreshToken che non è un'azione in sé. Per favore fatemi sapere qualsiasi altro approccio sapete che gestisce il token JWT in scadenza con redux.

+0

È possibile che refreshToken riceva un'Azione posticipata che verrà inviata se l'aggiornamento viene eseguito correttamente invece di restituire una nuova Promessa. Almeno è così che ho risolto questo. –

+0

@ MatíasHernánGarcía - Ne hai un esempio? –

+0

@PI Sto indovinando @ MatíasHernánGarcía significava qualcosa come (pseudo codice): 'funzione di esportazione refreshToken (postponedAction) {refreshSuccess.then (() => dispatch (postponedAction))}' –

15

Invece di "attesa" per un'azione per finire, si potrebbe invece mantenere una variabile negozio per sapere se si sta ancora andare a prendere gettoni:

Esempio riduttore

const initialState = { 
    fetching: false, 
}; 
export function reducer(state = initialState, action) { 
    switch(action.type) { 
     case 'LOAD_FETCHING': 
      return { 
       ...state, 
       fetching: action.fetching, 
      } 
    } 
} 

Ora il creatore di azione:

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()); 
     } 
    }; 
} 

Viene chiamato quando il componente è montato. Se la chiave di autenticazione è obsoleta, invierà un'azione per impostare fetching su true e aggiornare anche il token. Si noti che non stiamo ancora caricando il profilo o le risorse.

nuovo componente:

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() 
    } 
} 

noti che ora si tenta di caricare le tue cose sul monte, ma anche, in determinate condizioni, quando la ricezione di oggetti di scena (questo verrà chiamato quando il negozio cambia così possiamo mantenere fetching lì) Quando il recupero iniziale non riesce, attiverà lo refreshToken. Al termine, imposterà il nuovo token nel negozio, aggiornando il componente e quindi chiamando componentWillReceiveProps. Se non sta ancora recuperando (non è sicuro che questo controllo sia necessario), caricherà le cose.

+0

Grazie! Questo ha sicuramente senso per il carico iniziale. Ma non sono sicuro che funzioni per i token in scadenza dopo che l'app è stata caricata ed è in uso. Ogni chiamata all'API richiede un token valido. Abbiamo molte visualizzazioni popup che richiedono l'accesso e caricano i dati, quindi non sono sicuro se la gestione della scadenza attraverso gli oggetti di scena per quelle viste funzionerebbe. – lanan

+0

È possibile modificare la logica per verificare la scadenza del token anziché la differenza di token.L'idea è che qualsiasi azione attiverà questo metodo del ciclo di vita in modo da poterlo utilizzare per aggiornare la variabile 'fetching' e reagire di conseguenza – ZekeDroid

+1

Il mio primo problema con l'aggiunta di' dispatch ({type: 'LOAD_FETCHING', fetching: true}) ' per ogni azione che richiede JWT è la duplicazione del codice. Il secondo problema è come sapere quando l'aggiornamento è stato completato. Diciamo che c'è un pulsante "Aggiungi ai Preferiti" che invia una chiamata API che richiede l'autenticazione. Voglio aggiungere "se il token è scaduto aggiornare quindi effettuare una chiamata" logica per tale azione? Che ne pensi di altre azioni simili? Questo è il motivo per cui sto cercando di utilizzare il middleware. In altri contesti/lingue ho usato decoratori ma non sono sicuro di poterlo fare con React. – lanan

2

Ho creato un semplice wrapper attorno allo redux-api-middleware per posticipare le azioni e aggiornare il token di accesso.

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(); 

tengo gettoni nello stato, e utilizzare un semplice aiuto per iniettare Acess pedina in un header di richiesta

export function withAuth(headers={}) { 
    return (state) => ({ 
    ...headers, 
    'Authorization': `Bearer ${accessToken(state)}` 
    }) 
} 

Così redux-api-middleware azioni rimane pressoché invariato

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 
     ] 
    } 
}) 

Ho scritto il article e condiviso il project example, che mostra il flusso di lavoro del token di aggiornamento JWT in azione

Problemi correlati