react 項目實戰(九)登陸與身份認證

SPA的鑑權方式和傳統的web應用不一樣:因爲頁面的渲染再也不依賴服務端,與服務端的交互都經過接口來完成,而REASTful風格的接口提倡無狀態(state less),一般不使用cookie和session來進行身份認證。javascript

比較流行的一種方式是使用web token,所謂的token能夠看做是一個標識身份的令牌。客戶端在登陸成功後能夠得到服務端加密後的token,而後在後續須要身份認證的接口請求中在header中帶上這個token,服務端就能夠經過判斷token的有效性來驗證該請求是否合法。java

咱們先來改造一下服務端,實現一個簡單的基於token的身份認證(可直接複製代碼,無需關心具體實現)。node

改造服務端

先在根目錄下執行npm i json-server -D,雖然一開始以全局的方式安裝過json-server這個工具,但本次要在代碼中使用json-serverapi,須要將其安裝爲項目依賴react

而後新建/server/auth.js文件,寫入如下代碼:web

/**
 * 到期時間
 */
const expireTime = 1000 * 60;
 
module.exports = function (req, res, next) {
  res.header('Access-Control-Expose-Headers', 'access-token');
  const now = Date.now();
 
  let unauthorized = true; // 未受權
  const token = req.headers['access-token'];
  if (token) {
    const expired = now - token > expireTime;
    if (!expired) {
      unauthorized = false;
      res.header('access-token', now);
    }
  }
 
  if (unauthorized) {
    res.sendStatus(401);
  } else {
    next();
  }
};

新建/server/index.js文件,寫入如下代碼:npm

const path = require('path');
const jsonServer = require('json-server');
const server = jsonServer.create();
const router = jsonServer.router(path.join(__dirname, 'db.json'));
const middlewares = jsonServer.defaults();

server.use(jsonServer.bodyParser);
server.use(middlewares);

server.post('/login', function (req, res, next) {
  res.header('Access-Control-Expose-Headers', 'access-token');
  const {account, password} = req.body;
  if (account === 'admin' && password === '123456') {
    res.header('access-token', Date.now());
    res.json(true);
  } else {
    res.json(false);
  }
});

server.use(require('./auth'));
server.use(router);

server.listen(8000, function () {
  console.log('JSON Server is running in http://localhost:8000');
});

修改/package.json文件中的scripts.serverjson

{
  ...
  "scripts": {
    "server": "node server/index.js",
    ...
  },
  ...
}

而後使用npm run server重啓服務器。api

如今咱們的服務器就擁有了身份認證的功能,訪問除了’/login’外的其它接口時,服務端會根據請求的header中access-token來判斷請求是否有效,若是無效則會返回401狀態碼。服務器

當客戶端收到401的狀態碼時,須要跳轉到登陸頁面進行登陸,有效的管理員帳號爲admin,密碼爲123456。cookie

以POST方法提交下面的參數到’http://localhost:8000/login‘接口,就可以完成登陸。

{
  "account": "admin",
  "password": "123456"
}

登陸成功後,接口返回true,而且在返回的headers中包含了一個有效的access-token,用於在後面的請求中使用;登陸失敗則返回false

access-token的有效期爲1分鐘,每次有效的接口請求都會得到新的access-token;若1分鐘內沒有作操做,則會過時須要從新登陸。

咱們的access-token只是一個簡單的timestamp,且沒有作任何加密措施

封裝fetch

因爲咱們每一個接口的請求都須要加上一個名爲access-token的header,在每次須要調用接口的時候都寫一遍就很是的不明智了,因此咱們須要封裝fetch方法。

新建/src/utils/request.js,寫入如下代碼:

/**
 * 封裝 fetch
 */
import { hashHistory } from 'react-router';
 
export default function request (method, url, body) {
  method = method.toUpperCase();
  if (method === 'GET') {
    // fetch的GET不容許有body,參數只能放在url中
    body = undefined;
  } else {
    body = body && JSON.stringify(body);
  }
 
  return fetch(url, {
    method,
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      'Access-Token': sessionStorage.getItem('access_token') || '' // 從sessionStorage中獲取access token
    },
    body
  })
  .then((res) => {
    if (res.status === 401) {
      hashHistory.push('/login');
      return Promise.reject('Unauthorized.');
    } else {
      const token = res.headers.get('access-token');
      if (token) {
        sessionStorage.setItem('access_token', token);
      }
      return res.json();
    }
  });
}

// GET 請求
export const get = url => request('GET', url);
// POST 請求
export const post = (url, body) => request('POST', url, body);
// PUT 上傳
export const put = (url, body) => request('PUT', url, body);
// DELETE 刪除
export const del = (url, body) => request('DELETE', url, body);

request方法封裝了添加access-token頭等邏輯,而後就能夠在須要調用接口的時候使用request或get、post等方法了,好比/src/components/BookEditor.js

...
import request, {get} from '../utils/request';

class BookEditor extends React.Component {
  ...
  handleSubmit (e) {
    ...

    let editType = '添加';
    let apiUrl = 'http://localhost:8000/book';
    let method = 'post';
    if (editTarget) {
      ...
    }

    request(method, apiUrl, {
      name: name.value,
      price: price.value,
      owner_id: owner_id.value
    })
      .then((res) => {
        if (res.id) {
          ...
        } else {
          ...
        }
      })
      .catch((err) => console.error(err));
  }

  getRecommendUsers (partialUserId) {
    get('http://localhost:8000/user?id_like=' + partialUserId)
      .then((res) => {
        if (res.length === 1 && res[0].id === partialUserId) {
          return;
        }
        ...
      });
  }
  ...
}
...

其它還有/src/components/UserEditor.js/src/pages/BookEdit.js/src/pages/BookList.js/src/pages/UserEdit.js/src/pages/UserList.js文件須要進行相應的修改。

/src/components/UserEditor.js

/**
 * 用戶編輯器組件
 */
import React from 'react';
import FormItem from '../components/FormItem'; // 或寫成 ./FormItem
// 高階組件 formProvider表單驗證
import formProvider from '../utils/formProvider';
// 引入 prop-types
import PropTypes from 'prop-types';
// 引入 封裝fetch工具類
import request from '../utils/request'; 

class UserEditor extends React.Component {
  // 按鈕提交事件
  handleSubmit(e){
    // 阻止表單submit事件自動跳轉頁面的動做
    e.preventDefault();
    // 定義常量
    const { form: { name, age, gender }, formValid, editTarget} = this.props; // 組件傳值
    // 驗證
    if(!formValid){
      alert('請填寫正確的信息後重試');
      return;
    }

    // 默認值
    let editType = '添加';
    let apiUrl = 'http://localhost:8000/user';
    let method = 'post';
    // 判斷類型
    if(editTarget){
      editType = '編輯';
      apiUrl += '/' + editTarget.id;
      method = 'put';
    }

    // 發送請求
    request(method,apiUrl, {
      name: name.value,
      age: age.value,
      gender: gender.value
    })
    // 成功的回調
    .then((res) => {
      // 當添加成功時,返回的json對象中應包含一個有效的id字段
      // 因此可使用res.id來判斷添加是否成功
      if(res.id){
        alert(editType + '添加用戶成功!');
        this.context.router.push('/user/list'); // 跳轉到用戶列表頁面
        return;
      }else{
        alert(editType + '添加用戶失敗!');
      }
    })
    // 失敗的回調
    .catch((err) => console.error(err));
  }

  // 生命週期--組件加載中
  componentWillMount(){
    const {editTarget, setFormValues} = this.props;
    if(editTarget){
      setFormValues(editTarget);
    }
  }
  
  render() {
    // 定義常量
    const {form: {name, age, gender}, onFormChange} = this.props;
    return (
      <form onSubmit={(e) => this.handleSubmit(e)}>
        <FormItem label="用戶名:" valid={name.valid} error={name.error}>
          <input
            type="text"
            value={name.value}
            onChange={(e) => onFormChange('name', e.target.value)}/>
        </FormItem>

        <FormItem label="年齡:" valid={age.valid} error={age.error}>
          <input
            type="number"
            value={age.value || ''}
            onChange={(e) => onFormChange('age', e.target.value)}/>
        </FormItem>

        <FormItem label="性別:" valid={gender.valid} error={gender.error}>
          <select
            value={gender.value}
            onChange={(e) => onFormChange('gender', e.target.value)}>
            <option value="">請選擇</option>
            <option value="male">男</option>
            <option value="female">女</option>
          </select>
        </FormItem>
        <br />
        <input type="submit" value="提交" />
      </form>
    );
  }
}

// 必須給UserEditor定義一個包含router屬性的contextTypes
// 使得組件中能夠經過this.context.router來使用React Router提供的方法
UserEditor.contextTypes = {
  router: PropTypes.object.isRequired
};

// 實例化
UserEditor = formProvider({ // field 對象
  // 姓名
  name: {
    defaultValue: '',
    rules: [
      {
        pattern: function (value) {
          return value.length > 0;
        },
        error: '請輸入用戶名'
      },
      {
        pattern: /^.{1,4}$/,
        error: '用戶名最多4個字符'
      }
    ]
  },
  // 年齡
  age: {
    defaultValue: 0,
    rules: [
      {
        pattern: function(value){
          return value >= 1 && value <= 100;
        },
        error: '請輸入1~100的年齡'
      }
    ]
  },
  // 性別
  gender: {
    defaultValue: '',
    rules: [
      {
        pattern: function(value) {
          return !!value;
        },
        error: '請選擇性別'
      }
    ]
  }
})(UserEditor);

export default UserEditor;

/src/pages/BookEdit.js

/**
 * 編輯圖書頁面
 */
import React from 'react';
// 佈局組件
import HomeLayout from '../layouts/HomeLayout';
// 引入 prop-types
import PropTypes from 'prop-types';
// 圖書編輯器組件
import BookEditor from '../components/BookEditor';
// 引入 封裝fetch工具類
import { get } from '../utils/request'; 

class BookEdit extends React.Component {
  // 構造器
  constructor(props) {
    super(props);
    // 定義初始化狀態
    this.state = {
      book: null
    };
  }

  // 生命週期--組件加載中
  componentWillMount(){
    // 定義常量
    const bookId = this.context.router.params.id;
    /**
     * 發送請求
     * 獲取用戶數據
     */
    get('http://localhost:8000/book/' + bookId)
    .then((res) => {
      console.log(res);
      // 設置狀態
      this.setState({
        book: res
      });
    })
  }

  render() {
    const {book} = this.state;
    return (
      <HomeLayout title="編輯圖書">
        {
          book ? <BookEditor editTarget={book} /> : '加載中...'
        }
      </HomeLayout>
    );
  }
}

BookEdit.contextTypes = {
  router: PropTypes.object.isRequired
};

export default BookEdit;

/src/pages/BookList.js

/**
 * 圖書列表頁面
 */
import React from 'react';
// 佈局組件
import HomeLayout from '../layouts/HomeLayout';
// 引入 prop-types
import PropTypes from 'prop-types';
// 引入 封裝fetch工具類
import { get, del } from '../utils/request'; 

class BookList extends React.Component {
  // 構造器
  constructor(props) {
    super(props);
    // 定義初始化狀態
    this.state = {
      bookList: []
    };
  }

  /**
   * 生命週期
   * componentWillMount
   * 組件初始化時只調用,之後組件更新不調用,整個生命週期只調用一次
   */
  componentWillMount(){
    // 請求數據
    get('http://localhost:8000/book')
      .then((res) => {
        /**
         * 成功的回調
         * 數據賦值
         */
        this.setState({
          bookList: res
        });
      });
  }

  /**
   * 編輯
   */
  handleEdit(book){
    // 跳轉編輯頁面
    this.context.router.push('/book/edit/' + book.id);
  }

  /**
   * 刪除
   */
  handleDel(book){
    // 確認框
    const confirmed = window.confirm(`確認要刪除書名 ${book.name} 嗎?`);
    // 判斷
    if(confirmed){
      // 執行刪除數據操做
      del('http://localhost:8000/book/' + book.id, {
      })
      .then(res => {
        /**
         * 設置狀態
         * array.filter
         * 把Array的某些元素過濾掉,而後返回剩下的元素
         */
        this.setState({
          bookList: this.state.bookList.filter(item => item.id !== book.id)
        });
        alert('刪除用戶成功');
      })
      .catch(err => {
        console.log(err);
        alert('刪除用戶失敗');
      });
    }
  }

  render() {
    // 定義變量
    const { bookList } = this.state;

    return (
      <HomeLayout title="圖書列表">
        <table>
          <thead>
            <tr>
              <th>圖書ID</th>
              <th>圖書名稱</th>
              <th>價格</th>
              <th>操做</th>
            </tr>
          </thead>

          <tbody>
            {
              bookList.map((book) => {
                return (
                  <tr key={book.id}>
                    <td>{book.id}</td>
                    <td>{book.name}</td>
                    <td>{book.price}</td>
                    <td>
                      <a onClick={() => this.handleEdit(book)}>編輯</a>
                       
                      <a onClick={() => this.handleDel(book)}>刪除</a>
                    </td>
                  </tr>
                );
              })
            }
          </tbody>
        </table>
      </HomeLayout>
    );
  }
}

/**
 * 任何使用this.context.xxx的地方,必須在組件的contextTypes裏定義對應的PropTypes
 */
BookList.contextTypes = {
  router: PropTypes.object.isRequired
};

export default BookList;

/src/pages/UserEdit.js

/**
 * 編輯用戶頁面
 */
import React from 'react';
// 佈局組件
import HomeLayout from '../layouts/HomeLayout';
// 引入 prop-types
import PropTypes from 'prop-types';
// 用戶編輯器組件
import UserEditor from '../components/UserEditor';
// 引入 封裝fetch工具類
import { get } from '../utils/request'; 

class UserEdit extends React.Component {
  // 構造器
  constructor(props) {
    super(props);
    // 定義初始化狀態
    this.state = {
      user: null
    };
  }

  // 生命週期--組件加載中
  componentWillMount(){
    // 定義常量
    const userId = this.context.router.params.id;
    /**
     * 發送請求
     * 獲取用戶數據
     */
    get('http://localhost:8000/user/' + userId)
    .then((res) => {
      // 設置狀態
      this.setState({
        user: res
      });
    })
  }

  render() {
    const {user} = this.state;
    return (
      <HomeLayout title="編輯用戶">
        {
          user ? <UserEditor editTarget={user} /> : '加載中...'
        }
      </HomeLayout>
    );
  }
}

UserEdit.contextTypes = {
  router: PropTypes.object.isRequired
};

export default UserEdit;

/src/pages/UserList.js

/**
 * 用戶列表頁面
 */
import React from 'react';
// 佈局組件
import HomeLayout from '../layouts/HomeLayout';
// 引入 prop-types
import PropTypes from 'prop-types';
// 引入 封裝後的fetch工具類
import { get, del } from '../utils/request';

class UserList extends React.Component {
  // 構造器
  constructor(props) {
    super(props);
    // 定義初始化狀態
    this.state = {
      userList: []
    };
  }

  /**
   * 生命週期
   * componentWillMount
   * 組件初始化時只調用,之後組件更新不調用,整個生命週期只調用一次
   */
  componentWillMount(){
    // 請求數據
    get('http://localhost:8000/user')
      .then((res) => {
        /**
         * 成功的回調
         * 數據賦值
         */
        this.setState({
          userList: res
        });
      });
  }

  /**
   * 編輯
   */
  handleEdit(user){
    // 跳轉編輯頁面
    this.context.router.push('/user/edit/' + user.id);
  }

  /**
   * 刪除
   */
  handleDel(user){
    // 確認框
    const confirmed = window.confirm(`確認要刪除用戶 ${user.name} 嗎?`);
    // 判斷
    if(confirmed){
      // 執行刪除數據操做
      del('http://localhost:8000/user/' + user.id, {
      })
      .then((res) => {
        /**
         * 設置狀態
         * array.filter
         * 把Array的某些元素過濾掉,而後返回剩下的元素
         */
        this.setState({
          userList: this.state.userList.filter(item => item.id !== user.id)
        });
        alert('刪除用戶成功');
      })
      .catch(err => {
        console.log(err);
        alert('刪除用戶失敗');
      });
    }
  }

  render() {
    // 定義變量
    const { userList } = this.state;

    return (
      <HomeLayout title="用戶列表">
        <table>
          <thead>
            <tr>
              <th>用戶ID</th>
              <th>用戶名</th>
              <th>性別</th>
              <th>年齡</th>
              <th>操做</th>
            </tr>
          </thead>

          <tbody>
            {
              userList.map((user) => {
                return (
                  <tr key={user.id}>
                    <td>{user.id}</td>
                    <td>{user.name}</td>
                    <td>{user.gender}</td>
                    <td>{user.age}</td>
                    <td>
                      <a onClick={() => this.handleEdit(user)}>編輯</a>
                       
                      <a onClick={() => this.handleDel(user)}>刪除</a>
                    </td>
                  </tr>
                );
              })
            }
          </tbody>
        </table>
      </HomeLayout>
    );
  }
}

/**
 * 任何使用this.context.xxx的地方,必須在組件的contextTypes裏定義對應的PropTypes
 */
UserList.contextTypes = {
  router: PropTypes.object.isRequired
};

export default UserList;

實現登陸頁面

如今嘗試訪問一下用戶列表頁,發現表格裏面並無數據,由於沒有登陸接口訪問被拒絕了而且嘗試跳轉到路由’/login’。

如今來實現一個登陸頁面組件,在/src/pages下新建Login.js文件:

/**
 * 登陸頁
 */
import React from 'react';
// 頁面佈局組件
import HomeLayout from '../layouts/HomeLayout';
import FormItem from '../components/FormItem';
// 引入 封裝後的fetch工具類
import { post } from '../utils/request';
// 表單驗證組件
import formProvider from '../utils/formProvider';
// 引入 prop-types
import PropTypes from 'prop-types';
 
class Login extends React.Component {
  // 構造器
  constructor () {
    super();
    this.handleSubmit = this.handleSubmit.bind(this);
  }
  
  handleSubmit (e) {
    e.preventDefault();
 
    const {formValid, form: {account, password}} = this.props;
    if (!formValid) {
      alert('請輸入帳號或密碼');
      return;
    }
 
    post('http://localhost:8000/login', {
      account: account.value,
      password: password.value
    })
      .then((res) => {
        if (res) {
          this.context.router.push('/');
        } else {
          alert('登陸失敗,帳號或密碼錯誤');
        }
      })
  }
 
  render () {
    const {form: {account, password}, onFormChange} = this.props;
    return (
      <HomeLayout title="請登陸">
        <form onSubmit={this.handleSubmit}>
          <FormItem label="帳號:" valid={account.valid} error={account.error}>
            <input type="text" value={account.value} onChange={e => onFormChange('account', e.target.value)}/>
          </FormItem>
          <FormItem label="密碼:" valid={password.valid} error={password.error}>
            <input type="password" value={password.value} onChange={e => onFormChange('password', e.target.value)}/>
          </FormItem>
          <br/>
          <input type="submit" value="登陸"/>
        </form>
      </HomeLayout>
    );
  }
}
 
Login.contextTypes = {
  router: PropTypes.object.isRequired
};
 
Login = formProvider({
  account: {
    defaultValue: '',
    rules: [
      {
        pattern (value) {
          return value.length > 0;
        },
        error: '請輸入帳號'
      }
    ]
  },
  password: {
    defaultValue: '',
    rules: [
      {
        pattern (value) {
          return value.length > 0;
        },
        error: '請輸入密碼'
      }
    ]
  }
})(Login);
 
export default Login;

登陸頁面組件和UserEditor或者BookEditor相似,都是一個表單。

在這裏提交表單成功後跳轉到首頁。

最後,別忘了加上登陸頁面的路由。

最終效果

項目結構:

相關文章
相關標籤/搜索