Cookie 规范(RFC 6265)要点翻译

发布于 2022-06-12 15:52:41 字数 31169 浏览 1268 评论 0

1.简介

本文档定义了 HTTP Cookie 以及 HTTP 头的 Set-Cookie 字段。通过使用 Set-Cookie 头,一个 HTTP 服务器可以传递name/value键值对以及相对应的元数据(所谓的cookies)到user agent。当user agent向服务器发送后续请求时,user agent会根据元数据和其他信息来决定是否要在 Cookie 头中返回 name/value 键值对。

虽然表面上看起来很简单, 但时cookies有很多复杂的地方。例如,服务器在向user agent发送cookie时,对每个cookie会设定一个作用域。 作用域制定了user agent回传cookie的规则:cookie需要回传的最大期限,需要回传cookie到哪些服务器,以及需要应用到哪些模式的URI上。

由于历史原因,cookie包含许多在安全性和隐私上不恰当的地方。例如,服务器可以指定一个给出的cookie字段需要“安全的”连接,但是安全属性并没有保证在存在网络中间人攻击时cookie的完整性。相似的是,给定host的cookies将会被这个host上的所有端口共享,尽管通常来说,浏览器所用的“同源策略”会将从不同端口上取回的东西孤立开来。

这份标准有两类受众:会生产cookie的web服务器的开发者,以及会消费cookie的user agent的开发者。

为了最大化在user agent中的通用性,web服务器在生成cookie时应该把他们自身限制为一个在第4章定义的良好的实现者。

User agent必须实现比第5章中定义的更加宽松的规则,以达到最大化和现有的不符合第4章定义的良好实现者的服务器的互通性。

这份文档说明了在互联网上经常被使用的头的句法和语义。特别地,这份文档并没有创造新的句法和语义。对cookie生成的推荐标准在第4章提供,表述了一些现有服务器行为的子集,在第5章中表述了一些今天并不被推荐的句法和语义,更加宽松的cookie处理算法。某些现存软件的实现和推荐的协议有一些重大的不同,这份文档也包含了一份解释这些不同的内容。

在这份文档之前,至少存在着三份不同的cookie描述:所谓的“Netscape cookie 标准”,RFC 2109, RFC2965。然而,这些文档都没有描述Cookie和Set-Cookie头是如何在互联网上被使用的。根据之前IETF的HTTP状态管理机制的标准,这份文档请求下列操作:

[]将RFC2109的状态改为Historic(已经被RFC2965废止)
[]将RFC2965的状态改为Historic
[]指定RFC2965已经被这份文档废止

特别的是,通过将RFC2965移到Historic并且将其废止,这份文档反对使用Cookie2和Set-Cookie2头。

2.约定

2.1. 一致标准

此处略去几百字

2.2. 句法注解

本文档使用扩充巴科斯范式(ABNF),在RFC5234中有注释。

下列在RFC5234中定义的核心规则被引用,ALPHA(字母),CR(回车),CRLF(CR LF),CTLs(控制字符),DIGIT(数字0-9),DQUOTE(双引号),HEXDIG(十六进制元素 0-9/A-F/a-f),LF(换行符),NUL(空八比特),OCTET(除了NUL以外的所有八比特串),SP(空格),HTAB(水平制表符),CHAR(任意ascii码字符),VCHAR(任意可见的ascii码字符),以及WSP(空白符)。

OWS(可选空白符)规则被用在0个或更多的线性空白符可能出现的场合:

OWS = *([obs-fold]WSP)

obs-fold = CRLF

OWS应该要么不产生要么就产生为一个单独的SP字符。

2.3. 术语

下列术语:user agent,client,server,proxy,origin server(源服务器)和HTTP/1.1标准(RFC2616,1.3节)中的含义相同。

术语request-host是指host的名字,也就是已经被user agent所知的,对user agent来说发送HTTP请求的目的地,或者接收HTTP响应的来源。(也就是发送相应的HTTP请求的host名字)。

术语request-uri已经在RFC2616的5.1.2节中定义。

两个八比特的序列被称为大小写不敏感地相同,当且仅当他们在RFC4790中定义的大小写映射关系被满足时成立。

术语字符串意思是一个非NUL的八位bit序列。

3.概述

这一节概括了一种源服务器将状态信息传递给user agent的方式,也包含了一种user agent将状态信息回传服务器的方式。

为了存储状态,源服务器在HTTP响应中包含了一个Set-Cookie头。在后续的请求中,user agent将回传一个Cookie请求头到源服务器。Cookie头包含了user agent在前面Set-Cookie头中包含的cookie。源服务器可以选择忽略Cookie头或将Cookie用于应用所定义的目的。

源服务器可以在任何响应中发送Set-Cookie响应头。user agent可以在响应码为1xx的请求中忽略Set-Cookie,但必须在除此以外的任何种类响应中处理Set-Cookie(包括响应码为4xx和5xx的响应)。源服务器可以在单个请求的响应中包含多个Set-Cookie字段。Cookie或者Set-Cookie的出现不会阻止存储和复用HTTP请求的缓存。

源服务器不应该把多个Set-Cookie字段打包到单个HTTP头中。通常打包HTTP头的字段可能会更改Set-Cookie字段的语义,因为%x2c(",")字符被Set-Cookie使用,从而在这种打包方式中存在冲突。

3.1. 示例

使用Set-Cookie头,服务器可以向在一个HTTP响应中user agent发送一条短字符串,这条字符串会在未来符合cookie作用域的HTTP请求中回传给服务器。例如,服务器可以给user agent发送一个名叫“SID”的“session标识符”,值为 31d4d96e407aad42。user agent会在后续的请求中回传这个session标识符以及其值。

== Server -> User Agent ==

Set-Cookie: SID=31d4d96e407aad42

== User Agent -> Server ==

Cookie: SID=31d4d96e407aad42

服务器可以使用Path和Domain属性变更cookie的作用域。例如,服务器可以委托user agent在每个path每个example.com的子域都返回cookie。

 == Server -> User Agent ==

Set-Cookie:SID=31d4d96e407aad42;Path=/;Domain=example.com

 == User Agent -> Server ==

Cookie: SID=31d4d96e407aad42

就如下一个例子中展示的那样,服务器可以在user agent中存储多个cookie。例如,服务器可以通过返回两个Set-Cookie字段,实现既存储一个session标识符,又存储用户的偏好语言。值得注意的是,服务器用Secure和HttpOnly属性来对更加敏感的session标识符提供额外的安全保护(见4.1.2.)

 == Server -> User Agent ==

Set-Cookie: SID=31d4d96e407aad42; Path=/; Secure; HttpOnly
Set-Cookie: lang=en-US; Path=/; Domain=example.com

 == User Agent -> Server ==

Cookie: SID=31d4d96e407aad42; lang=en-US

注意上面的Cookie头包含了两个cookie,一个名叫SID,另一个为lang。如果服务器希望cookie在user agent的多个“会话“(sessions,例如,user agent重启之后)中持续存在,服务器可以在Expires属性中指定一个过期时间。注意,如果user agent的cookie存储超过它的定额或者用户手动删除了cookie的话,user agent可能会在过期时间到达之前删除cookie。

 == Server -> User Agent ==

Set-Cookie: lang=en-US; Expires=Wed, 09 Jun 2021 10:18:14 GMT

 == User Agent -> Server ==

Cookie: SID=31d4d96e407aad42; lang=en-US

最后,为了移除一个cookie,服务器要返回一个把过期时间设置在过去的Set-Cookie字段。服务器只有在Set-Cookie头中Path和Domain属性与创建cookie时相符时,才能成功删除cookie。

== Server -> User Agent ==

Set-Cookie: lang=; Expires=Sun, 06 Nov 1994 08:49:37 GMT

== User Agent -> Server ==

Cookie: SID=31d4d96e407aad42

4. 服务端的要求

本节描述了“表现良好”的Cookie和Set-Cookie头的句法和语义。

4.1. Set-Cookie

HTTP 响应头中的 Set-Cookie 被用于从服务器向 user agent 发送 cookie。

4.1.1 句法

不正式地说,Set-Cookie响应头包含了名字叫做“Set-Cookie”并跟着的一个“:”以及一个cookie。每个cookie由一个name-value键值对打头,后面跟着0个或者多个attribute-value键值对。服务器不应该发送未能遵从下列语法的Set-Cookie头。

 set-cookie-header = "Set-Cookie:" SP set-cookie-string  
                    ;Set-Cookie: 之后必须有空格,空格之后才是  
                    ;具体的set-cookie-string    

 set-cookie-string = cookie-pair *( ";" SP cookie-av )  
                    ;cookie-pair以及每个cookie-av  
                    ;之间分隔符都是";" SP(也就是分号加空格)  

 cookie-pair       = cookie-name "=" cookie-value   

 cookie-name       = token  
                    ;token表示的是除了分隔符和CTLs以外的ASCII字符  
                    ;分隔符包括:  
                    ;小中大尖括号 "("|")"|"[]"|"]"|"{"|"}"|"<"|">"  
                    ;空格和水平制表符 SP | HT  
                    ;逗号分号冒号引号问号等号 ","|";"|":"|"?"|"="|"""  
                    ;斜线: "" | "/"  
                    ;@: "@"  

 cookie-value      = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )

 cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E  
                       ; ASCII字符中除了CTRL(控制字符, 空白符  
                       ; 双引号, 逗号, 分号, 反斜线()  

 token             = <token, defined in [RFC2616], Section 2.2>  

 cookie-av         = expires-av / max-age-av / domain-av /  
                     path-av / secure-av / httponly-av /  
                     extension-av  

 expires-av        = "Expires=" sane-cookie-date  

 sane-cookie-date  = <rfc1123-date, defined in [RFC2616], Section 3.3.1>  

 max-age-av        = "Max-Age=" non-zero-digit *DIGIT  
                       ; In practice, both expires-av and max-age-av  
                       ; are limited to dates representable by the  
                       ; user agent.  

 non-zero-digit    = %x31-39  
                       ; digits 1 through 9  

 domain-av         = "Domain=" domain-value  

 domain-value      = <subdomain>  
                       ; defined in [RFC1034], Section 3.5, as  
                       ; enhanced by [RFC1123], Section 2.1  

 path-av           = "Path=" path-value  

 path-value        = <any CHAR except CTLs or ";">  

 secure-av         = "Secure"  

 httponly-av       = "HttpOnly"  

 extension-av      = <any CHAR except CTLs or ";">  

注意上面的参考文档中某些条目使用了一些和这份文档(ABNF,RFC5234)不同的语法标记。

这份文档没有定义任何关于cookie-value的语义。

为了最大化和user agent的兼容性,服务器在要使用一些任意的数据作为cookie-value时,应该将数据编码,例如使用Base64(RFC4648)。

set-cookie-string中由cookie-av项贡献的部分是被大家熟知的属性。为了最大化和user agent的兼容性,服务器不应该产生在set-cookie-string中有两个属性相同的名字的cookie。(关于user agent如何处理这种情况,见5.3节)

服务器不应该在同一个响应中包含超过一个具有相同cookie-name的Set-Cookie字段。(关于如何处理这种情况,见5.2节)

如果一个服务器向user agent并发地发送了多条包含Set-Cookie头的响应(例如,当在多个sockets上和user agent通信时),这些响应将会创造一个“竞争条件”,最终会导致不可预期的后果。

注意:一些现有的user agent对两位数的年份有不同的处理。为了避免兼容性问题,服务器应该使用要求四位数年份的RFC1123定义的日期格式。

注意:一些user agent会用32位的UNIX time_t来存储和处理日期。time_t相关的库的bug可能会导致这些user agent在2038年之后错误地处理日期。

4.1.2. 句法(非正式)

本节描述了简化的关于Set-Cookie头的语义。这些语义对于要理解最常见的服务器上cookie用法已经足够详细。全部地语义在第5节描述。

当user agent接收到一个Set-Cookie头时,user agent会将cookie及其属性一起存储。随后,当user agent发起HTTP请求时,user agent会在Cookie头中包含合适的并且没有过期的cookie。

如果user agent接收到了一个和某个现有cookie的cookie-name、domain-value和path-value都相同的新cookie,现有的那个cookie将会被驱逐,取而代之的是那个新cookie。注意服务器可以通过向user agent发送一个拥有值为过去某一时刻的Expires属性的新cookie,来删除一个cookie。

除非cookie的属性额外指定,cookie将只会回传到源服务器(例如,不会回传到任何子域上),并且cookie将会在当前会话结束时过期(会话由user agent自己定义)。user agent会忽略未被识别的cookie属性(但不会忽略整个cookie)。

4.1.2.1. Expires属性

Expires属性指明了cookie的最大生命周期,形式为cookie过期的时刻。user agent并不被要求在设定的时间之前保留cookie。实际上,user agent经常由于存储压力或者隐私上的考虑驱逐了cookie。

4.1.2.2. Max-Age属性

Max-Age属性指明了cookie的最大生命周期,形式为cookie过期之前的具体秒数。user agent并不被要求在这段指定的时长内保留cookie。实际上,user agent经常由于存储压力或者隐私上的考虑驱逐了cookie。

注意:某些现有的user agent并不支持Max-Age属性。不支持Max-Age属性的user agent将会直接忽略。

如果cookie既有Max-Age也有Expires属性,Max-Age属性将会有更高的优先级,并且控制cookie的过期时间。如果一个cookie既没有Max-Age也没有Expires属性,user agent将会在本次会话(会话由user agent定义)结束之前保留这个cookie。

4.1.2.3. Domain属性

Domain属性指明了cookie会被发送到哪些host。例如,如果某cookie的Domain属性的值为"example.com",user agent将会在向example.com,www.example.com以及www.corp.example.com(注意,最前面的%x2E("."), 如果出现,将会被忽略,尽管它除了出现在末尾以外都是非法的)发送HTTP请求时,在Cookie头中包含该cookie。如果服务器漏掉了这个Domain属性,user agent只会向源服务器返回cookie。

警告:某些现存的user agent会将不存在Domain属性时,错误地假设为Domain属性存在,并且值为当前的host name。例如,如果example.com返回了一个没有Domain属性的Set-Cookie头,这些user agent将会错误地也向www.example.com发送cookie。

user agent将会拒绝cookie,除非Domain属性为cookie指定的作用域会包含源服务器。例如,user agent将会接受一段来自“foo.example.com”的Domain属性为"example.com"或者"foo.example.com"的cookie,但是user agent不会接受Domain属性为"bas.example.com"或者"baz.foo.example.com"的cookie。

注意:出于安全的原因,许多user agent被设定为拒绝Domain属性对应为"公共结尾"的cookie。例如,一些user agent将会拒绝Domain属性为"com"或"co.uk"等。(详见5.3节)

4.1.2.4. Path属性

每个cookie的作用域被限定到了由path组成集合中,由Path属性控制。如果服务器没有提供Path属性,user agent将会使用当前的require-uri中path元素的“目录”作为默认值(更多细节详见5.1.4节)

user agent会在一次HTTP请求中包含该cookie,条件是require-uri中路径的部分匹配Path属性(或者是Path的子目录),其中%x2F("/")被解释为路径分隔符。

虽然这看起来对分隔同一host中不同路径的cookie十分实用,但是Path属性不能作安全的凭据。(见第8节)

4.1.2.5.Secure属性

Secure属性将cookie的作用域限定到“安全的”传输途径(“安全的”是有user agent所定义的)。当一个cookie拥有Secure属性时,user agent只有在请求是从一个安全的传输途径(典型的是以TLS方式传输HTTP,也就是HTTPS,RFC2818)传输时,才会发送该cookie。

尽管这看起来对保护cookie以免受中间人攻击很有用,但是Secure属性只在机密性上保护了cookie。一个网络中间攻击者可以通过非安全的传输途径覆盖该cookie,从而破坏其完整性。(详见8.6节)

4.1.2.6. HttpOnly属性

HttpOnly属性将cookie的作用域限制到HTTP请求中。尤其是,这个属性委托user agent在提供非HTTP方式访问cookie时(例如,浏览器提供给脚本访问cookie的接口),忽略该cookie。

4.2. Cookie

4.2.1. 句法

user agent会将存储的cookie放在Cookie头中发送给源服务器。如果服务器遵从4.1中的要求(并且user agent遵从第5节的要求),user agent将会发送符合下述语法的Cookie头部:

cookie-header = "Cookie:" OWS cookie-string OWS  

cookie-string = cookie-pair *( ";" SP cookie-pair )  

4.2.2. 语义

每个cookie键值对都表述了一个被user agent保存的cookie。cookie键值对包括从Set-Cookie头中接收到的的cookie-name和cookie-value。

注意cookie的属性没有被返回。尤其是,服务器不能单靠Cookie头部就能确定,什么时候cookie会过期,cookie对哪些host有效,对什么路径有效,还有cookie是否设置了Secure或者HttpOnly属性。

每个单独的cookie的语义没有在该文档中定义。服务器被期望以应用相关地语义来填充cookie。

虽然cookie在Cookie头中被线性地序列化,但是服务器不应该依赖序列化的顺序。尤其是,当Cookie头中包含了两个具有相同名字的cookie时(例如,被设置成不同Path或者Domain属性但拥有相同名字的cookie),服务器不应该依赖这些cookie在头部中出现的顺序。

5. User Agent的要求

本节用具体地细节说明了Cookie和Set-Cookie头部,使得实现这些要求的user agent可以和现存的服务器(即使那些不满足第4节中要求的)进行互操作。

5.1. 子元素算法

本节定义了user agent所用的用于处理Cookie和Set-Cookie头部子元素得一些算法。

5.1.1. 日期

user agent必须使用一个和下述算法等价的算法来实现解析cookie-date。注意下述的各种被定义为算法一部分的boolean-flag(例如,found-time,found-day-of-month, found-month,found-year)最初状态是“没有设定”)。

1.使用下述算法,将cookie-date分割成date-token

cookie-date     = *delimiter date-token-list *delimiter  
date-token-list = date-token *( 1*delimiter date-token )  
                  ;date-token由终结符分隔  
date-token      = 1*non-delimiter  

delimiter       = %x09 / %x20-2F / %x3B-40 / %x5B-60 / %x7B-7E  

non-delimiter   = %x00-08 / %x0A-1F / DIGIT / ":" / ALPHA /%x7F-FF  
                  ;此处的非终结符指的就是非ascii,以及ascii码中的不可见字符  
                  ;以及字母和数字,以及:  
                  ;也就是说非终结符在可见ascii字符中只包括数字字母和冒号  

non-digit       = %x00-2F / %x3A-FF  

day-of-month    = 1*2DIGIT ( non-digit *OCTET )  
month           = ( "jan" / "feb" / "mar" / "apr" /  
                       "may" / "jun" / "jul" / "aug" /  
                       "sep" / "oct" / "nov" / "dec" ) *OCTET  

year            = 2*4DIGIT ( non-digit *OCTET )  
time            = hms-time ( non-digit *OCTET )   
                ;可以看出时间之后必须要有一个数字来分隔  
                ;但是再往后是什么都无所谓了  
hms-time        = time-field ":" time-field ":" time-field  
time-field      = 1*2DIGIT  

2.按照下述顺序出列每个在cookie-date中出现的token:

1. 如果found-time的flag还没有设定并且token匹配了time产生式,设定found-time的flag,并且相应地  设定hour、minute、second为token所表示的值。跳过下面剩余的步骤,并继续处理下一个  date-token。  
2. 如果found-day-of-month的flag还没有被设定,并且token匹配了day-of-month的产生式,设定  found-day-of-month的flag,并且相应地设定day-of-month为token所表示的值。跳过下面剩余的步骤,  并继续处理下一个date-token。
3. 如果found-month的flag还没有被设定,并且token匹配了month的产生式,设定found-month的flag,  并且相应地设定month为token所表示的值。跳过下面剩余的步骤,并继续处理下一个date-token。  
4. 如果found-year的flag还没有被设定,并且token匹配了year的产生式,设定found-year的flag,  并且相应地设定year为token所表示的值。跳过下面剩余的步骤,并继续处理下一个date-token。

3.如果year的值大于70且小于等于99,在year的值的基础上增加1900.

4.如果year的值大于0且小于等于69,在year值得基础上增加2000.

  1. 注意:某些现有的user agent会用不同的方式解析两位数的年份。

5.在下列情况中,终止这些步骤并且解析cookie-date以失败告终:

  • [*] 至少在 found-day-of-month,found-month,found-year,或者 found-time 的 flag 中有一个没有设定
  • [*] day-of-month的值小于1或者大于31
  • [*] year的值小于1601
  • [*] hour的值大于23
  • [*] minute的值大于59
  • [*] second的值大于59

注意在这个句法中闰秒不能被体现

6.将解析出的 cookie-date 转换为 UTC 时间。如果这个日期不存在,终止算法并且以失败告终。

7.将转换出的时间作为算法的结果返回。

5.1.1. 规范化 host 名

一个规范的host名是通过下述算法生成的字符串:

1. 将host名转换为独立域名标签序列.  
2. 将每个不属于LDH非保留字标签的标签,转换为A标签详见[RFC5890] 2.3.2.1),或者转换为“punycode 标签”(RFC3490的第四节所定义的“TOASCII”方法).  
3. 将最终的标签序列拼接,中间以%x2E(".")分隔.  

5.1.3. Domain 匹配

只有在满足下列条件之一时:

A. domain 字符串和给定的字符串相同。(注意两者会被规范化为
小写之后进行比较)  
B. 当满足下列所有条件时:  
    [*] domain字符串是给定字符串的一个尾部子串  
    [*] 给定字符串不包含在domain字符串中的最后一位字符是%X2E(".")  
    [*] 该字符串是一个host name(也就是说,不是一个ip地址)

给定的字符串才匹配给定的domain字符串的domain。

注意此处Domain是指hostname,是不包含端口号的

5.1.4. 路径和路径匹配

user agent必须使用和下述算法等价的算法来计算cookie的默认路径:

1. 将uri-path设定为require-uri中的path部分,如果path不存在, 则设为空。  
例如,如果request-uri刚好包含了path(以及可选的query string),那么  
uri-path就是该path(不包括%X3F("?")字符或者query string),而当request-uri  
包含一个完整的绝对路径时,uri-path就是URI的path元素。  

2. 如果uri-path为空,或者uri-path的第一位不是%X2F("/")字符,输出%x2F并且  
跳过剩余的步骤。  

3. 如果uri-path只包含不超过一个%x2F("/"),输出%x2F并且  
跳过剩余的步骤。  

4. 输出uri-path从左边数第一位到最后一个%x2F("/")之间的字符串,但是结果不  
包含最后一个%x2F("/")

至少满足下面的一个条件时,request-path才匹配cookie-path的路径:

[*] cookie-path和request-path相等  
[*] cookie-path是request-path的前缀,并且cookie-path的最后一位是%x2F("/")  
[*] cookie-path是request-path的前缀,并且第一个不包含在cookie-path的
字符是%x2F("/")

5.2. Set-Cookie 头

当user agent在一次HTTP响应中接收到一个Set-Cookie字段时,user agent可能会完全会略Set-Cookie字段。例如,user agent可能希望禁止“第三方”cookie(见7.3节)。

如果user agent没有完全忽略Set-Cookie字段,那么user agent必须解析将Set-Cookie头中每个字段的值作为cookie-string解析。(下面将定义)

注意:下述算法比4.1节中的语法更加宽松。例如,该算法会将cookie的name-value的头尾空白符
移除(但保留中间的空白符),不过4.1节中的语法禁止了这些地方的空白符。user agent使用这个算法来和哪些不符合第四节中的推荐的服务器进行互操作。

user agent必须使用和下面所述的算法等价的算法来解析set-cookie-string:

1. 如果set-cookie-string包含%x3B(";"), 那么name-value-pair由set-cookie-string  
开头到第一个%x3B(“;”)组成,但不包含%x3B,并且unparsed-attributes由剩下的set-cookie-string  组成(包括%x3B); 否则,name-value-pair就是set-cookie-string,并且  
unparsed-attributes时空串。  

2. 如果name-value-pair缺少%x3D("=")字符,那么完全忽略set-cookie-string。      
3. name字符串(可能为空)由name-value-pair的开头,到第一个%x3D("=")组成,但不包含%x3D,  同时value(可能为空)由第一个%x3D之后的所有字符组成。    

4. 移除name和value的开头和结尾处的所有WSP(空白符)。    

5. 如果name为空,完全忽略set-cookie-string。    

6. cookie-name就是name字符串,cookie-value就是value字符串。  

user agent必须使用和下面所述的算法等价的算法来解析unparsed-attributes:

1. 如果unparsed-attributes为空,跳过剩下的这些步骤。    

2. 丢弃unparsed-attributes的第一个字符串(也就是%x3B)。    

3. 如果剩下的unparsed-attributes中包含%x3B(“;”),那么消耗从开头到第一个%x3B  
的字符串(但不包含);否则,消耗剩下的所有unparsed-attributes。之后,令  
这个步骤中消耗的字符串为cookie-av。  
4. 如果cookie-av包含一个%x3D(“=”),那么attribute-name(可能为空)由开头  
到第一个%x3D组成,并且不包含%x3D,同时attribute-name(可能为空)为第一个  
%x3D之后的字符串;否则,attribute-name的值为整个cookie-av,并且attribute-value  
为空。  

5. 移除attribute-name和attribute-value的首尾空格。    

6. 根据下面子章节所述的要求解析attribute-name和attribute-value(注意,未被  
识别的attribute-name将会被忽略)。  

7. 回到算法的第一步。

当user agent完成对set-cookie-string的解析时,就说 user agent 从 require-uri 中获取到了名字为 cookie-name,值为cookie-value,以及属性为cookie-attribute-list的cookie。(由接收到cookie所触发的额外的要求详见5.3节)

5.2.1. Expires属性

如果attribute-name大小写不敏感地匹配了字符串 Expires,user agent 必须按照下列步骤处理 cookie-av。

令expiry-time的值为将attribute-value按cookie-date(见5.1.1节)解析后的值。

如果attribute-value没有成功地解析成一个cookie date,那么忽略这个cookie-av。

如果expiry-time晚于user agent可以表达的最晚的时间,user agent可以将 expiry-time 替换为 user agent 可以表达的最晚的时间。

如果expiry-time早于user agent可以表达的最早的时间,user agent可以将 expiry-time 替换为能表达的最早的时间。

向cookie-attribute-list追加一个属性名为Expires,属性值为expiry-time
的属性。

5.2.2. Max-Age属性

如果attribute-name大小写不敏感地匹配了字符串“Max-Age”,user agent 必须按照下列步骤处理cookie-av。

如果attribute-value的第一个字符不是DIGIT(数字)或者“-”,那么忽略cookie-av。

如果attribute-value剩余的字符中包含一个非DIGIT(非数字)字符,那么忽略这个cookie-av。

令delta-seconds为attribute-value转换为整数之后的值。

如果delta-seconds小于或等于0,令expiry-time为最早可以表达的日期和时间。否则,令expiry-time为当前的日期和时间加上delta-seconds的秒数。

向cookie-attribute-list追加一个属性名为Max-Age,属性值为expiry-time的属性。

5.2.3. Domain属性

如果attribute-name大小写不敏感地匹配了字符串“Domain”,user agent必须按照下列步骤处理cookie-av。

如果attribute-value是空值,那么行为是未知的。然而,user agent应该忽略整条cookie-av。

如果attribute-value的第一个字符是%x2E("."),令cookie-domain为除去attribute-value第一个%x2E(".")之后的值;否则,令cookie-domain的值为整个attribute-value。

将cookie-domain转换为小写。

向cookie-attribute-list追加一个属性名为Domain,属性值为cookie-domain的属性。

5.2.4. Path属性

如果attribute-name大小写不敏感地匹配了字符串“Path”,user agent必须按照下列步骤处理cookie-av。

如果attribute-value时空值或者attribute-value的第一个字符不是%x2F(“/”),那么令cookie-path为默认路径;否则,令cookie-path为整个attribute-value。

向cookie-attribute-list追加一个属性名为Path,属性值为cookie-path的属性。

5.2.5. Secure属性

如果attribute-name大小写不敏感地匹配了字符串“Secure”,user agent必须向cookie-attribute-list追加一个属性名为Secure,属性值为空的属性。

5.2.5. HttpOnly属性

如果attribute-name大小写不敏感地匹配了字符串“HttpOnly”,user agent必须向cookie-attribute-list追加一个属性名为HttpOnly,属性值为空的属性。

5.3. 存储模型

user agent的每个cookie会存储下列所述的字段:name,value,expiry-time,domain,path,creation-time,last-access-time,persistent-flag,host-only-flag,secure-only-flag,以及http-only-flag。

当user agent从一个request-uri接受了一个拥有名叫cookie-name,值为cookie-value,以及属性为cookie-attribute-list的cookie时,user agent必须像下面这样处理cookie:

1. user agent可能完全忽略某个接收到的cookie。例如,user agent可能希望禁止掉来自  
第三方的cookie,或者user agent不希望存储超过某个大小的cookie。   

2. 新建一个名字为cookie-name,值为cookie-value的新cookie。将creation-time和  
last-access-time设定为当前日期和时间。  

3. 如果cookie-attribute-list包含一个属性名为"Max-Age"的属性,那么将cookie  
的presistent-flag设为true,将cookie的expiry-time设定为cookie-attribute-list中  
最后一个属性名为Max-Age的属性的属性值; 

否则,如果cookie-attribute-list中包含一个属性名为“Expires”的属性  (并且不包含“Max-Age”属性) ,那么将cookie的presistent-flag设定为true,并且将cookie的expiry-time  
设定为cookie-attribute-list中最后一个属性名为Expires的属性值;    

否则,将cookie的presistent-flag属性设为false,并且将cookie的expiry-time设定为  
能表达的最远的日期。  

4. 如果cookie-attribute-list包含一个属性名为“Domain”的属性,令domain-attribute  
为cookie-attribute-list中最后一个属性名为Domain的属性值;否则,令domain-attribute为空串。    

5. 如果user agent被配置为拒绝“公共后缀”,并且domain-attribute的值为某个  
公共后缀时:如果domain-attribute和规范化后的request-host相同的话,令domain-  
attribute属性为空串;否则,忽略整个cookie病跳过这些步骤。  

> 注意: “公共后缀”是一个由公开注册机构控制的域名,比如说“com”,"co.uk",以及
> "pvt.k12.wy.us"。一个步骤对于预防从attacker.com,通过设置一个Domain属性
> 为"com"的cookie,来破坏example.com的cookie完整性很重要。不幸的是,
> 公共后缀(著称的注册商控制的域名)的集合随时间发生着变化。如果可能的话,user agent
> 应该用一份最新的公共后缀列表,例如有Mozillas维护的一份列表(https://publicsuffix.org/)。  

6. 如果domain-attribute非空:如果规范化之后的request-host不匹配domain-attribute  
中的域名,那么完全忽略掉cookie并且终止这些步骤;否则,将cookie的host-only-flag  
设定为false,并且将cookie的domain设定为domain-attribute。    
否则:将cookie的host-only-flag设定为true,并且将domain设定为规范化之后的request-host。    

7. 如果cookie-attribute-list包含一个属性名为“Path”的属性,将cookie的path  
属性设定为cookie-attribute-list中最后一个属性名为“path”的属性值;否则,将  
cookie的path设定为request-uri的默认路径。(5.1.4节)    

8. 如果cookie-attribute-list包含一个属性名为“Secure”的属性时,将cookie的  
Secure-only-flag设定为true;否则,设为false。    

9. 如果cookie-attribute-list包含一个属性名为“HttpOnly”的属性时,将cookie的  
http-only-flag设定为true;否则,设为false。  

10. 如果cookie是从非HTTP的API传入的,并且设定了cookie的http-only-flag,那么  
终止这些步骤并且完全忽略该cookie。  

11. 如果cookie的存储中已经包含了一个和新建的cookie的name、domain、path  
都相同的cookie,那么:a. 令old-cookie为现存的domain、name、path都相同的  
cookie(注意这个算法保持了至多只有一个这样cookie的不变性);b. 如果新创建的  
cookie是从一个非HTTP的API接收到的并且old-cookie的http-only-flag已经设定,  
那么终止这些步骤并且完全忽略这个新创建的cookie;c. 将新创建的cookie的creation-time  
更新为old-cookie的creation-time;d. 将old-cookie从cookie 存储中移除。    

12. 将新创建的cookie插入到cookie存储中。

当cookie的过期时间是在过去时,这个cookie就是“过期的”。

user agent必须从cookie存储中清除掉所有的过期cookie,条件为在任何时刻,cookie存储中存在一个过期的cookie时。

在任何时刻,user agent都可能会从cookie存储中“移除过量的cookie”,条件为共享同一个domain字段的cookie的数量超过了预设的边界值(例如50个cookie)。

在任何时刻,user agent都可能会从cookie存储中“移除过量的cookie”,条件为所有cookie的总数量超过了预设的边界值(例如3000个cookie)。

当user agent从cookie存储中移除过量的cookie时,user agent必须按下面的优先级清除cookie:

  1. 过期的cookie
  2. 共享一个Domain的cookie数目超过其他cookie预设的一个数量(?)时
  3. 所有的cookie

如果两个cookie具有相同的移除优先级,那么user agent必须先移除last-access更早的那个cookie。

当“当前会话结束”(由user agent定义)时,user agent必须从cookie存储中
移除所有persisitent-flag属性为false的cookie。

5.4. Cookie头

user agent将会在HTTP请求的Cookie头中包含存储的cookie。

当user agent产生一个HTTP请求时,user agent禁止产生多余一个的Cookie头字段。

user agent可能会在HTTP中删除Cookie头部。例如,user agent可能希望禁止
发送从第三方请求中获取到的cookie。(见7.1节)

如果user agnet已经把一个Cookie头部字段添加到HTTP头部中,user agent必须把cookie-string(由下面定义)当做这个头部字段的值发送。

user agent必须用一个等价于下面所述的算法来从cookie存储中结合request-uri,来
计算“cookie-string”:

1. 令cookie-list为cookie存储中符合下面所有要求的cookie的集合:
    [*] 要么,cookie的host-only-flag为true,并且规范化后的request-host和
        cookie的域名相同;或者,cookie的host-only-flag为false,并且规范化后
        的request-host和cookie的domain匹配;  
    [*] request-uri的path部分和cookie的path部分相匹配;  
    [*] 如果cookie的secure-only-flag为true, 那么request-uri的scheme
    必须表示一个“安全的”协议(由user agent定义)。  
    > 注意:“安全的”协议不是由本文档定义的。典型的情况是,user agent会将使用
    安全传输的协议认为是安全的,比如SSL或者TLS。例如,大多user agent会将“https”
    看作是表示安全协议的scheme。  
    [*] 如果cookie的http-only-flag被设定为true,那么这个cookie将会在为“非HTTP”
    接口(由user agent定义)生产cookie-string时被忽略。   

2. user agent应该按照下列顺序对cookie-list进行排序:
    [*] 拥有更长的path的cookie将会排在拥有更短的path的cookie前面。  
    [*] 当cookie拥有相同的path字段长度时,拥有更早creation-time的cookie将会
    被排在更晚creation-time的cookie前面。  
    > 注意:不是所有的user agent都按这个吮吸排列cookie-list,但是这个顺序
    反应了在撰写本文档时以及历史上,现有的服务器(错误地)所依赖的顺序的经验。    

 3. 将cookie-list中每个cookie的last-access-time更新为当前的时刻。  

 4. 将cookie-list序列化为一个cookie-string,需要按照下列顺序处理每个cookie:
    1. 输出cookie的名字,%x3D("=")字符,以及cookie的值.
    2. 如果在cookie-list中还存在未处理的cookie,输出字符%x3B以及%x20("; ").
 >注意:除了它的名字,cookie-string其实是一个八bit序列,而不是字符序列。为了将
 cookie-string(或者其中的元素)转换为字符序列(例如,为了展示给用户),user agent
 可能希望尝试使用UTF-8编码(RFC3629)来解码这个八bit序列。但是,这个解码可能会失败,因为
 不是所有的八bit序列都是合法的UTF-8.

6. 实现上的考虑

6.1. 限制

实际的user agtn实现在他们能存储的cookie的数目和大小上有限制。常见的user agent应该提供下述的最小容量:

  • [] 每个cookie至少4096bytes(以cookie的name,value和attribute之和衡量)
  • [] 每个domain至少50个cookie
  • [*] 至少总共3000个cookie

服务器应该使用尽可能小和尽可能小的cookie来防止达到这些实现上的限制,以及最小化网络带宽的需求,因为Cookie头会在每一个请求中都被包含。

如果user agent未能在Cookie头中返回一个或者多个cookie,服务器应该优雅地处理这种情况,因为user agent随时可能会按照用户的要求移除任何cookie。

6.2. API

Cookie和Set-Cookie头使用一个这么难懂的句法的原因是,许多平台(包括服务器和user agent)都提供了一个基于字符串的有关cookie的API,这就要求应用层的开发者自己生成和解析Cookie和Set-Cookie头,这导致许多程序员错误地实现了,最终导致不兼容问题。

为了替代提供基于字符串的cookie相关API的做法,平台最好提供更加语义化的API。推荐详细的API设计超过了本文档的范畴,但是显然接收一个抽象的"Date"对象而不是一段序列化的date string会有清晰的好处。

6.3. IDNA依赖与迁移

IDNA2008(RFC5890)取代了IDNA2003(RFC3490)。然而,在两份说明中存在差异,因此处理(比如,转换)从一个标准下注册的域名标签到另一个存在差异。IDNA2003存在的过渡时期会有一定的时常。user agent应该实现IDNA2008(RFC5890)并且可能会实现UTS46或者RFC5895以加快IDNA的过渡。如果user agent没有实现IDNA2008,那它应该实现IDNA2003(RFC3490)。

7. 隐私的考虑

cookie因为允许服务器追踪用户饱受批评。例如,很多“web分析”公司使用cookie来识别用户是否回到了网站或者访问了另一个站点。虽然cookie不是服务器唯一可以用来追踪跨HTTP请求的手段,但是cookie促进了追踪,因为他们在user agent的会话之间可以一直存在,并且可以被多个host共享。

7.1. 第三方cookie

特别令人担忧的是所谓的“第三方”cookie。在渲染一个HMTL文档时,user agent经常从其他服务器请求资源(例如广告网络)。这些第三方服务器可以使用cookie来跟踪用户,尽管用户没有直接访问他们的服务器。例如,如果一个用户访问了一个带有第三方内容的网站,之后用户浏览另一个包含这个内容的网站时,第三方可以跨域两个站点追踪用户。

一些user agent限制了第三方cookie的行为。例如,其中一些user agent拒绝在向第三方的请求中发送Cookie头部。另外一些则是拒绝处理第三方请求的响应中的Set-Cookie头部。user agent在处理的第三方cookie策略方面有很多不同。本文档允许user agent在很大范围内尝试第三方cookie的策略,来满足用户对隐私和兼容性的需求。然而,本文档并不偏向于任何特别的第三方cookie策略。

禁止第三方cookie的策略,当服务器尝试绕过这个追踪用户的限制来实现他们隐私追踪时,是无效的。
尤其是,两个协作的服务器经常通过动态url而不是cookie添加识别信息,来追踪用户的时候。

7.2. 用户控制

user agent应该提供一种给用户管理存储在cookie存储中cookie的机制。例如,user agent可能让用户删除一段时间内的所有cookie或者和某个domain相关的所有cookie另外,许多user agent都包含了一个让用户检查存储在cookie store中的cookie的界面。

user agent应该提供一种可以让用户禁用cookie的机制。当cookie被禁用时,user agent禁止在发出的HTTP请求中包含Cookie头部,并且user agent处理收到的HTTP响应中的Set-Cookie头部。

一些user agent提供了阻止cookie跨session存储的选项。当这个被配置到的时候,user agent必须将所有收到的cookie当做persistent-flag设定为false进行对待。一些流行的user agent通过“匿名浏览模式”来开放这个功能。

某些user agent提供了允许用户自己写入cookie的能力。在大多数常见的场景中,这会产生大量的对话框。然而,尽管如此,某些看中隐私的用户认为这个功能很有用。

7.3. 过期时间

虽然服务器可以将cookie的过期时间设定到一个遥远的未来,但是绝大多数user agent实际上并不会将cookie保留几十年。与其选择无厘头的很长的国务时间,服务器应该,基于实际目的来选择一个合适的cookie过期时间,以提高用户的隐私性。例如,一个典型的cookie标识符应该设置成过期时间为两周比较合理。

8. 安全考虑

8.1. 概述

cookie可能有很多安全陷阱。本节概述了几个比较显著的问题。

尤其是,cookie鼓励开发者依赖Ambient Authority做认证,往往提供了被攻击的弱点,比如说跨站请求伪造(CSRF)。同时,当在cookie中存储session标识符时,开发者往往也留下了session fixation的隐患。

传输层加密,例如使用HTTPS,并不足以防御网络攻击者获得或者更改受攻击者的cookie,因为cookie协议本身有很多脆弱性(见下面的“弱机密性”和“弱完整性”)。另外,默认情况下,cookie不能从网络攻击者那儿获得完整性和机密性,甚至在使用HTTPS时。

8.2. 环境授权(Ambient Authority)

使用cookie来认证用户的服务器可能会承受安全上的脆弱性,因为一些user agent允许远程组织从user agent发送HTTP请求(例如,通过HTTP重定向或者HTML表单)。当处理这些请求时,user agent会附上这些cookie,尽管远程组织不知道cookie的内容,但是也潜在地允许远程组织利用在
没有防备性的服务器的授权。

尽管这个安全担忧有过许多名字(比如,跨站请求伪造,confused deputy),这个问题起源于将cookie作为一种环境授权(Ambient Authority)。cookie鼓励服务器的管理员将名称(用URL的形式)和授权(cookie)中分离。结果是,user agent可能会向攻击者指定的地址进行授权,可能导致服务器和客户端承受由攻击者指定的动作,因为他们曾经被用户授权。

服务器管理员可能会考虑通过将URL作为授权表,将名字(URL)和授权绑在一起,来作为取代使用cookie作为授权的手段。取代将secret存在cookie的是,这个方法将secret存在URL中,要求远程
实体自己提供授权。虽然这个方法不是万能药,审慎的运用这个原则可以获得更好的安全性。

8.3. 明文

除非以安全的途径传输(例如TLS),在Cookie和Set-Cookie字头是以明文传输的。

  1. 这些头部中所有传输的敏感信息都曝光给了窃听者。
  2. 一个怀有恶意的中间人可以更改任一方向中的头部,带来不可预知的后果。
  3. 一个怀有恶意的客户端可以在传输之前更改Cookie头部,带来不可预知的后果。

当传输到user agent时(就算在安全的隧道发送cookie也是),服务器应该加密并签名cookie的内容(使用任何服务器想使用的格式)。然而,加密并签名的cookie内容并没有预防攻击者将cookie从一个user agent转移到另一个,或者之后重放cookie进行攻击。

签名和加密每个cookie的内容之外,要求一个更高安全等级的服务器应该只在一个安全的隧道中使用Cookie和Set-Cookie头。当使用安全渠道的cookie时,服务器应该设定每个cookie的Secure属性。如果服务器没有设置Secure属性,由安全隧道提供的保护将会很大程度上的没有意义。

例如,考虑一个webmail服务器,将session标识符存在一个cookie钟,并且通过HTTPS访问。如果服务器没有在它的cookie上设定安全属性,一个活跃的网络攻击者将可以拦截任何由user agent发出的HTTP请求并且将其请求重定向到向HTTP上的webmail服务器。就算webmail服务器没有监听HTTP连接,user agent也会在该请求中包含cookie。这个活跃的网络攻击者拦截这些cookie,并且向服务器重放攻击,之后获取到用户的邮件内容。如果,取而代之,服务器在cookie中设定了Secure属性,
user agent将会在明文请求中包含这个cookie。

8.4. session标识符

服务器一般在cookie中存储一个nonce(或者session标识符)来代替,直接在cookie中存储session信息(可能会被攻击者获得或者重放)。当服务器收到一个拥有nonce的HTTP请求时,服务器会把nonce当做key查找和cookie相关联的状态信息。

使用session标识符限制了攻击者可以造成的危害,如果攻击者拿到了cookie的内容,因为nonce是唯一一个和服务器交互的有用信息(不像非nonce得cookie内容,其本身就是敏感的)。而且,使用单个nonce避免了攻击者将两个交互中的内容混杂起来,造成服务器不可预期的行为。

使用session标识符也不是完全没有风险。例如,服务器应该避免“session fixation”造成的易受攻击性。固化session攻击以下面三个步骤进行。首先,攻击者将session标识符从他的user agent中
移植到受害者的user agent中。第二,受害者使用这个session和服务器交互,可能会用用户信息填充这个session标识符。第三,攻击者直接使用这个session标识符和服务器交互,可能会获得授权信息或者机密信息。

8.5. 弱机密性

cookie没有提供端口隔离。如果一个cookie在一个端口上是可读的,那么这个cookie对另一个运行在同一个服务器上不同端口的服务来说也是可读的。如果一个cookie在一个端口上是可写的,那么这个cookie对另一个运行在同一个服务器上不同端口的服务来说也是可写的。出于这个原因,服务器不应该在具有相同host的不同端口上运行相互不信任的服务,并且用cookie来存储安全敏感信息。

cookie没有提供协议带来的隔离性。虽然绝大多数通常都使用http和HTTPS协议,给定host的cookie也可能被其他协议获取到,例如ftp和gopher。虽然缺少协议隔离性,对从非HTTP的API获取cookie的权限是显著的,但是实际上由协议导致的隔离性缺失表现在了他们需要自己处理cookie(例如,考虑通过HTTP取回一个使用gopher协议的URI)。

cookie往往也没有提供基于path的隔离性。虽然网络层的协议并没有将存储在一个path的协议发到另一个,但是某些user agent通过非HTTP的api暴露了这些cookie,例如HTML的document.cookie API。因为其中的某些user agent(例如,浏览器)并没有将从不同path获得的资源隔离起来,从一个path取回的某个资源也可能可以拿到存储到另一个path的cookie。

8.6. 弱完整性

cookie没有为兄弟域名(极其子域名)提供完整性保障。例如,考虑foo.example.com和bar.example.com。foo.example.com服务器可以设置一个Domain属性为"example.com"的cookie(可能覆盖一个现有的由bar.example.com设置的"example.com"的cookie)。并且user agent会在想bar.example.com的HTTP请求中包含这个cookie。在最坏的情况下,bar.example.com将不能把这个cookie和一个自己设定cookie区分开来。foo.example.com服务器可能会利用这个能力来发起对bar.example.com的攻击。

尽管Set-Cookie头支持Path属性,path属性也没有提供任何完整性保证,因为user agent将会接受一个Set-Cookie头部的任意路径。例如,一个对 http://example.com/foo/bar 的HTTP响应,可以设置一个Path属性为"/qux"的cookie。因此,服务器不应该在相同host上的不同path运行两个相互不信任的服务,并且使用cookie来存储安全敏感信息。

某个活动的网络攻击者也可以向发送到 https://example.com/ 的请求中的Cookie头部注入cookie,方法是仿造一个 http://example.com/ 的响应并且注入一个Set-Cookie头。位于example.com的HTTPS服务器将不能分辨gaicooie是否是他自己在HTTPS响应中设置的cookie。一个活跃的网络攻击者可能会通过这个机制来攻击example.com,即使example.com只使用了HTTPS。

译者注:例如,用户直接敲了一个HTTP站点,www.baidu.com 在重完成定向到HTTPS之前,中间人可以伪造跳转,比如返回一个302跳转到 https://www.baidu.com 同时中间人设定一个 cookie。

服务器可以通过加密和签名他们的 cookie 来缓和这些攻击。然而,使用密码学并没有完全缓解这个问题,因为一个攻击者可以重放TA从真实的 example.com 服务器中的 sessuib 获取到的 cookie,以导致不可预期的后果。

最后,一个攻击者可以通过存储大量 cookie 来强迫 user agent 删除掉 cookie。一旦 user agent 达到了它的存储限制,user agent 会被迫驱除掉某些 cookie。服务器不应该依赖 user agent 对 cookie 的保留。

8.7. 依赖 DNS

cookie 依赖 DNS 系统来提供安全性。如果 DNS 部分或全部受损,cookie 协议可能会无法提供应用所要求的安全属性

9. IANA 考虑

永久的消息头部注册(RFC 3864)已经被下列登记项目更新。

9.1. Cookie

  • 头部名称: Cookie
  • 应用协议: http
  • 状态: 标准
  • 作者/变化控制者: IETF
  • 标准文档: 本文档(5.4节)

9.2. Set-Cookie

  • 头部名称: Set-Cookie
  • 应用协议: http
  • 状态: 标准
  • 作者/变化控制者: IETF
  • 标准文档: 本文档(5.2节)

9.3. Cookie2

  • 头部名称: Cookie2
  • 应用协议: http
  • 状态: 已淘汰
  • 作者/变化控制者: IETF
  • 标准文档: RFC2965

9.4. Set-Cookie2

  • 头部名称: Set-Cookie2
  • 应用协议: http
  • 状态: 已淘汰
  • 作者/变化控制者: IETF
  • 标准文档: RFC2965

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

0 文章
0 评论
22 人气
更多

推荐作者

已经忘了多久

文章 0 评论 0

15867725375

文章 0 评论 0

LonelySnow

文章 0 评论 0

走过海棠暮

文章 0 评论 0

轻许诺言

文章 0 评论 0

信馬由缰

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文