2016-04-29 28 views
31

Nasza aplikacja React Native Redux wykorzystuje tokeny JWT do uwierzytelniania. Istnieje wiele działań, które wymagają takich tokenów, a wiele z nich jest wysyłanych jednocześnie, np. po załadowaniu aplikacji.Jak używać Redux do odświeżania tokena JWT?

E.g.

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

Zarówno loadProfile i loadAssets wymagają JWT. Zapisujemy token w stanie i AsyncStorage. Moje pytanie brzmi: jak poradzić sobie z wygaśnięciem tokena.

Początkowo miałem zamiar użyć middleware do obsługi tokenu wygasania

}

Problem, że wpadłem na to, że odświeżanie tokena stanie się dla obu loadProfile i loadAssets działań, ponieważ w w momencie wysyłki znacznik wygasa. Idealnie chciałbym "wstrzymać" akcje, które wymagają uwierzytelnienia, dopóki token nie zostanie odświeżony. Czy można to zrobić za pomocą oprogramowania pośredniego?

+1

Proponuję, abyś spojrzał na bibliotekę zwaną [redux-saga] (https://github.com/yelouafi/redux-saga) ... To rozwiązuje ten problem doskonale . –

Odpowiedz

19

Znalazłem sposób na rozwiązanie tego problemu. Nie jestem pewien, czy jest to podejście oparte na najlepszych praktykach i prawdopodobnie istnieją pewne ulepszenia, które można w nim wprowadzić.

Mój oryginalny pomysł pozostaje: odświeżanie JWT jest w oprogramowaniu pośredniczącym. To oprogramowanie pośrednie musi nadejść przed thunk, jeśli użyto thunk.

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

Następnie w kodzie oprogramowania pośredniego sprawdzamy, czy token wygasł przed jakąkolwiek operacją asynchroniczną. Jeśli wygasł, sprawdzamy również, czy już odświeżamy token - aby mieć taki czek, dodajemy obietnicę nowego tokena do stanu.

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

Najważniejszą częścią jest funkcja refreshToken. Ta funkcja musi wysłać akcję, gdy token jest odświeżany, aby państwo zawierało obietnicę nowego tokena. W ten sposób, jeśli wyślemy wiele akcji asynchronicznych, które używają tokenu uwierzytelniania jednocześnie token zostanie odświeżony tylko raz.

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

Zdaję sobie sprawę, że jest to dość skomplikowane. Jestem również trochę zaniepokojony wysyłaniem akcji w refreshToken, która nie jest sama w sobie. Daj mi znać o każdym innym podejściu, które wiesz, że obsługuje wygasanie tokena JWT za pomocą redux.

+0

Możesz spowodować, że refreshToken otrzyma "postponedAction", która zostanie wysłana, jeśli odświeżenie zakończy się pomyślnie, zamiast zwrócić nową obietnicę. Przynajmniej tak to rozwiązałem. –

+0

@ MatíasHernánGarcía - Czy masz na to przykład? –

+0

@PI Zgaduję @ MatíasHernánGarcía oznaczało coś takiego (pseudo kod): 'funkcja eksportu refreshToken (opóźnionaAkcja) {refreshSuccess.then (() => wysyłanie (opóźnionaAkcja))}' –

15

Zamiast „czeka” na działanie do końca, można zamiast zachować zmienną sklepu wiedzieć, czy nadal jesteś pobierania tokeny:

reduktor Próbka

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

Teraz twórca działanie:

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

Zostanie wywołana, gdy zamontowany jest komponent. Jeśli klucz autoryzacji jest nieaktualny, wywoła akcję, która ustawi wartość true na fetching, a także odświeży token. Zauważ, że nie będziemy jeszcze ładować profilu ani zasobów.

Nowy komponent:

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

Uwaga, teraz próbować załadować swoje rzeczy na górze, ale także w pewnych warunkach podczas odbierania rekwizyty (będzie uzyskać wywołana, gdy sklep zmienia więc możemy zachować fetching tam) Kiedy Początkowe pobieranie nie powiedzie się, spowoduje to wyświetlenie refreshToken. Po wykonaniu tej czynności ustawi nowy token w sklepie, aktualizując komponent i wywołując w ten sposób componentWillReceiveProps. Jeśli nie jest jeszcze pobierany (nie wiesz, czy to konieczne), załaduje rzeczy.

+0

Dzięki! To zdecydowanie ma sens dla początkowego obciążenia. Ale nie jestem pewien, czy działa dla wygasających tokenów po załadowaniu aplikacji i jest w użyciu. Każde wywołanie interfejsu API wymaga poprawnego tokena. Mamy wiele wyskakujących widoków, które wymagają danych logowania i ładowania, więc nie jestem pewien, czy zadziała obsługa wygaśnięcia przez rekwizyty dla tych widoków. – lanan

+0

Możesz zmienić logikę, aby sprawdzić datę wygaśnięcia tokena, a nie różnicę w tokenie.Chodzi o to, że każda akcja uruchomi tę metodę cyklu życia, dzięki czemu można jej użyć do aktualizacji zmiennej 'fetching' i odpowiednio zareagować. – ZekeDroid

+1

Mój pierwszy problem z dodaniem' dispatch ({type: 'LOAD_FETCHING', fetching: true}) ' do każdego działania, które wymaga JWT, jest powielanie kodu. Drugi problem polega na tym, aby dowiedzieć się, kiedy zakończyło się odświeżanie. Powiedzmy, że jest przycisk "Dodaj do ulubionych", który wywołuje wywołanie api wymagające autoryzacji. Czy chcę dodać "odświeżenie tokena, a następnie wykonać połączenie" do tej akcji? A co z innymi podobnymi działaniami? Właśnie dlatego próbuję używać oprogramowania pośredniego. W innych frameworkach/językach użyłem dekoratorów, ale nie jestem pewien, czy mogę to zrobić w React. – lanan

2

Zrobiłem prosty wrapper wokół redux-api-middleware, aby odłożyć akcje i odświeżyć token dostępu.

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

Trzymam tokeny w stanie, i używać prostych pomocnika wstrzyknąć Acess żeton do nagłówków żądania

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

Więc redux-api-middleware działania pozostaje prawie niezmieniona

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

Napisałem article i udostępniłem project example, który pokazuje przepływ pracy tokena odświeżania JWT w akcji

Powiązane problemy