构建具有用户身份认证的 React + Flux 应用程序

序言:这是一篇内容详实的 React + Flux 教程,文章主要介绍了如何使用 API 获取远程数据以及如何使用 JSON Web Tokens 进行用户身份认证。在阅读本文之后,我一直使用文章介绍的方法,通过搭建 Node 服务器,模拟接口数据进行前端开发。这篇文章发表于 2016 年 5 月,我是去年读的本文,但迟迟没有翻译,而现在准备重新学习 React ,所以把这篇文章翻出来与大家共勉。

原文:Build a React + Flux App with User Authentication

译者:nzbin

React 的生态系统很大,为了解决 React 中比较困难的问题,你可以选择多种模块。大多数实际的 React 应用程序都有一些共同的需求,这些需求主要包括状态管理及路由。而解决这些需求最常用的是 Flux 及 React Router。

在 Scotch 上, Ken 有一些关于React 和 Flux 的 awesome series,当然,网上也有很多关于这些话题的教程。但是,在构建一个真实的 React 应用程序时,我们还需要考虑其它一些不经常讨论的事情:如何调用远程 API 以及如何验证用户身份。

在这篇教程中,我们将通过 API 获取数据的方式制作一个简单的通讯录应用。我们会使用 Express (NodeJS)服务器发送数据,需要说明的是并不一定非要使用 Node。只要能输出 JSON 数据,我们可以使用任何服务器。

单页应用中进行用户身份验证的最好方式就是 JSON Web Tokens (JWT) 。从头开始设置 JWT 身份验证非常繁琐,所以我们将使用 Auth0

使用 Auth0,我们只需要放置一个 script 标签就可以立即得到一个 登录框 ,它具有 社交登录多重身份认证 等等。

当我们 注册 Auth0 之后,我们会得到一个免费账户,它提供 7,000 个免费用户以及两个社交认证供应商。最好的一点是这个账户是针对产品就绪的,所以我们可以开发真正的应用程序。

开始吧!

创建一个新的 React 项目

在这篇教程中,我们将使用 React 以及 ES2015,这意味着需要一个编译器才能使用所有特性并兼容所有浏览器。我们会使用 webpack 编译,而使用 React + Webpack 构建一个新项目最简单的方式就是使用 Yeoman 的生成器。

npm install -g yo
npm install -g generator-react-webpack
mkdir react-auth && cd react-auth
yo react-webpack

根据 Yeoman 的提示一步步安装,最后会得到一个搭配 webpack 的 React 新项目。

还需要安装一些 Yeoman 中没有的依赖包,快开始吧。

npm install flux react-router bootstrap react-bootstrap keymirror superagent

为了使用 React Bootstrap,需要对 webpack 配置文件中的 url-loader 稍作调整。

// cfg/defaults.js

...

{
  test: /\.(png|woff|woff2|eot|ttf|svg)$/,
  loader: 'url-loader?limit=8192'
},

...

另外,要改一下 webpack 用于保存项目的路径,否则使用 React Router 会出问题。打开 server.js ,在最底部,将

open('http://localhost:' + config.port + '/webpack-dev-server/');

改成

open('http://localhost:' + config.port);

创建一个 Express 服务器

项目开始之前先创建 Express 服务器,保证 React 应用程序可以获取数据。这个服务器非常简单,只需要几个依赖模块。

mkdir react-auth-server && cd react-auth-server
npm init
npm install express express-jwt cors
touch server.js

安装 express-jwt 包是为了创建用户身份验证的中间件来保护 API 端口。

// server.js

const express = require('express');
const app = express();
const jwt = require('express-jwt');
const cors = require('cors');

app.use(cors());

// Authentication middleware provided by express-jwt.
// This middleware will check incoming requests for a valid
// JWT on any routes that it is applied to.
const authCheck = jwt({
  secret: new Buffer('YOUR_AUTH0_SECRET', 'base64'),
  audience: 'YOUR_AUTH0_CLIENT_ID'
});

var contacts = [
  {
    id: 1,
    name: 'Chris Sevilleja',
    email: 'chris@scotch.io',
    image: '//gravatar.com/avatar/8a8bf3a2c952984defbd6bb48304b38e?s=200'
  },
  {
    id: 2,
    name: 'Nick Cerminara',
    email: 'nick@scotch.io',
    image: '//gravatar.com/avatar/5d0008252214234c609144ff3adf62cf?s=200'
  },
  {
    id: 3,
    name: 'Ado Kukic',
    email: 'ado@scotch.io',
    image: '//gravatar.com/avatar/99c4080f412ccf46b9b564db7f482907?s=200'
  },
  {
    id: 4,
    name: 'Holly Lloyd',
    email: 'holly@scotch.io',
    image: '//gravatar.com/avatar/5e074956ee8ba1fea26e30d28c190495?s=200'
  },
  {
    id: 5,
    name: 'Ryan Chenkie',
    email: 'ryan@scotch.io',
    image: '//gravatar.com/avatar/7f4ec37467f2f7db6fffc7b4d2cc8dc2?s=200'
  }
];

app.get('/api/contacts', (req, res) => {
  const allContacts = contacts.map(contact => { 
    return { id: contact.id, name: contact.name}
  });
  res.json(allContacts);
});

app.get('/api/contacts/:id', authCheck, (req, res) => {
  res.json(contacts.filter(contact => contact.id === parseInt(req.params.id)));
});

app.listen(3001);
console.log('Listening on http://localhost:3001');

我们得到了从两个端口返回的联系人数据数组。在 /api/contacts 端口,我们使用 map 方法获取数组中对象的 idname 字段。而在 /api/contacts/:id 端口,我们通过特殊的 id 字段检索数组并获得对应的对象。为了简单起见,我们只是使用模拟数据。在真实的应用中,这些数据是从服务器返回的。

注册 Auth0

你可能注意到我们在 Express 服务器中定义的 authCheck 。这是应用于 /api/contacts/:id 路由的中间件,它需要从我们这里获取验证信息。很显然,我们需要设置一个密钥,它会对比发送给 API 的解码 JWT 验证合法性。如果使用 Auth0,我们只需要将我们的密钥及用户 ID 提供给中间件。

如果你还没有 注册 Auth0,那现在就去注册一个。在你注册之后,你会在 management area 中找到用户密码及用户 ID。拿到这些关键信息之后,你要把它们放到中间件的合适位置,这样就大功告成了。

你要在 “Allowed Origins” 输入框中输入 localhost 域名及端口,这样 Auth0 才允许从测试域名获取请求。

 

创建 Index 文件和路由

先设置 index.js 文件,我们需要修改 Yeoman 生成器提供的文件。

// src/index.js

import 'core-js/fn/object/assign';
import React from 'react';
import ReactDOM from 'react-dom';
import { browserHistory } from 'react-router';
import Root from './Root';

// Render the main component into the dom
ReactDOM.render(<Root history={browserHistory} />, document.getElementById('app'));

我们渲染了一个名为 Root 的组件,这个组件有一个名为 browserHistory 的属性,渲染到名为 app 的 DOM 节点上。

为了完成路由设置,我们需要创建一个设置路由的 Root.js 文件。

// Root.js

import React, { Component } from 'react';
import { Router, Route, IndexRoute } from 'react-router';
import Index from './components/Index';
import ContactDetail from './components/ContactDetail';

import App from './components/App';

class Root extends Component {

  // We need to provide a list of routes
  // for our app, and in this case we are
  // doing so from a Root component
  render() {
    return (
      <Router history={this.props.history}>
        <Route path='/' component={App}>
          <IndexRoute component={Index}/>
          <Route path='/contact/:id' component={ContactDetail} />
        </Route>
      </Router>
    );
  }
}

export default Root;

通过 React Router ,我们可以使用 Router 包裹私有的 Routes ,然后给它们指定路径及组件。 Router 有一个名为 history 的参数,它可以解析 URL 并构建路径对象。之前我们在index.js 文件中也传递了一个 history 属性。

现在我们还应该添加 Lock 组件。可以使用 npm 安装,然后通过 webpack 构建的方式添加,或者作为 script 标签插入。为了简单一点,我们直接使用一个 script 标签插入。

    <!-- src/index.html --> 
    ...

    <!-- Auth0Lock script -->
    <script src="//cdn.auth0.com/js/lock-9.1.min.js"></script>

    <!-- Setting the right viewport -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />   
      ...

创建 App 组件

我们设置的第一个组件是 App 根组件。将 Main.js 命名为 App.js ,然后从 React Bootstrap 导入组件。

// src/components/App.js

import 'normalize.css/normalize.css';
import 'bootstrap/dist/css/bootstrap.min.css';

import React, { Component } from 'react';
import Header from './Header';
import Sidebar from './Sidebar';
import { Grid, Row, Col } from 'react-bootstrap';

class AppComponent extends Component {

  componentWillMount() {
    this.lock = new Auth0Lock('YOUR_AUTH0_CLIENT_ID', 'YOUR_AUTH0_DOMAIN);
  }

  render() {
    return (
      <div>
        <Header lock={this.lock}></Header>
        <Grid>
          <Row>
            <Col xs={12} md={3}>
              <Sidebar />
            </Col>
            <Col xs={12} md={9}>
              {this.props.children}
            </Col>
          </Row>
        </Grid>
      </div>
    );
  }
}

export default AppComponent;

我们调用了名为 HeaderSidebar 的组件。之后会创建这些组件,但是现在,让我们看一看 componentWillMount  中发生的变化。我们在这里可以设置 Auth0Lock 实例,只需要调用 new Auth0Lock 然后传入用户 ID 以及用户域名。提醒一下,这两项可以在 Auth0 的 management area 中获得。

需要注意的一点是我们在第二个 Col 组件中调用了 {this.props.children} 。这个地方会展示 React Router 中的子路由, 通过这种方式,我们的应用程序会有一个侧边栏及动态视图。

我们已经将 Auth0Lock 实例作为 prop 传递到 Header 中,所以接下来创建 Header。

创建 Header 组件

导航条可以放置用户用来登录及注销应用程序的按钮。

// src/components/Header.js

import React, { Component } from 'react';
import { Nav, Navbar, NavItem, Header, Brand } from 'react-bootstrap';
// import AuthActions from '../actions/AuthActions';
// import AuthStore from '../stores/AuthStore';

class HeaderComponent extends Component {

  constructor() {
    super();
    this.state = {
      authenticated: false
    }
    this.login = this.login.bind(this);
    this.logout = this.logout.bind(this);
  }

  login() {
    // We can call the show method from Auth0Lock,
    // which is passed down as a prop, to allow
    // the user to log in
    this.props.lock.show((err, profile, token) => {
      if (err) {
        alert(err);
        return;
      }
      this.setState({authenticated: true});
    });
  }

  logout() {
    // AuthActions.logUserOut();
    this.setState({authenticated: false});
  }

  render() {
    return (
      <Navbar>
        <Navbar.Header>
          <Navbar.Brand>
            <a href="#">React Contacts</a>
          </Navbar.Brand>
        </Navbar.Header>
        <Nav>
          <NavItem onClick={this.login}>Login</NavItem>
          <NavItem onClick={this.logout}>Logout</NavItem>
        </Nav>
      </Navbar>
    );
  }
}

export default HeaderComponent;

不可否认,我们省略了用户验证的一些细节,因为我们还没有创建 actions 和 stores。但是,现在已经可以看到程序的工作流程。 login 方法可以弹出 Lock 组件,它由 “Login” NavItem 控制。现在我们只是简单的设置 authenticated 的状态为 true 或者 false,但是之后它的状态将由用户的 JWT 决定。

在我们看到屏幕上的东西之前,我们需要先创建 SidebarIndex 组件。

创建 Sidebar 和 Index 组件

// src/components/Sidebar.js

import React, { Component } from 'react';

class SidebarComponent extends Component {
  render() {
    return (
      <h1>Hello from the sidebar</h1>
    );
  }
}

export default SidebarComponent;

最终这个组件会渲染从服务器返回的联系人列表,但是现在只展示一条简单的信息。

我们需要一个 Index 组件作为路由的 IndexRoute 。这个组件只是展示点击的用户信息。

// src/components/Index.js

import React, { Component } from 'react';

class IndexComponent extends Component {

  constructor() {
    super();
  }
  render() {
    return (
      <h2>Click on a contact to view their profile</h2>
    );
  }
}

export default IndexComponent;

现在准备查看我们的应用程序。但是首先我们要删除或者注释掉 Root.js 中的一些内容。我们还没有 ConactDetail 组件,所以先临时删除导入部分及这个组件的 Route

如果一切顺利,我们应该能看到渲染出的应用程序。

当我们点击 Login,应该可以看到 Lock 组件。

使用 Flux

Flux 非常适合状态管理,但是它的缺点就是需要大量代码,这意味着这一部分有些啰嗦。为了尽可能简洁,我们不会详细讨论 Flux 是什么以及如何工作,如果你想深入了解,你可以阅读 Ken 的文章

简单介绍一下 Flux,它是一种帮助我们处理应用程序中单向数据流的结构。当应用程序变得庞大时,拥有一个单向流动的数据结构非常重要,因为相比混乱的双向数据流更容易理解。

为了做到这一点,Flux 需要 actions, dispatcher 以及 stores

创建 Dispatcher

先创建一个 dispatcher 。

// src/dispatcher/AppDispatcher.js

import { Dispatcher } from 'flux';

const AppDispatcher = new Dispatcher();

export default AppDispatcher;

在 React + Flux 应用中只有一个 dispatcher,可以通过调用 new Dispatcher() 创建。

创建 Actions

接下来,我们创建 actions 检索从 API 获取的联系人数据。

// src/actions/ContactActions.js

import AppDispatcher from '../dispatcher/AppDispatcher';
import ContactConstants from '../constants/ContactConstants';
import ContactsAPI from '../utils/ContactsAPI';

export default {

  recieveContacts: () => {
    ContactsAPI
      .getContacts('http://localhost:3001/api/contacts')
      .then(contacts => {
        AppDispatcher.dispatch({
          actionType: ContactConstants.RECIEVE_CONTACTS,
          contacts: contacts
        });
      })
      .catch(message => {
        AppDispatcher.dispatch({
          actionType: ContactConstants.RECIEVE_CONTACTS_ERROR,
          message: message
        });
      });
  },

  getContact: (id) => {
    ContactsAPI
      .getContact('http://localhost:3001/api/contacts/' + id)
      .then(contact => {
        AppDispatcher.dispatch({
          actionType: ContactConstants.RECIEVE_CONTACT,
          contact: contact
        });
      })
      .catch(message => {
        AppDispatcher.dispatch({
          actionType: ContactConstants.RECIEVE_CONTACT_ERROR,
          message: message
        });
      });
  }

}

在一个 Flux 架构中,actions 需要 dispatch 一个 action type 和一个 payload 。payload 通常是与 action 有关的数据,而这些数据是从相关联的 store 中获得。

人们对于是否在应该在 actions 中调用 API 等操作有不同的看法,有些人认为应该保存在 stores 中。最终,你选择的方式取决于它是否适合你的应用程序,在 actions 中调用 API 是处理远程数据比较好的方式。

该组件依赖还没有创建的 ContactsAPIContactConstants ,所以现在就开始创建吧。

创建 Contact Constants

// src/constants/ContactConstants.js

import keyMirror from 'keymirror';

export default keyMirror({
  RECIEVE_CONTACT: null,
  RECIEVE_CONTACTS: null,
  RECIEVE_CONTACT_ERROR: null,
  RECIEVE_CONTACTS_ERROR: null
});

Constants 可以识别 action 的类型,可以同步 actions 及 stores,之后会看到。我们使用 keyMirror 来保证 constants 的值可以匹配键值。

创建 Contacts API

我们已经从 ContactActions 组件中简单了解了 ContactsAPI 的功能。我们想创建一些向服务器端发送 XHR 请求的方法,用于接收数据并处理返回的 Promise 。对于 XHR 请求,我们将使用 superagent ,它是封装 XHR 比较好的一个库并且提供了处理 HTTP 请求的简单方法。

// src/utils/ContactsAPI.js

import request from 'superagent/lib/client';

export default {

  // We want to get a list of all the contacts
  // from the API. This list contains reduced info
  // and will be be used in the sidebar
  getContacts: (url) => {
    return new Promise((resolve, reject) => {
      request
        .get(url)
        .end((err, response) => {
          if (err) reject(err);
          resolve(JSON.parse(response.text));
        })
    });
  },

  getContact: (url) => {
    return new Promise((resolve, reject) => {
      request
        .get(url)
        .end((err, response) => {
          if (err) reject(err);
          resolve(JSON.parse(response.text));
        })
    });
  }
}

通过 superagent,我们可以调用 get 方法发送 GET 请求。在 end 方法中有一个处理错误或者响应的回调函数,我们可以用这些方法做任何事情。

如果我们在请求中遇到任何错误, 我们可以 reject (排除)错误。排除操作在 actions 的 catch 方法中。另外,我们可以 resolve (处理)从 API 获取的数据。

创建 Contact Store

在我们将通讯录数据渲染到屏幕上之前,我们需要创建 store 。

// src/stores/ContactStore.js

import AppDispatcher from '../dispatcher/AppDispatcher';
import ContactConstants from '../constants/ContactConstants';
import { EventEmitter } from 'events';

const CHANGE_EVENT = 'change';

let _contacts = [];
let _contact = {};

function setContacts(contacts) {
  _contacts = contacts;
}

function setContact(contact) {
  _contact = contact;
}

class ContactStoreClass extends EventEmitter {

  emitChange() {
    this.emit(CHANGE_EVENT);
  }

  addChangeListener(callback) {
    this.on(CHANGE_EVENT, callback)
  }

  removeChangeListener(callback) {
    this.removeListener(CHANGE_EVENT, callback)
  }

  getContacts() {
    return _contacts;
  }

  getContact() {
    return _contact;
  }

}

const ContactStore = new ContactStoreClass();

// Here we register a callback for the dispatcher
// and look for our various action types so we can
// respond appropriately
ContactStore.dispatchToken = AppDispatcher.register(action => {

  switch(action.actionType) {
    case ContactConstants.RECIEVE_CONTACTS:
      setContacts(action.contacts);
      // We need to call emitChange so the event listener
      // knows that a change has been made
      ContactStore.emitChange();
      break

    case ContactConstants.RECIEVE_CONTACT:
      setContact(action.contact);
      ContactStore.emitChange();
      break

    case ContactConstants.RECIEVE_CONTACT_ERROR:
      alert(action.message);
      ContactStore.emitChange();
      break

    case ContactConstants.RECIEVE_CONTACTS_ERROR:
      alert(action.message);
      ContactStore.emitChange();
      break

    default:
  }

});

export default ContactStore;

和大多数 stores 的功能一样,我们在 AppDispatcher 注册了一个 switch 状态,可以响应程序中派发的各种 actions 。当 RECIEVE_CONTACTS action 被派发的时候,意味着我们正在从 API 获取联系人数据,而且我们想将联系人数据转成数组。这个功能由 setContacts 函数实现,之后通知 EventListener 发生变化,这样应用程序就知道发生了变化。

我们已经有了获取单个联系人或者整个列表的逻辑,这些方法会用在组件中。

在看到通讯录之前,我们需要创建几个组件来专门处理我们的列表。

创建 Contacts 组件

 Contacts 组件将用于在侧边栏中展示联系人列表。我们将在列表中设置 Link 链接,稍后详细说明。

// src/components/Contacts.js

import React, { Component } from 'react';
import { ListGroup } from 'react-bootstrap';
// import { Link } from 'react-router';
import ContactActions from '../actions/ContactActions';
import ContactStore from '../stores/ContactStore';
import ContactListItem from './ContactListItem';

// We'll use this function to get a contact
// list item for each of the contacts in our list
function getContactListItem(contact) {
  return (
    <ContactListItem
      key={contact.id}
      contact={contact}
    />
  );
}
class ContactsComponent extends Component {

  constructor() {
    super();
    // For our initial state, we just want
    // an empty array of contacts
    this.state = {
      contacts: []
    }
    // We need to bind this to onChange so we can have
    // the proper this reference inside the method
    this.onChange = this.onChange.bind(this);
  }

  componentWillMount() {
    ContactStore.addChangeListener(this.onChange);
  }

  componentDidMount() {
    ContactActions.recieveContacts();
  }

  componentWillUnmount() {
    ContactStore.removeChangeListener(this.onChange);
  }

  onChange() {
    this.setState({
      contacts: ContactStore.getContacts()
    });
  }

  render() {
    let contactListItems;
    if (this.state.contacts) {
      // Map over the contacts and get an element for each of them
      contactListItems = this.state.contacts.map(contact => getContactListItem(contact));
    }
    return (
      <div>
        <ListGroup>
          {contactListItems}
        </ListGroup>
      </div>
    );
  }
}

export default ContactsComponent;

我们需要有一个初始状态, 如果使用 ES2015,可以在 constructor 中设置 this.state 。我们给 onChange 方法绑定了 this ,所以在方法中我们可以获得正确的 this 上下文环境。 在组件方法中像 this.setState 这样处理其它操作非常重要。

当组件加载后,我们通过直接调用 ContactActions.recieveContacts action 来请求原始列表。这会向服务器发送一个 XHR (和在 ContactsAPI 定义的一样) 并触发 ContactStore 来处理数据。 我们需要在 componentWillMount 生命周期方法中添加一个 change 监听器,它将 onChange 方法作为回调函数。 onChange 方法负责设置 store 中当前联系人列表的状态。

我们使用 map 方法循环设置了状态的 contacts 数据,为每一项都创建一个列表项,这样可以很好的使用 ListGroup (React Bootstrap 的组件)展示。所以我们需要另外一个名为 ContactListItem 的组件,快开始吧。

创建 Contact List Item 组件

 ContactListItem 组件会创建一个带有 React Router LinkListGroupItem (另一个 React Bootstrap 组件) ,它最终会展示联系人的详细信息。

// src/components/ContactListItem.js

import React, { Component } from 'react';
import { ListGroupItem } from 'react-bootstrap';
import { Link } from 'react-router';

class ContactListItem extends Component {
  render() {
    const { contact } = this.props;
    return (
      <ListGroupItem>
        <Link to={`/contact/${contact.id}`}>          
          <h4>{contact.name}</h4>
        </Link>
      </ListGroupItem>
    );
  }
}

export default ContactListItem;

我们通过 prop 获取 contact ,然后很容易地渲染出 name 属性。

修改 Sidebar

在预览应用之前做最后一次调整,就是修改 Sidebar ,这样一来之前的信息就会被替换成联系人列表。

// src/components/Sidebar.js

import React, { Component } from 'react';
import Contacts from './Contacts';

class SidebarComponent extends Component {
  render() {
    return (
      <Contacts />
    );
  }
}

export default SidebarComponent;

完成这一步,我们就可以查看联系人列表了。

 

创建 Contact Detail 组件

应用程序的最后一部分是联系人详情区域,它占据页面的主要部分。当点击联系人姓名时,会向服务器端发送请求,然后接收联系人信息并显示出来。

你已经注意到,在我们设置 Express 应用时,一开始我们就向 /contacts/:id 路由申请 JWT 中间件 (authCheck) ,这就意味着只有获得有效的 JWT,我们才能获取资源。也许这并不是你的应用程序的真实场景, 但是在这个例子中,限制用户信息很好的演示了需要认证的应用程序是如何工作的。

我们已经有了处理单个联系人的 action 和 store,所以让我们开始编写组件。

// src/components/ContactDetail.js

import React, { Component } from 'react';
import ContactActions from '../actions/ContactActions';
import ContactStore from '../stores/ContactStore';

class ContactDetailComponent extends Component {

  constructor() {
    super();
    this.state = {
      contact: {}
    }
    this.onChange = this.onChange.bind(this);
  }

  componentWillMount() {
    ContactStore.addChangeListener(this.onChange);
  }

  componentDidMount() {
    ContactActions.getContact(this.props.params.id);
  }

  componentWillUnmount() {
    ContactStore.removeChangeListener(this.onChange);
  }

  componentWillReceiveProps(nextProps) {
    this.setState({
      contact: ContactActions.getContact(nextProps.params.id)
    });
  }

  onChange() {
    this.setState({
      contact: ContactStore.getContact(this.props.params.id)
    });
  }

  render() {
    let contact;
    if (this.state.contact) {
      contact = this.state.contact;
    }
    return (
      <div>
        { this.state.contact &&
          <div>
            <img src={contact.image} width="150" />
            <h1>{contact.name}</h1>
            <h3>{contact.email}</h3>
          </div>
        }
      </div>
    );
  }
}

export default ContactDetailComponent;

这个组件看上去和 Contacts 组件很像,但是它只处理单个联系人对象。注意我们向 ContactActionsContactStore 组件的 getContact 方法传递了一个 id 参数。这个 id 来自于 React Router,由 params 提供。当我们在列表中的联系人之间切换时,或者换句话说,当我们想查看“下一个”联系人时, componentWillReceiveProps 方法用于提取 params 中的 id

回顾 Contact Detail 路由

在预览这个组件之前,我们回顾 Root.js 文件中的 ContactDetail 路由。

/ src/Root.js

...

render() {
    return (
      <Router history={this.props.history}>
        <Route path='/' component={App}>
          <IndexRoute component={Index}/>
          <Route path='/contact/:id' component={ContactDetail} />
        </Route>
      </Router>
    );
  }

  ...

现在我们可以点击联系人查看详情,但是无权访问。

这个无权访问的错误是因为服务器端的中间件在保护联系人的详情资源。服务器需要一个有效的 JWT 才允许请求。为了做到这一点,我们首先需要对用户进行身份验证。让我们完成验证部分。

完成用户身份认证

当用户使用 Auth0 登录后会发生什么? 回调函数会返回很多内容,其中最重要的是 id_token ,它是一个 JWT 。其它内容还包括用户配置文件, access token,  refresh token  等等。

好消息是, 由于大部分的工作在 Auth0 的沙盒中完成,所以我们已经完成了身份认证。我们需要做的认证部分就是提供处理用户信息数据的逻辑以及成功登陆后返回的 JWT。

我们将遵循 Flux 的架构,为认证创建一系列的 actions, constants 以及 store 。

创建 AuthActions

// src/actions/AuthActions.js

import AppDispatcher from '../dispatcher/AppDispatcher';
import AuthConstants from '../constants/AuthConstants';

export default {

  logUserIn: (profile, token) => {
    AppDispatcher.dispatch({
      actionType: AuthConstants.LOGIN_USER,
      profile: profile,
      token: token
    });
  },

  logUserOut: () => {
    AppDispatcher.dispatch({
      actionType: AuthConstants.LOGOUT_USER
    });
  }

}

以上设置和 ContactActions 组件类似,但在这里,我们关注用户的登录和注销。在 logUserIn 方法中,当我们调用 action 的时候,我们分发了来自 Header 组件的用户信息和 token

创建 Auth Constants

我们的用户身份认证需要一些新的 constants (静态变量)

// src/constants/AuthConstants.js

import keyMirror from 'keymirror';

export default keyMirror({
  LOGIN_USER: null,
  LOGOUT_USER: null
});

创建 Auth Store

 AuthStore 是成功登陆后处理用户信息 及 JWT 的组件。那么我们到底需要做些什么呢?处理用户信息和 token 最简单的方式就是把它们保存在 local storage 中,这样它们在之后可以被重新利用。

// src/stores/AuthStore.js

import AppDispatcher from '../dispatcher/AppDispatcher';
import AuthConstants from '../constants/AuthConstants';
import { EventEmitter } from 'events';

const CHANGE_EVENT = 'change';

function setUser(profile, token) {
  if (!localStorage.getItem('id_token')) {
    localStorage.setItem('profile', JSON.stringify(profile));
    localStorage.setItem('id_token', token);
  }
}

function removeUser() {
  localStorage.removeItem('profile');
  localStorage.removeItem('id_token');
}

class AuthStoreClass extends EventEmitter {
  emitChange() {
    this.emit(CHANGE_EVENT);
  }

  addChangeListener(callback) {
    this.on(CHANGE_EVENT, callback)
  }

  removeChangeListener(callback) {
    this.removeListener(CHANGE_EVENT, callback)
  }

  isAuthenticated() {
    if (localStorage.getItem('id_token')) {
      return true;
    }
    return false;
  }

  getUser() {
    return localStorage.getItem('profile');
  }

  getJwt() {
    return localStorage.getItem('id_token');
  }
}

const AuthStore = new AuthStoreClass();

// Here we register a callback for the dispatcher
// and look for our various action types so we can
// respond appropriately
AuthStore.dispatchToken = AppDispatcher.register(action => {

  switch(action.actionType) {

    case AuthConstants.LOGIN_USER:
      setUser(action.profile, action.token);
      AuthStore.emitChange();
      break

    case AuthConstants.LOGOUT_USER:
      removeUser();
      AuthStore.emitChange();
      break

    default:
  }

});

export default AuthStore;

 setUser 是登陆成功之后使用的函数, 它的功能是将用户信息和 token 保存在 local storage 中。我们在组件中也写了一些有助于我们的工具类方法。其中 isAuthenticated 方法可以根据用户是否登录来隐藏或显示一些元素。

但是让我们再考虑一下。在传统的身份认证设置中,当用户成功登录时,服务器会生成一个 session ,这个 session 稍后用于检查用户是否经过身份认证。然而,JWT 认证是无状态的,它的工作原理是通过服务器去检查请求中的 token 令牌是否与密钥匹配。没有会话或也没有必要的状态。 出于很多原因 ,这是一种很好的方式,但是在我们的前端应用中应该如何验证用户的身份。

好消息是,我们真正需要做的是检查令牌是否保存在本地存储中。如果令牌无效,则请求将被拒绝,用户将需要重新登录。我们可以进一步检查令牌是否已经过期,但是现在只需要检查 JWT 是否存在。

修改 Header 组件

让我们赶快修改 header 组件,这样它就可以使用 AuthActions 以及 AuthStore 来分发正确的 actions 。

// src/components/Header.js

...

import AuthActions from '../actions/AuthActions';
import AuthStore from '../stores/AuthStore';

class HeaderComponent extends Component {

  ...

  login() {
    this.props.lock.show((err, profile, token) => {
      if (err) {
        alert(err);
        return;
      }
      AuthActions.logUserIn(profile, token);
      this.setState({authenticated: true});
    });
  }

  logout() {
    AuthActions.logUserOut();
    this.setState({authenticated: false});
  }

  ...

正确修改文件之后,如果用户已经登录,用户信息及 JWT 会被保存。

发送身份认证请求

联系人详情资源受 JWT 身份认证的保护,现在我们为用户添加了有效的 JWT 。我们还需要在发送请求时将令牌添加到 Authorization header 中。通过 superagent,很容易在请求中设置。

// src/utils/ContactsAPI.js

import AuthStore from '../stores/AuthStore';

...

  getContact: (url) => {
    return new Promise((resolve, reject) => {
      request
        .get(url)
        .set('Authorization', 'Bearer ' + AuthStore.getJwt())
        .end((err, response) => {
          if (err) reject(err);
          resolve(JSON.parse(response.text));
        })
    });
  }
}

我们在 Authorization header 中添加了 Bearer scheme 以及从 store 中获取的 JWT 。做完这一步,我们就可以访问受保护的内容了。

最后:根据条件显示和隐藏元素

我们的应用程序已经做的差不多了!最后,让我们根据条件展示和隐藏一些元素。 我们将在用户未验证时显示“Login”导航项,而验证之后将其隐藏起来。 “Logout”导航项正好相反。

// src/components/Header.js

...

constructor() {
    super();
    this.state = {
      authenticated: AuthStore.isAuthenticated()
    }
    ...
  }

  ...

  render() {
    return (
      <Navbar>
        <Navbar.Header>
          <Navbar.Brand>
            <a href="#">React Contacts</a>
          </Navbar.Brand>
        </Navbar.Header>
        <Nav>
          { !this.state.authenticated ? (
            <NavItem onClick={this.login}>Login</NavItem>
          ) : (
            <NavItem onClick={this.logout}>Logout</NavItem>
          )}
        </Nav>
      </Navbar>
    );
  }

  ...

当组件加载后,我们从 store 中获得用户的身份验证状态。根据 authenticated 状态显示或隐藏 NavItems 。

我们可以用同样的方法设置 Index 组件中的提示信息。

// src/components/Index.js

...

constructor() {
    super();
    this.state = {
      authenticated: AuthStore.isAuthenticated()
    }
  }
  render() {
    return (
      <div>
        { !this.state.authenticated ? (
          <h2>Log in to view contact details</h2>
        ) : (
          <h2>Click on a contact to view their profile</h2>
        )}
      </div>
    );
  }

...

总结

如果你跟着本教程做完,现在你已经有了一个 React + Flux 的应用,它调用 API 获取数据以及使用 Auth0 完成用户身份认证。非常棒!

毫无疑问: 创建一个 React + Flux 应用程序需要写大量代码,而构建小项目很难看到它的优势。但是,随着应用程序体量的增长,单向数据流以及 Flux 遵循的应用结构变得非常重要。当应用程序变得越来越大时,有必要消除双向绑定带来的困惑。

幸运的是,令人棘手的身份验证部分使用 Auth0 来做非常简单。如果你的应用程序没有使用 Node 作为后端,务必选择适合你的 Auth0 SDK 。几乎所有流行的语言和框架都有集成,包括:

posted @ 2017-09-14 17:45  叙帝利  阅读(1786)  评论(0编辑  收藏  举报