On React Router, how to stay logged in state even

2019-01-29 22:40发布

问题:

I'm making the website with React and React Router with Redux. Lots of routes(pages) requires logged in. I can redirect to login if user is not logged in like this:

function requireAuth(nextState, replace) {
    let loggedIn = store.getState().AppReducer.UserReducer.loggedIn;

    if(!loggedIn) {
        replace({
            pathname: '/login',
            state: {
                nextpathname: nextState.location.pathname
            }
        });
    }
}

ReactDOM.render(
    <Provider store={store}>
        <Router history={history}>
            <Route path="/" component={App}>
                <IndexRoute component={Index} />
                <Route path="login" component={Login} />
                <Route path="register" component={Register} />
                <Route path="dashboard" component={Graph} onEnter={requireAuth}>
                    ... some other route requires logged in ...
                </Route>
            </Route>
        </Router>
    </Provider>,
    document.getElementById('entry')
);

Please see the code, I used onEnter hook to redirect to '/login' route if user is not logged in. Data for checking user logged in is in the store, and it will update after user logged in.

It's working perfectly, but problem is when I refresh the page, store will be reset and user is not logged in state back.

I know this happens because Redux store is just memory storage, so refesh page will loose all data.

Check the server session on every refresh can be work but this might be too much request, so that looks bad idea.

Save the logged in state data into localStorage might be work, but in this case, I should check every AJAX calls fail that request rejected because session is expired or not exists like something, and it looks like bad idea either.

Is there a way to solve this problem more clearly? My website will gonna use lots of people, so I want to reduce XHR calls as possible.

Any advice will be very appreciated.

回答1:

Another way to go is to use JSON Web Tokens (JWT) that are required for each route, and localStorage to check for the JWT.

TL;DR

  • On the front end you have a signin and signup route that queries your server for a JWT according to the authentication on the server. Once passed the appropriate JWT you would then set a property of state to true. You can have a signout route that allows the user to set this state to false.

  • The index.js which contains your routes can check local storage before rendering, thus eliminating your problem with losing the state on refresh but keeping some security.

  • All routes requiring authentication in your application are rendered through a Composed Component, and secured with the necessity of having JWTs in the header for authorization on the server API.

Setting this up takes a little time but it will make your application 'reasonably' secure.


To solve your problem:

Check the local storage before the routes in your index.js file as shown below, updating the state to authenticated if required.

The application maintains security with the fact that the API is secured by the JWT which would solve your refresh issue, and maintain a secure link to your server and data.

Thus in the routes you would have something like this:

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware, compose } from 'redux';
import { Router, Route, browserHistory, IndexRoute } from 'react-router';
import reduxThunk from 'redux-thunk';
import { AUTHENTICATE_THE_USER } from './actions/types';
import RequireAuth from './components/auth/require_auth';
import reducers from './reducers';

/* ...import necessary components */

const createStoreWithMiddleware = compose(applyMiddleware(reduxThunk))(createStore);

const store = createStoreWithMiddleware(reducers);

/* ... */

// Check for token and update application state if required
const token = localStorage.getItem('token');
if (token) {
    store.dispatch({ type: AUTHENTICATE_THE_USER });
}

/* ... */

ReactDOM.render(
  <Provider store={store}>
    <Router history={history}>
      <Route path="/" component={App}>
        <IndexRoute component={Index} />
        <Route path="login" component={Login} />
        <Route path="register" component={Register} />
        <Route path="dashboard" component={RequireAuth{Graph}} />
        <Route path="isauthenticated" component={RequireAuth(IsAuthenticated)} />
        ... some other route requires logged in ...
      </Route>
    </Router>
  </Provider>
  , .getElementById('entry'));

RequiredAuth is the composed component while Graph and IsAuthenticated (can be any number of appropriately named components) require the state.authenticated to be true.

The Components, in this case Graph and IsAuthenticated rendered if the state.authenticated is true. Otherwise is defaults back to the root route.


Then you could build a Composed Component like this, through which all your routes are rendered. It will check that the state in which you are holding whether or not the user is authenticated (a boolean) is true before rendering.

require_auth.js

import React, { Component } from 'react';
import { connect } from 'react-redux';

export default function (ComposedComponent) {

  // If user not authenticated render out to root

  class Authentication extends Component {
    static contextTypes = {
      router: React.PropTypes.object
    };

    componentWillMount() {
      if (!this.props.authenticated) {
        this.context.router.push('/');
      }
    }

    componentWillUpdate(nextProps) {
      if (!nextProps.authenticated) {
        this.context.router.push('/');
      }
    }

    render() {
      return <ComposedComponent {...this.props} />;
    }
  }

  function mapStateToProps(state) {
    return { authenticated: state.authenticated };
  }

  return connect(mapStateToProps)(Authentication);
}

On the signup/signin side you could create an action that stores the JWT and sets up the state to authenticated through an action-creator -> redux store. This example makes use of axios to run the async HTTP request response cycle.

export function signinUser({ email, password }) {

  // Note using the npm package 'redux-thunk'
  // giving direct access to the dispatch method
  return function (dispatch) {

    // Submit email and password to server
    axios.post(`${API_URL}/signin`, { email, password })
      .then(response => {
        // If request is good update state - user is authenticated
        dispatch({ type: AUTHENTICATE_THE_USER });

        // - Save the JWT in localStorage
        localStorage.setItem('token', response.data.token);

        // - redirect to the route '/isauthenticated'
        browserHistory.push('/isauthenticated');
      })
      .catch(() => {
        // If request is bad show an error to the user
        dispatch(authenticationError('Incorrect email or password!'));
      });
  };
} 

You would also need to set up your store (Redux in this case) and action creator of course.

The 'real' security comes from the back end. And to do this you use localStorage to keep the JWT on the front end and pass it in the header to any API calls that have sensitive/protected information.

Creating and parsing the JWT for users on the server API is another step. I have found passport to be effective.



回答2:

Why not using sessionStorage with the logged in state and expiration date? You will have to write more code for checking the sessionStorage state but that's the only way in my opinion you can save the XHR call from being sent.