構建具有用戶身份認證的 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 。幾乎所有流行的語言和框架都有集成,包括:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM