細緻想一想。咱們的後臺系統尚未一個登陸功能,太不靠譜,趕忙把防盜門安上!javascript
SPA的鑑權方式和傳統的web應用不一樣:由於頁面的渲染再也不依賴服務端,與服務端的交互都經過接口來完畢,而REASTful風格的接口提倡無狀態(state less)。一般不使用cookie和session來進行身份認證。css
比較流行的一種方式是使用web token,所謂的token可以看做是一個標識身份的令牌。java
client在登陸成功後可以得到服務端加密後的token,而後在興許需要身份認證的接口請求中在header中帶上這個token。服務端就可以經過推斷token的有效性來驗證該請求是否合法。node
咱們先來改造一下服務端。實現一個簡單的基於token的身份認證(可直接複製代碼。無需關心詳細實現)。react
先在根文件夾下運行npm i json-server -D
,儘管一開始以全局的方式安裝過json-server這個工具。但本次要在代碼中使用json-server的api,需要將其安裝爲項目依賴。web
而後新建/server/auth.js
文件,寫入下面代碼:npm
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
文件,寫入下面代碼:json
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
:api
{
...
"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,且沒有作不論什麼加密措施。
由於咱們每個接口的請求都需要加上一個名爲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類似,都是一個表單。
在這裏提交表單成功後跳轉到首頁。
最後,別忘了加上登陸頁面的路由。