作者好diao啊,我不懂的他全都懂了。
Cookies vs Tokens. Getting auth right with Angular.JS
基本上服务端有两种不同的方式来实现对前端和 API 的授权:
- 用的最多的是,Cookie-Based 授权(你可以看这),用服务端的 cookie 来验证每个请求
- 比较新的是,Token-Based 授权,依赖于发送到服务端的请求中的授权令牌
Token based vs. Cookie based
下面的图解释了这两种方式的工作流
用 token-based 有什么好处?
- 跨域/CORS
cookies + CORS 在跨域的时候处理的不是很好。而 token-based 方式允许你用 AJAX 调用任何服务,任何域。因为你用的是 HTTP 头来发送你的用户信息。
- 无状态 (又称 服务端可扩展性)
不需要保存 session,token 保存了用户的信息,并且存放在客户端的 cookies 或者 local storage 上。
- CDN
你可以把你的资产全扔 CDN 上(比如 javascript,HTML,images,等),你的服务端只需要提供 API。
- 解耦
你不需要绑定任何认证方式。 token 可以在任何地方产生,因此你的 API 可以从任何地方调用,而仅仅需要授权该调用。
- 移动优先
当你要实现不同平台(iOS,Android,Windows 8,等等)版本应用的时候,cookies 需要用到该平台的安全 API(你必须处理 cookie 的容器)。而 token-based 就简单多了。
- CSRF
既然你不需要依赖 cookies 了,你就不需要处理跨站请求了
- 性能
我们没任何关于性能的硬证据,可是网络回路(比如说从数据库中找一个 session),应该会比计算一个 HMACSHA256 然后验证 token 和解析内容来得慢。
- 登录页面不再是一个特例
如果你用 Protractor 写测试方法,你不需要再写任何关于登陆的测试了。
- 标准
你的 API 可以接收标准的 JSON Web Token (JWT)。这是一个标准,并且有很多可用的库(.NET, Ruby, Java, Python, PHP)以及支持他们的公司(比如 Firebase, Google, Microsoft)。比如说, Firebase 允许他们客户用任何的授权机制,只要你生成一个 JWT ,包含了预定义的属性,然后用共享签名来访问他们的API。
什么是 JSON Web Token? JWT(念:揍它)是一个比较新令牌格式,用在有限的存储空间内的。比如说 HTTP 的 Authorization headers。 JWT 是在各部流转的 claims based 间传递加密的一个结构化方法。
实现
假设你已经有 nodejs 应用了,然后你可以找到下面的组件。
服务端
让我们先安装 express-jwt 和 jsonwebtoken :
<!-- lang: js -->
$ npm install express-jwt jsonwebtoken
配置 express 的中间件,用来过滤所有的 /api 请求。
<!-- lang: js -->
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 请求,附带用户认证。
<!-- lang: js -->
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 });
});
GET 一个资源,/api/restricted 直接访问。注意的是,用 expressJwt 中间件 来检查认证非常快。
<!-- lang: js -->
app.get('/api/restricted', function (req, res) {
console.log('user ' + req.user.email + ' is calling /api/restricted');
res.json({
name: 'foo'
});
});
客户端
客户端处理的第一步就是用 AngularJS 来接收 JWT。为了拿到用户信息,我们先创建一个表格,给客户填写用户名和密码。
<!-- lang: js -->
<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>
然后控制器处理提交动作:
<!-- lang: js -->
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。数据持续到把浏览器 tab 关了就失效了。
<!-- lang: js -->
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');
});
然后,我们可以发送一个请求给上面定义的 restricted 资源了:
<!-- lang: js -->
$http({url: '/api/restricted', method: 'GET'})
.success(function (data, status, headers, config) {
console.log(data.name); // Should log 'foo'
});
随后你看到服务端输出日志:
<!-- lang: js -->
user foo@bar.com is calling /api/restricted
整套代码在这里,和一个 AngularJS Seed。
下一步?
之后的文章我们将会重温一下:
- 如何处理社交授权
- 如何处理session过期
更新:作者三连发
- Token based authentication in realtime frameworks like Socket.io
- 10 Things you should know about Tokens