介绍
对于前端和api调用而言,Cookies和Tokens是两种服务器端基本的验证方式。
大多数人都采用基于cookie验证的方式(你能在这里找到例子),在服务器端使用cookie对用户对每个请求进行身份验证。
另一种新的方式是基于Token验证。每一次向服务器发送请求的时候依赖一个签名的Token。
Cookies对比Token
这是Cookies和Tokens工作方式的图解
使用Token的好处是什么呢?
- 跨域:cookies + CORS不能很好的在不同的域下使用。但是Token允许你使用ajax在不同域下调用任何服务器,因为你使用http请求头去传输用户的信息。
- 无状态:没有必要保持session的存储,需要传输的用户信息都包含在Token里面,其它的状态可以存储在客户端的local storage或者cookies里。
- CDN:在你的应用里面,你可以从CDN里面使用所有的资源(比如javascript, HTML, images等等),你的服务器端只是一个接口。
- 移动优先:当你在移动端平台(iOS, Android, Windows 8等)开发的时候,你无法与移动终端共享服务器创建的 session 和 cookie。相比之下,用Token要简单的多。
- 解耦:你不用绑定一个特定的身份验证的方案,Token可以在任何地方生成,因此你的api可以在任何地方单独调用验证的方法。
- CSRF(跨站请求伪造):一旦你不再依赖cookie,你不需要防止跨站请求。(通过iframe攻击你的网站是不可能的,因为cookie是空的,所以不能再使用现有的验证通过的cookie生成post请求)。
- 性能:在这里我们不展示任何复杂的性能标准,但是一次网络请求的往返(例如,在数据库查找session),花费的时间很可能比计算一个HMACSHA256的token并解析其内容的时间更多。
- 登录页面不需要特殊处理:如果你使用Protractor写你的功能测试,你不需要对你的登陆页面做特殊处理。
- 基于标准:你的api可能采用的是JSON Web Token (JWT)标准,这是一个多个后端库的标准(.NET, Ruby, Java, Python, PHP),并且很多公司支持(例如Firebase, Google, Microsoft),举一个例子, Firebase允许它们的用户使用任何的authentication机制,只要你生成一个JWT,与某些预定义的属性,并签署了共享密钥调用API。
什么是JSON Web Token?JSON Web Token是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
实现
假设你有一个node.js的应用,下面你可以找到这个架构的组件。
服务端
让我们开始安装express-jwt和jsonwebtoken:
$ npm install express-jwt jsonwebtoken
定义一个express中间件保护每一次/api的调用。
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的应用,将会带上用户的凭证通过ajax去执行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
的资源。注意凭证的验证在expressJwt中间件执行。
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。为了得到我们需要的用户凭证,我们得先创建一个表单的视图,能够让用户输入用户名和密码。
<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';
});
};
});
现在我们JWT保存到sessionStorage中,如果token设置了,我们将在每一次使用$http请求的时候设置Authorization。作为请求头的一部分值,我们将使用Bearer<token>
。
sessionStorage: 尽管不支持所有的浏览器,但是你可以使用polyfill,它是代替cookies的一个比较好的方案。
($cookies, $cookieStore)以及localStorage:在用户关闭浏览器标签之后数据依然还会存在。
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');
});
然后,我们发送一个请求到/api/restricted
。
$http({url: '/api/restricted', method: 'GET'})
.success(function (data, status, headers, config) {
console.log(data.name); // Should log 'foo'
});
服务器端控制台:
user foo@bar.com is calling /api/restricted