转载

利用redux和react-redux去Cookie&localStroge保持登录状态的实践

最近使用React和Redux构建一个后台项目,在做登录系统的时候,看了网上很多资料,一般都是使用sessionStorage(包括Cookie,下略)或者localStroge保存从服务器获取的token,然后使用react-router中 onEnter 这个方法依据sessionStorage或者localStroge中是否存在相应的token来判定登录状态。

Cookie, LocalStorage 与 SessionStorage的详解可以参考: 详说 Cookie, LocalStorage 与 SessionStorage 一文。

react-router的onEnter方法使用可以参考react-router官方的用例:auth-flow。

仓库地址: https://github.com/reactjs/react-router/tree/master/examples/auth-flow

auth-flow这个用例使用localStorage来保存token,react-router的onEnter调用requireAuth方法来判断auth.loggedIn()是否能正确返回localStorage.token,来维持登录状态,这也是目前常用的做法。

app.js

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

Web安全方面,我并没有深入研究,但据说localStroge等本地存储容器保存一些用户信息,多少可能会存在XSS注入的风险,那么可不可以不使用这些本地存储来维持用户状态呢?

于是我尝试用redux结合react-router来保持用户的登录状态,最开始的思路是用onEnter调用一个方法来获取store里的登录状态信息,但是发现react-router的路由声明中并不能从store中拿到props,只有路由的history等信息。可能水平有限,只能到处翻文档,无意间在Github中发现一个用例: react-redux-jwt-auth-example

这个用例使用了一个高级函数(high-order function,用例中为requireAuthentication)来包装需要登录权限的Compenent(用例中为ProtectedView),这个Compenent位于所有需要登录权限的顶层:

routers.js

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

不想搬运代码的兄弟可以参考文档直接拿来用~

第一次写文章,如果有概念错误的地方,请多多指正!!感谢!!

原文  https://segmentfault.com/a/1190000005607817
正文到此结束
Loading...