1. 程式人生 > >【React全家桶入門之十】登錄與身份認證

【React全家桶入門之十】登錄與身份認證

cto json head AC push 操作 undefine 防盜 項目依賴

細致想想。我們的後臺系統還沒有一個登錄功能,太不靠譜,趕緊把防盜門安上!

SPA的鑒權方式和傳統的web應用不同:因為頁面的渲染不再依賴服務端,與服務端的交互都通過接口來完畢,而REASTful風格的接口提倡無狀態(state less)。通常不使用cookie和session來進行身份認證。

比較流行的一種方式是使用web token,所謂的token能夠看作是一個標識身份的令牌。

client在登錄成功後能夠獲得服務端加密後的token,然後在興許須要身份認證的接口請求中在header中帶上這個token。服務端就能夠通過推斷token的有效性來驗證該請求是否合法。

我們先來改造一下服務端。實現一個簡單的基於token的身份認證(可直接復制代碼。無需關心詳細實現)

改造服務端

先在根文件夾下運行npm i json-server -D,盡管一開始以全局的方式安裝過json-server這個工具。但本次要在代碼中使用json-server的api,須要將其安裝為項目依賴。

然後新建/server/auth.js文件,寫入下面代碼:

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文件,寫入下面代碼:

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(3000, function () {
  console.log(‘JSON Server is running in http://localhost:3000‘);
});

改動/package.json文件裏的scripts.server

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

然後使用npm run server重新啟動服務器。

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

當client收到401的狀態碼時。須要跳轉到登錄頁面進行登錄。有效的管理員賬號為admin。密碼為123456。

以POST方法提交下面的參數到’http://localhost:3000/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。寫入下面代碼:

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

export const get = url => request(‘GET‘, url);
export const post = (url, body) => request(‘POST‘, url, body);
export const put = (url, body) => request(‘PUT‘, url, body);
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:3000/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:3000/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文件須要進行對應的改動。

實現登錄頁面

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

如今來實現一個登錄頁面組件。在/src/pages下新建Login.js文件;

import React from ‘react‘;
import HomeLayout from ‘../layouts/HomeLayout‘;
import FormItem from ‘../components/FormItem‘;
import { post } from ‘../utils/request‘;
import formProvider from ‘../utils/formProvider‘;

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:3000/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: React.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相似,都是一個表單。

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

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

終於效果

技術分享圖片

【React全家桶入門之十】登錄與身份認證