Redux中利用函數式組件替代onEnter進行登陸狀態驗證的實踐

最近使用React和Redux構建一個後臺項目,在作登陸系統的時候,看了網上不少資料,通常都是使用sessionStorage(包括Cookie,下略)或者localStorage保存從服務器獲取的token,而後使用react-router中onEnter這個方法依據sessionStorage或者localStorage中是否存在相應的token來斷定登陸狀態。react

Cookie, LocalStorage 與 SessionStorage的詳解能夠參考:詳說 Cookie, LocalStorage 與 SessionStorage一文。git

react-router的onEnter方法使用能夠參考react-router官方的用例:auth-flow。
倉庫地址:https://github.com/reactjs/re...github

auth-flow這個用例使用localStorage來保存token,react-router的onEnter調用requireAuth方法來判斷auth.loggedIn()是否能正確返回localStorage.token,來維持登陸狀態,這也是目前經常使用的作法。redux

app.jssegmentfault

import React from 'react'
import { render } from 'react-dom'
import { browserHistory, Router, Route, Link, withRouter } from 'react-router'
import auth from './auth'

const App = React.createClass({
  getInitialState() {
    return {
      loggedIn: auth.loggedIn()
    }
  },

  updateAuth(loggedIn) {
    this.setState({
      loggedIn: loggedIn
    })
  },

  componentWillMount() {
    auth.onChange = this.updateAuth
    auth.login()
  },

  render() {
    return (
      <div>
        <ul>
          <li>
            {this.state.loggedIn ? (
              <Link to="/logout">Log out</Link>
            ) : (
              <Link to="/login">Sign in</Link>
            )}
          </li>
          <li><Link to="/about">About</Link></li>
          <li><Link to="/dashboard">Dashboard</Link> (authenticated)</li>
        </ul>
        {this.props.children || <p>You are {!this.state.loggedIn && 'not'} logged in.</p>}
      </div>
    )
  }
})

const Dashboard = React.createClass({
  render() {
    const token = auth.getToken()

    return (
      <div>
        <h1>Dashboard</h1>
        <p>You made it!</p>
        <p>{token}</p>
      </div>
    )
  }
})

const Login = withRouter(
  React.createClass({

    getInitialState() {
      return {
        error: false
      }
    },

    handleSubmit(event) {
      event.preventDefault()

      const email = this.refs.email.value
      const pass = this.refs.pass.value

      auth.login(email, pass, (loggedIn) => {
        if (!loggedIn)
          return this.setState({ error: true })

        const { location } = this.props

        if (location.state && location.state.nextPathname) {
          this.props.router.replace(location.state.nextPathname)
        } else {
          this.props.router.replace('/')
        }
      })
    },

    render() {
      return (
        <form onSubmit={this.handleSubmit}>
          <label><input ref="email" placeholder="email" defaultValue="joe@example.com" /></label>
          <label><input ref="pass" placeholder="password" /></label> (hint: password1)<br />
          <button type="submit">login</button>
          {this.state.error && (
            <p>Bad login information</p>
          )}
        </form>
      )
    }
  })
)

const About = React.createClass({
  render() {
    return <h1>About</h1>
  }
})

const Logout = React.createClass({
  componentDidMount() {
    auth.logout()
  },

  render() {
    return <p>You are now logged out</p>
  }
})

function requireAuth(nextState, replace) {
  if (!auth.loggedIn()) {
    replace({
      pathname: '/login',
      state: { nextPathname: nextState.location.pathname }
    })
  }
}

render((
  <Router history={browserHistory}>
    <Route path="/" component={App}>
      <Route path="login" component={Login} />
      <Route path="logout" component={Logout} />
      <Route path="about" component={About} />
      <Route path="dashboard" component={Dashboard} onEnter={requireAuth} />
    </Route>
  </Router>
), document.getElementById('example'))

auth.js瀏覽器

module.exports = {
  login(email, pass, cb) {
    cb = arguments[arguments.length - 1]
    if (localStorage.token) {
      if (cb) cb(true)
      this.onChange(true)
      return
    }
    pretendRequest(email, pass, (res) => {
      if (res.authenticated) {
        localStorage.token = res.token
        if (cb) cb(true)
        this.onChange(true)
      } else {
        if (cb) cb(false)
        this.onChange(false)
      }
    })
  },

  getToken() {
    return localStorage.token
  },

  logout(cb) {
    delete localStorage.token
    if (cb) cb()
    this.onChange(false)
  },

  loggedIn() {
    return !!localStorage.token
  },

  onChange() {}
}

function pretendRequest(email, pass, cb) {
  setTimeout(() => {
    if (email === 'joe@example.com' && pass === 'password1') {
      cb({
        authenticated: true,
        token: Math.random().toString(36).substring(7)
      })
    } else {
      cb({ authenticated: false })
    }
  }, 0)
}

localStorage等本地存儲容器保存一些用戶信息,多少可能會有潛在的風險,那麼可不能夠不使用這些本地存儲來維持用戶狀態呢?服務器

因而我嘗試用redux結合react-router來保持用戶的登陸狀態,最開始的思路是用onEnter調用一個方法來獲取store裏的登陸狀態信息,可是發現react-router的路由聲明中並不能從store中拿到props,只有路由的history等信息。可能水平有限,只能處處翻文檔,無心間在Github中發現一個用例:react-redux-jwt-auth-examplesession

這個用例使用了一個高級函數(high-order function,用例中爲requireAuthentication)來包裝須要登陸權限的Compenent(用例中爲ProtectedView),這個Compenent位於全部須要登陸權限的頂層:react-router

routers.jsapp

import {HomeView, LoginView, ProtectedView, AView, BView } from '../views';
import {requireAuthentication} from '../components/AuthenticatedComponent';

export default(
    <Route path='/' component={App}>
        <IndexRoute component={HomeView}/>
        <Route path="login" component={LoginView}/>
        <Route path="protected" component={requireAuthentication(ProtectedView)}
            <Route path="a" component={AView}/>
            <Route path="b" component={BVieew}/>
        </Route>
    </Route>
);

利用requireAuthentication()這個高階函數將ProtectedView這個Compenent做爲參數傳人,requireAuthentication()中生成一個Compenent,而後調用react-redux中的connect結合mapStateToProps就能將store中的登陸狀態,token等信息塞入Props中,當前這個requireAuthentication中的Compenent根據Props中的狀態信息來決定是否繼續渲染ProtectedView Compenent,或者在用戶進行頁面跳轉,檢測到登陸狀態爲false時,就會重定向到登陸頁面。

AuthenticatedComponent.js

import React from 'react';
import {connect} from 'react-redux';
import {pushState} from 'redux-router';

export function requireAuthentication(Component) {

    class AuthenticatedComponent extends React.Component {

        componentWillMount() {
            this.checkAuth();
        }

        componentWillReceiveProps(nextProps) {
            this.checkAuth();
        }

        checkAuth() {
            if (!this.props.isAuthenticated) {
                let redirectAfterLogin = this.props.location.pathname;
                this.props.dispatch(pushState(null, `/login?next=${redirectAfterLogin}`));
            }
        }

        render() {
            return (
                <div>
                    {this.props.isAuthenticated === true
                        ? <Component {...this.props}/>
                        : null
                    }
                </div>
            )

        }
    }

    const mapStateToProps = (state) => ({
        token: state.auth.token,
        userName: state.auth.userName,
        isAuthenticated: state.auth.isAuthenticated
    });

    return connect(mapStateToProps)(AuthenticatedComponent);

}

上面是做者給的用法,其中須要注意的是:

import {pushState} from 'redux-router';

因爲幾個react-router的區別這個問題,打包編譯後可能報錯,我項目中使用的是react-router-redux。

參考react-router-redux文檔中What if I want to issue navigation events via Redux actions?

使用方法是在AuthenticatedComponent中:

import {push} react-router-redux

也就是push代替了原例子中的pushState,做用都差很少。

而後就是加一箇中間件:

import { routerMiddleware, push } from 'react-router-redux'

// Apply the middleware to the store
const middleware = routerMiddleware(browserHistory)
const store = createStore(
  reducers,
  applyMiddleware(middleware)
)

這樣就能夠在AuthenticatedComponent中愉快地重定向了。

以上利用react-redux,redux-router || react-router-redux基於redux來保持登陸狀態和進行登陸權限驗證,能夠避免使用Cookie&localStroge等來保存登陸信息,其中的缺點就是,用戶刷新頁面或關閉瀏覽器後,登陸狀態就被銷燬了,若是有記住用戶名等需求,可能依然會用到本地存儲容器。

翻閱這個用例最後還發現做者已經寫了一個登陸權限驗證的library:
redux-auth-wrapper

不想搬運代碼的兄弟能夠參考文檔直接拿來用~

第一次寫文章,若是有概念錯誤的地方,請多多指正!!感謝!!

相關文章
相關標籤/搜索