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-server的api,須要將其安裝爲項目依賴。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.server
:json
{ ... "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,且沒有作任何加密措施。
因爲咱們每一個接口的請求都須要加上一個名爲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相似,都是一個表單。
在這裏提交表單成功後跳轉到首頁。
最後,別忘了加上登陸頁面的路由。
項目結構: