cookie、session和token
https://zhuanlan.zhihu.com/p/25495290?utm_source=wechat_session&utm_medium=social
一、cookie
众所周知,HTTP 是一个无状态协议,所以客户端每次发出请求时,下一次请求无法得知上一次请求所包含的状态数据,如何能把一个用户的状态数据关联起来呢?
1、首先产生了 cookie 这门技术来解决这个问题,cookie 是 http 协议的一部分,它的处理分为如下几步:
1 服务器向客户端发送 cookie。 2 通常使用 HTTP 协议规定的 set-cookie 头操作。 3 规范规定 cookie 的格式为 name = value 格式,且必须包含这部分。 4 浏览器将 cookie 保存。 5 每次请求浏览器都会将 cookie 发向服务器。
2、其他可选的 cookie 参数会影响将 cookie 发送给服务器端的过程,主要有以下几种:
1 path:表示 cookie 影响到的路径,匹配该路径才发送这个 cookie。 2 expires 和 maxAge:告诉浏览器这个 cookie 什么时候过期,expires 是 UTC 格式时间,maxAge 是 cookie 多久后过期的相对时间。当不设置这两个选项时,会产生 session cookie,session cookie 是 transient 的,当用户关闭浏览器时,就被清除。一般用来保存 session 的 session_id。 3 secure:当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效。 4 httpOnly:浏览器不允许脚本操作 document.cookie 去更改 cookie。一般情况下都应该设置这个为 true,这样可以避免被 xss 攻击拿到 cookie。
3、express 中的 cookie
express 在 4.x 版本之后,session管理和cookies等许多模块都不再直接包含在express中,而是需要单独添加相应模块。
var express = require('express'); // 首先引入 cookie-parser 这个模块 var cookieParser = require('cookie-parser'); var app = express(); app.listen(3000); // 使用 cookieParser 中间件,cookieParser(secret, options) app.use(cookieParser()); app.get('/', function (req, res) { // 如果请求中的 cookie 存在 isVisit, 则输出 cookie // 否则,设置 cookie 字段 isVisit, 并设置过期时间为1分钟 if (req.cookies.jack) { console.log(req.cookies.jack); res.send("welcome"); } else { res.cookie('jack', 'content', {maxAge: 60 * 1000}); res.send("no cookie"); } });
这里开发调试的时候用supervisor来启动,代码有改动,它会自动重启,避免不必要的手动重启工作。
新版本的开发者工具界面,在application里面可以看到cookies 这些存储
现在我们看到一个cookie名字为jack 内容为content的cookie就存储了,时间期限也有。
如果没有设置时间(maxage/expires),
那就是session cookie,
浏览器关闭的时候cookie就没了。
二、session
cookie 虽然很方便,但是使用 cookie 有一个很大的弊端,cookie 中的所有数据在客户端就可以被修改,数据非常容易被伪造,那么一些重要的数据就不能存放在 cookie 中了,而且如果 cookie 中数据字段太多会影响传输效率。为了解决这些问题,就产生了 session,session 中的数据是保留在服务器端的。
session 的运作通过一个session_id来进行。session_id通常是存放在客户端的 cookie 中,比如在 express 中,默认是connect.sid这个字段,当请求到来时,服务端检查 cookie 中保存的 session_id 并通过这个 session_id 与服务器端的 session data 关联起来,进行数据的保存和修改。
这意思就是说,当你浏览一个网页时,服务端随机产生一个 1024 比特长的字符串,然后存在你 cookie 中的connect.sid字段中。当你下次访问时,cookie 会带有这个字符串,然后浏览器就知道你是上次访问过的某某某,然后从服务器的存储中取出上次记录在你身上的数据。由于字符串是随机产生的,而且位数足够多,所以也不担心有人能够伪造。伪造成功的概率比坐在家里编程时被邻居家的狗突然闯入并咬死的几率还低。
1、session 可以存放在地方
1)内存
2)cookie本身
3)redis 或 memcached 等缓存中
4)数据库中。
线上来说,缓存的方案比较常见,存数据库的话,查询效率相比前三者都太低,不推荐;cookie session 有安全性问题,下面会提到。
2、express 中操作 session
express 中操作 session 要用到express-session(expressjs/session: Simple session middleware for Express) 这个模块,主要的方法就是session(options),其中 options 中包含可选参数,主要有:
1 name: 设置 cookie 中,保存 session 的字段名称,默认为connect.sid。 2 store: session 的存储方式,默认存放在内存中,也可以使用 redis,mongodb 等。express 生态中都有相应模块的支持。 3 secret: 通过设置的 secret 字符串,来计算 hash 值并放在 cookie 中,使产生的 signedCookie 防篡改。 4 cookie: 设置存放 session id 的 cookie 的相关选项,默认为(default: { path: '/', httpOnly: true, secure: false, maxAge: null }) 5 genid: 产生一个新的 session_id 时,所使用的函数, 默认使用uid2这个 npm 包。 6 rolling: 每个请求都重新设置一个 cookie,默认为 false。 7 resave: 即使 session 没有被修改,也保存 session 值,默认为 true。
3、在内存中存储 session
express-session默认使用内存来存 session,对于开发调试来说很方便
var express = require('express'); // 首先引入 express-session 这个模块 var session = require('express-session'); var app = express(); app.listen(5000); app.use(cookieParser('jack2016')); //解析cookie secret为‘jack2016’的cookie,可不可以不写secret?不写会报错 })); // 按照上面的解释,设置 session 的可选参数 app.use(session({ secret:'jack2016', // 建议使用 128 个字符的随机字符串,这里不写secret的话cookie存储的是不加密的sessionid name:'jacks', //cookie名字,这里cookie存的内容是用secret加密的sessionid, cookie: {maxAge:60*2000},//cookie设置,maxAge设置时间好像受到限制,太小直接没效,设置的够大无论是60*60还是60*60*24*12好像都是固定的4小时,这里有点疑惑。 })); /* GET home page. */ router.get('/',function(req,res,next) { console.log(req.sessionID,req.cookies.jack,req.signedCookies.jack); res.render('index',{title:'Express'}); });
signedCookie
cookie 虽然很方便,但是使用 cookie 有一个很大的弊端,cookie 中的所有数据在客户端就可以被修改,数据非常容易被伪造
其实不是这样的,那只是为了方便理解才那么写。要知道,计算机领域有个名词叫签名,专业点说,叫信息摘要算法。
三、token
API使得传统的前端和后端的概念解耦。开发者可以脱离前端,独立的开发后端,在测试上获得更大的便利。这种途径也使得一个移动应用和网页应用可以使用相同的后端。
当使用一个API时,其中一个挑战就是认证(authentication)。在传统的web应用中,服务端成功的返回一个响应(response)依赖于两件事。一是,他通过一种存储机制保存了会话信息(Session)。每一个会话都有它独特的信息(id),常常是一个长的,随机化的字符串,它被用来让未来的请求(Request)检索信息。其次,包含在响应头(Header)里面的信息使客户端保存了一个Cookie。服务器自动的在每个子请求里面加上了会话ID,这使得服务器可以通过检索Session中的信息来辨别用户。这就是传统的web应用逃避HTTP面向无连接的方法(This is how traditional web applications get around the fact that HTTP is stateless)。
API应该被设计成无状态的(Stateless)。这意味着没有登陆,注销的方法,也没有sessions,API的设计者同样也不能依赖Cookie,因为不能保证这些request是由浏览器所发出的。自然,我们需要一个新的机制。这篇文章关注于JSON Web Tokens,简写为JWTs,一个可能的解决这个问题的机制。这篇文章利用Node的Express框架作为后端,以及Backbone作为前端。
1、背景
我们来简短的看一下几个通常的保护(secure)API的方法。
第一个是使用在HTTP规范中所制定的Basic Auth, 它需要在在响应中设定一个验证身份的Header。客户端必须在每个子响应是附加它们的凭证(credenbtial),包括它的密码。如果这些凭证通过了,那么用户的信息就会被传递到服务端应用。
第二个方面有点类似,但是使用应用自己的验证机制。通常包括将发送的凭证与存储的凭证进行检查。和Basic Auth相比,这种需要在每次请求(call)中发送凭证。
第三种是OAuth(或者OAuth2)。为第三方的认证所设计,但是更难配置。至少在服务器端更难。
2、使用Token的方法
不是在每一次请求时提供用户名和密码的凭证。我们可以让用户通过token交换凭证(we can allow the client to exchange valid credentials for a token),这个token提供用户访问服务器的权限。Token通常比密码更加长而且复杂。比如说,JWTs通常会应对长达150个字符。一旦获得了token,在每次调用API的时候都要附加上它。然后,这仍然比直接发送账户和密码更加安全,哪怕是HTTPS。
把token想象成一个安全的护照。你在一个安全的前台验证你的身份(通过你的用户名和密码),如果你成功验证了自己,你就可以取得这个。当你走进大楼的时候(试图从调用API获取资源),你会被要求验证你的护照,而不是在前台重新验证。
3、关于JWTs
JWTs是一份草案,尽管在本质上它是一个老生常谈的一种更加具体的认证个授权的机制。一个JWT被周期(period)分寸了三个部分。JWT是URL-safe的,意味着可以用来查询字符参数。(译者注:也就是可以脱离URL,不用考虑URL的信息)。
JWT的第一部分是一个js对象,表面JWT的加密方法。实例使用了HMAC SHA-266
{ "typ":"JWT", "alg":"HS256" }
在加密之后,这个对象变成了一个字符串:
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
JWT的第二部分是token的核心,他也是一个JS兑现,包含了一些信息。有一些是必须的,有一些是选择性的。一个实例如下:
{"iss":"joe", "exp":1300819380, "http://example.com/is_root":true}
这被称为JWT Claims Set。因为这篇文章的目的,我们将忽视第三个参数。但是你可以阅读这篇文章.这个iss是issuer的简写,表明请求的实体。通常意味着请求API的用户。exp是expires的简写,是用来限制token的生命周期。一旦加密,JSON token就像这样:
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
第三个也是最后一个部分,是JWT根据第一部分和第二部分的签名(Signature)。像这个样子:
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
在规范中,有一些选择性的附加属性。有iat表明什么时候token被半吧,nbf去验证在什么时间之前token无效,和aud去指明这个token的收件人是谁。
处理Tokens我们将用JWT simple模块去处理token,它将使我们从钻研如何加密解密中解脱出来。如果你有兴趣,可以阅读这篇说明,或者读这个仓库的源码。首先我们将使用下面的命令安装这个库。记住你可以在命令中加入--save,让其自动的让其加入到你的package.json文件里面。
npm install jwt-simple
在你应用的初始环节,加入以下代码。这个代码引入了Express和JWT simple,而且创建了一个新的Express应用。最后一行设定了app的一个名为jwtTokenSecret的变量,其值为‘YOUR_SECRET_STRING’(记得把它换成别的)。
var express=require('express');varjwt=require('jwt-simple'); var app=express();app.set('jwtTokenSecret','YOUR_SECRET_STRING');
获取一个Token我们需要做的第一件事就是让客户端通过他们的账号密码交换token。这里有2种可能的方法在RESTful API里面。第一种是使用POST请求来通过验证,使服务端发送带有token的响应。除此之外,你可以使用GET请求,这需要他们使用参数提供凭证(指URL),或者更好的使用请求头。
这篇文章的目的是为了解释token验证的方法而不是基本的用户名/密码验证机制。所以我们假设我们已经通过请求得到了用户名和密码:
/** * 验证token */ functionauthToken(credentialsRequired) { returncompose() .use(function(req,res,next) { if(req.query && req.query.hasOwnProperty('access_token')) { req.headers.authorization='Bearer '+ req.query.access_token; } next(); }) .use(expressJwt({ secret: config.session.secrets, credentialsRequired:credentialsRequired//是否抛出错误 })) } /** * 验证用户是否登录 */ functionisAuthenticated() { returncompose() .use(authToken(true)) .use(function(err,req,res,next) { //expressJwt 错误处理中间件 if(err.name==='UnauthorizedError') { returnres.status(401).send(); } next(); }) .use(function(req,res,next) { User.findById(req.user._id,function(err,user) { if(err)returnres.status(500).send(); if(!user)returnres.status(401).send(); req.user= user; next(); }); }); } /** * 验证用户权限 */ functionhasRole(roleRequired) { if(!roleRequired)throw newError('Required role needs to be set'); returncompose() .use(isAuthenticated()) .use(functionmeetsRequirements(req,res,next) { if(config.userRoles.indexOf(req.user.role) >= config.userRoles.indexOf(roleRequired)) { next(); } else{ returnres.status(403).send(); } }); }
下一步,我们就需要返回JWT token通过一个验证成功的响应。
注意到jwt.encode()函数有2个参数。第一个就是一个需要加密的对象,第二个是一个加密的密钥。这个token是由我们之前提到的iss和exp组成的。注意到Moment.js被用来设置token将在7天之后失效。而res.json()方法用来传递这个JSON对象给客户端。