0. 什么是Cookie和Session
“Cookie”这一概念由网景(Netscape)公司的程序员在为客户开发电子商务应用程序时创造1,由于客户不希望总是在服务器中保存事务状态,于是网景提出了Cookies的解决方案。
网景的cookie头字段为
Set-Cookie
,RFC 2965添加了一个Set-Cookie2
头字段,即“RFC 2965 cookie”[13][14],但Set-Cookie2
很少使用,终于2011年4月的RFC 6265中弃用[15],已经没有现代浏览器可以识别Set-Cookie2
头字段[16]。
Session代表服务器和客户端之间的一个会话,Session可以记录特定用户的各种数据和配置信息等。
0.0 为什么要有Cookie和Session?
所有技术的提出都是为了解决遇到的问题,Cookie和Session也不例外。我们常用的HTTP协议有个缺点,就是无状态(stateless)。
于1997年1月提出的RFC2068中提到HTTP/1.1协议是无状态的。
1
2
3
4
5
6
7
8 The Hypertext Transfer Protocol (HTTP) is an application-level
protocol for distributed, collaborative, hypermedia information
systems. It is a generic, stateless, object-oriented protocol which
can be used for many tasks, such as name servers and distributed
object management systems, through extension of its request methods.
A feature of HTTP is the typing and negotiation of data
representation, allowing systems to be built independently of the
data being transferred.
简单来说就是使用HTTP协议进行通信的客户端和服务端,服务端无法判断两次请求是来自同一客户端还是不同客户端,这样的话不同客户的不同网站配置及数据无法正确展示,于是有了Cookie和Session,客户端通过传递Cookie给服务端告诉服务端自己的用户标识信息。
(不过这不是一个好的实现方式,因为可能会有数据安全问题。例如第三方拿到了你的Cookie,用来访问此网站,可能会有数据泄密的风险。所以Cookie这里做了一些安全措施,具体后面会提到。同时,一般不要用Cookie传递敏感个人信息,如密码、隐私如手机号、姓名等,而是通过HTTPS等安全通信协议来传递敏感信息。)
0.1 Cookie和Session存放在哪里?
Cookie保存在客户侧的浏览器中。
查看Google主页请求,可以看到Google服务器返回了3个Set-Cookie
字段,包含1P_JAR
、AEC
、NID
等字段,这些字段都应该是Google服务器为当前浏览器生成的客户标识信息。
Session存放在Tomcat中,实现类为StandardSession。
0.2 Quick Cookie & Session
Cookie中有个几个固定字段,挨个介绍一下:
Cookie声明周期
Expires
:定义Cookie过期时间,类似的字段还有Max-Age
Max-Age
:定义Cookie最大有效时间
Cookie作用域
path
:Path
标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符%x2F
(“/“) 作为路径分隔符,子路径也会被匹配。例如,设置
Path=/docs
,则以下地址都会匹配:/docs
/docs/Web/
/docs/Web/HTTP
Domain
:Domain
指定了哪些主机可以接受 Cookie。如果不指定,默认为 origin,不包含子域名。如果指定了Domain
,则一般包含子域名。因此,指定Domain
比省略它的限制要少。但是,当子域需要共享有关用户的信息时,这可能会有所帮助。例如,如果设置
Domain=mozilla.org
,则 Cookie 也包含在子域名中(如developer.mozilla.org
)。
Cookie限制访问
Secure
:标记为Secure
的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端,因此可以预防 man-in-the-middle 攻击者的攻击,但这样仍然是不安全的,因为攻击者仍可能通过读取本地硬盘来读取Cookie内容。HttpOnly
:JavaScriptDocument.cookie
API 无法访问带有HttpOnly
属性的 cookie;此类 Cookie 仅作用于服务器。例如,持久化服务器端会话的 Cookie 不需要对 JavaScript 可用,而应具有HttpOnly
属性。此预防措施有助于缓解跨站点脚本(XSS) (en-US)攻击。
上面是几个常用的字段,如果需要完整的参考,参考这里。
1. 为什么要分布式Session?
如果服务是单服务器部署的话,分布式Session是不需要的,如下图。
但是如果是分布式服务,多个tomcat通过nginx服务器做负载均衡,就会出现问题。
- 请求1通过nginx服务器请求到了tomcat1,会在tomcat1内生成一个session,并将对应的JSESSIONID通过返回到client。
- 短时间内带着第一次返回的JSESSIONID的另一个请求2,通过nginx服务器可能会请求到另外2个tomcat服务器,而这两个服务器没有存储JSESSIONID对应的session,它们会以为这是个未登录请求,可能会报错或者重新发起登录,那就会出现问题。
解决这个问题有几个思路:
- 指定应用服务器:通过客户标识(例如uid)做hash,使得每次相同的客户端访问到特定的服务器。这种处理方式有个问题,就是如果其中一个机器挂了,挂在这个机器上的所有用户必须得重新登录。
- session复制:tomcat自己给定了机制可以实现session复制,通过配置可以实现多个tomcat之间的session复制,但由于大体积session或者session数据频繁变化等问题,导致性能很差,业界使用不广泛,此处不做介绍。
- session持久化:将session通过数据库(例如redis、memcached、mysql等,其中redis、memcached由于性能好,用的较多)进行持久化,之后每次访问都从数据库中拿取session信息,这样就可以保证session在集群环境下可以正常使用,但这种处理方式需要注意数据库的单点故障问题。
- token:token也是为了应对在集群环境下,session扩展性不好的问题。它的思路和其它session处理策略不一样,它的思路是把数据保存到客户端,每次请求是带上来,服务端进行校验,确认没问题后放行。常用的有JWT token,具体可参见阮一峰的这篇博客,此处只做简单介绍。
1.1 JWT token
整个JWT token的格式为:
1 | Header.Payload.Signature |
例如:
1 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjpbeyJuYW1lIjoiU3RldmUgSm9icyJ9LHsiam9iQ29kZSI6IjAwMDAwMSJ9XSwiaWF0IjoxNjYzNzczMjYyLCJleHAiOjE2NjQ1NTM1OTksImF1ZCI6IiIsImlzcyI6IiIsInN1YiI6IiJ9.ns4Ko5yrNlWtsv9sUyvX3fwNwYcShQH14wXymmB0FV4 |
Header和Payload均经过Base64Url算法编码。Base64Url算法相比Base64有一些不同:由于token有时候可能放到url中作为参数传递,Base64算法中有三个字符+
, =
和/
,由于在url中有特殊含义,需要替换掉。Base64Url算法会忽略掉=
,+
号替换成-
,/
号替换成_
。
其中Header及含义为:
1 | // 密文为eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 |
Payload为:
1 | // 密文为eyJkYXRhIjpbeyJuYW1lIjoiU3RldmUgSm9icyJ9LHsiam9iQ29kZSI6IjAwMDAwMSJ9XSwiaWF0IjoxNjYzNzczMjYyLCJleHAiOjE2NjQ1NTM1OTksImF1ZCI6IiIsImlzcyI6IiIsInN1YiI6IiJ9 |
密钥secret为JustATestForJwt
。
根据Header、Payload和密钥生成的签名(signature)为
1 | ns4Ko5yrNlWtsv9sUyvX3fwNwYcShQH14wXymmB0FV4 |
signature的生成方式为:
1 | HMACSHA256( |
Jwt的使用方式一般放在Cookie或者localStorage里,请求的时候放入request header中的Authorization字段中(放入Cookie中不能跨域)例如:
1 | Authorization: Bearer <token> |
或者直接放到post请求body中。
Jwt的问题
- 不能控制失效时间,签发后在expire date之前一直有效,除非更换secret,但是这样所有之前签发的token都会失效
- 其他人获取到token后就可以使用,所以为了安全,token有效期应该设置比较短,重要权限应该使用时重新认证。
- payload默认不加密,所以不要把敏感信息放到payload中。
总之,JWT token适合对于数据安全要求不是特别高、但是适当需要校验的业务场景,如果对数据安全要求比较高,那么不应该使用JWT token,而是RSA之类的加密算法。