介紹
對于前端和api調(diào)用而言辽故,Cookies和Tokens是兩種服務(wù)器端基本的驗(yàn)證方式。
大多數(shù)人都采用基于cookie驗(yàn)證的方式(你能在這里找到例子)掉弛,在服務(wù)器端使用cookie對用戶對每個請求進(jìn)行身份驗(yàn)證喂走。
另一種新的方式是基于Token驗(yàn)證芋肠。每一次向服務(wù)器發(fā)送請求的時候依賴一個簽名的Token。
Cookies對比Token
這是Cookies和Tokens工作方式的圖解
使用Token的好處是什么呢?
- 跨域:cookies + CORS不能很好的在不同的域下使用碘裕。但是Token允許你使用ajax在不同域下調(diào)用任何服務(wù)器帮孔,因?yàn)槟闶褂胔ttp請求頭去傳輸用戶的信息不撑。
- 無狀態(tài):沒有必要保持session的存儲焕檬,需要傳輸?shù)挠脩粜畔⒍及赥oken里面,其它的狀態(tài)可以存儲在客戶端的local storage或者cookies里兼呵。
- CDN:在你的應(yīng)用里面腊敲,你可以從CDN里面使用所有的資源(比如javascript, HTML, images等等),你的服務(wù)器端只是一個接口懂昂。
- 移動優(yōu)先:當(dāng)你在移動端平臺(iOS, Android, Windows 8等)開發(fā)的時候凌彬,你無法與移動終端共享服務(wù)器創(chuàng)建的 session 和 cookie。相比之下勉失,用Token要簡單的多原探。
- 解耦:你不用綁定一個特定的身份驗(yàn)證的方案咽弦,Token可以在任何地方生成,因此你的api可以在任何地方單獨(dú)調(diào)用驗(yàn)證的方法段审。
- CSRF(跨站請求偽造):一旦你不再依賴cookie闹蒜,你不需要防止跨站請求寺枉。(通過iframe攻擊你的網(wǎng)站是不可能的,因?yàn)閏ookie是空的绷落,所以不能再使用現(xiàn)有的驗(yàn)證通過的cookie生成post請求)姥闪。
- 性能:在這里我們不展示任何復(fù)雜的性能標(biāo)準(zhǔn),但是一次網(wǎng)絡(luò)請求的往返(例如砌烁,在數(shù)據(jù)庫查找session)筐喳,花費(fèi)的時間很可能比計(jì)算一個HMACSHA256的token并解析其內(nèi)容的時間更多。
- 登錄頁面不需要特殊處理:如果你使用Protractor寫你的功能測試函喉,你不需要對你的登陸頁面做特殊處理避归。
- 基于標(biāo)準(zhǔn):你的api可能采用的是JSON Web Token (JWT)標(biāo)準(zhǔn),這是一個多個后端庫的標(biāo)準(zhǔn)(.NET, Ruby, Java, Python, PHP)梳毙,并且很多公司支持(例如Firebase, Google, Microsoft),舉一個例子撇寞, Firebase允許它們的用戶使用任何的authentication機(jī)制顿天,只要你生成一個JWT,與某些預(yù)定義的屬性蔑担,并簽署了共享密鑰調(diào)用API牌废。
什么是JSON Web Token?JSON Web Token是一個非常輕巧的規(guī)范啤握。這個規(guī)范允許我們使用JWT在用戶和服務(wù)器之間傳遞安全可靠的信息鸟缕。
實(shí)現(xiàn)
假設(shè)你有一個node.js的應(yīng)用,下面你可以找到這個架構(gòu)的組件。
服務(wù)端
讓我們開始安裝express-jwt和jsonwebtoken:
$ npm install express-jwt jsonwebtoken
定義一個express中間件保護(hù)每一次/api的調(diào)用懂从。
var expressJwt = require('express-jwt');
var jwt = require('jsonwebtoken');
// We are going to protect /api routes with JWT
app.use('/api', expressJwt({secret: secret}));
app.use(express.json());
app.use(express.urlencoded());
這是一個angular的應(yīng)用授段,將會帶上用戶的憑證通過ajax去執(zhí)行post請求。
app.post('/authenticate', function (req, res) {
//TODO validate req.body.username and req.body.password
//if is invalid, return 401
if (!(req.body.username === 'john.doe' && req.body.password === 'foobar')) {
res.send(401, 'Wrong user or password');
return;
}
var profile = {
first_name: 'John',
last_name: 'Doe',
email: 'john@doe.com',
id: 123
};
// We are sending the profile inside the token
var token = jwt.sign(profile, secret, { expiresInMinutes: 60*5 });
res.json({ token: token });
});
直接獲取到名字為/api/restricted
的資源番甩。注意憑證的驗(yàn)證在expressJwt中間件執(zhí)行侵贵。
app.get('/api/restricted', function (req, res) {
console.log('user ' + req.user.email + ' is calling /api/restricted');
res.json({
name: 'foo'
});
});
AngularJS端
第一步,在客戶端使用AngularJS取得 JWT Token缘薛。為了得到我們需要的用戶憑證窍育,我們得先創(chuàng)建一個表單的視圖,能夠讓用戶輸入用戶名和密碼宴胧。
<div ng-controller="UserCtrl">
<span></span>
<form ng-submit="submit()">
<input ng-model="user.username" type="text" name="user" placeholder="Username" />
<input ng-model="user.password" type="password" name="pass" placeholder="Password" />
<input type="submit" value="Login" />
</form>
</div>
一個處理表單提交的controller:
myApp.controller('UserCtrl', function ($scope, $http, $window) {
$scope.user = {username: 'john.doe', password: 'foobar'};
$scope.message = '';
$scope.submit = function () {
$http
.post('/authenticate', $scope.user)
.success(function (data, status, headers, config) {
$window.sessionStorage.token = data.token;
$scope.message = 'Welcome';
})
.error(function (data, status, headers, config) {
// Erase the token if the user fails to log in
delete $window.sessionStorage.token;
// Handle login errors here
$scope.message = 'Error: Invalid user or password';
});
};
});
現(xiàn)在我們JWT保存到sessionStorage中漱抓,如果token設(shè)置了,我們將在每一次使用$http請求的時候設(shè)置Authorization恕齐。作為請求頭的一部分值乞娄,我們將使用Bearer<token>
。
sessionStorage: 盡管不支持所有的瀏覽器显歧,但是你可以使用polyfill仪或,它是代替cookies的一個比較好的方案。
($cookies, $cookieStore)以及l(fā)ocalStorage:在用戶關(guān)閉瀏覽器標(biāo)簽之后數(shù)據(jù)依然還會存在士骤。
myApp.factory('authInterceptor', function ($rootScope, $q, $window) {
return {
request: function (config) {
config.headers = config.headers || {};
if ($window.sessionStorage.token) {
config.headers.Authorization = 'Bearer ' + $window.sessionStorage.token;
}
return config;
},
response: function (response) {
if (response.status === 401) {
// handle the case where the user is not authenticated
}
return response || $q.when(response);
}
};
});
myApp.config(function ($httpProvider) {
$httpProvider.interceptors.push('authInterceptor');
});
然后溶其,我們發(fā)送一個請求到/api/restricted
。
$http({url: '/api/restricted', method: 'GET'})
.success(function (data, status, headers, config) {
console.log(data.name); // Should log 'foo'
});
服務(wù)器端控制臺:
user foo@bar.com is calling /api/restricted