实验
下面通过两个大略的实验,来看Java和Php在获取web要求中的cookie的不同之处,我下面贴出http要求的干系信息,和做事端输出的结果。
Java

要求信息
GET / HTTP/1.1Host: localhost:7003...Cookie: test2=ab+cd; test1=ab%2Bcd
做事端
@Controller@Slf4jpublic class MainController { @Autowired private HttpServletRequest request; @GetMapping(\"大众/\"大众) public @ResponseBody String index() { Cookie[] cookies = request.getCookies(); if (null != cookies) { for (Cookie cookie : cookies) { log.info(cookie.getName() + \"大众=\"大众 + cookie.getValue()); } } return \"大众index\公众; }}
掌握台输出
2019-05-16 18:03:32.770 INFO 10114 --- [nio-7003-exec-1] net.mengkang.demo.MainController : test2=ab+cd2019-05-16 18:03:32.770 INFO 10114 --- [nio-7003-exec-1] net.mengkang.demo.MainController : test1=ab%2Bcd
Php
GET / HTTP/1.1Host: localhost:8084...Cookie: test2=ab+cd; test1=ab%2Bcd
做事端
var_exprot($_COOKIE);array ( 'test2' => 'ab cd', 'test1' => 'ab+cd',)
结果比拟
创造Java是不会对cookie数据做任何处理,但是php则会默认进行一次urldecode操作,这导致了,两边系统里面获取同一cookie时,结果不一致的 bug。
类似的问题PHP 源码探秘 - 在解析外部变量时的一个 BUG
Php 源码剖析
紧张查看两处源码
main/php_variables.cext/standard/url.cSAPI_API SAPI_TREAT_DATA_FUNC(php_default_treat_data){... switch (arg) { case PARSE_GET: case PARSE_STRING: separator = PG(arg_separator).input; break; case PARSE_COOKIE: separator = \"大众;\0\"大众; //可以在我们浏览器里看到要求的header里面cookie的分隔符便是这个 break; } var = php_strtok_r(res, separator, &strtok_buf); while (var) { val = strchr(var, '='); if (arg == PARSE_COOKIE) { / Remove leading spaces from cookie names, needed for multi-cookie header where ; can be followed by a space / while (isspace(var)) { var++; } if (var == val || var == '\0') { goto next_cookie; } } if (++count > PG(max_input_vars)) { php_error_docref(NULL, E_WARNING, \公众Input variables exceeded \公众 ZEND_LONG_FMT \"大众. To increase the limit change max_input_vars in php.ini.\"大众, PG(max_input_vars)); break; } if (val) { / have a value / size_t val_len; size_t new_val_len; val++ = '\0'; php_url_decode(var, strlen(var)); val_len = php_url_decode(val, strlen(val)); val = estrndup(val, val_len); if (sapi_module.input_filter(arg, var, &val, val_len, &new_val_len)) { php_register_variable_safe(var, val, new_val_len, &array); } efree(val); } else { size_t val_len; size_t new_val_len; php_url_decode(var, strlen(var)); val_len = 0; val = estrndup(\公众\"大众, val_len); if (sapi_module.input_filter(arg, var, &val, val_len, &new_val_len)) { php_register_variable_safe(var, val, new_val_len, &array); } efree(val); }next_cookie: var = php_strtok_r(NULL, separator, &strtok_buf); } if (free_buffer) { efree(res); }}
我们看到cookie的值会被实行php_url_decode操作,下面附带其源码,且加上一段测试代码
#include <stdio.h>#include <ctype.h>#include <memory.h>static int php_htoi(char s) { int value; int c; c = ((unsigned char ) s)[0]; if (isupper(c)) c = tolower(c); value = (c >= '0' && c <= '9' ? c - '0' : c - 'a' + 10) 16; c = ((unsigned char ) s)[1]; if (isupper(c)) c = tolower(c); value += c >= '0' && c <= '9' ? c - '0' : c - 'a' + 10; return (value);}size_t php_url_decode(char str, size_t len) { char dest = str; char data = str; while (len--) { if (data == '+') { dest = ' '; } else if (data == '%' && len >= 2 && isxdigit((int) (data + 1)) && isxdigit((int) (data + 2))) { dest = (char) php_htoi(data + 1); data += 2; len -= 2; } else { dest = data; } data++; dest++; } dest = '\0'; return dest - str;}int main() { char a[6] = {\公众ab+cd\公众}; php_url_decode(a, strlen(a)); printf(\"大众%s\n\"大众, a); return 0;}
上面php_url_decode用到了php_htoi,这个是由于urlencode是按照rfc1738对字符串中除了 -_. 之外的所有非字母数字字符都将被更换成百分号(%)后跟两位十六进制数。htoi浸染便是Converting Hexadecimal Digits Into Integers。然后把打算出来的整型转换为char,存回处理完之后的字符数组里。
小结
$_COOKIE的数据在 php 这边是经由urldecode的二手数据,这个导致和JAVA那边获取的cookie值不一样了就。
编码扩展谈论
rawurlencode与urlencode的差异是什么?
手册上的阐明是:
urlencode 返回字符串,此字符串中除了 -_. 之外的所有非字母数字字符都将被更换成百分号(%)后跟两位十六进制数,空格则编码为加号(+)。此编码与 WWW 表单 POST 数据的编码办法是一样的,同时与 application/x-www-form-urlencoded 的媒体类型编码办法一样。由于历史缘故原由,此编码在将空格编码为加号(+)方面与 » RFC3986 编码(拜会 rawurlencode())不同。
PHPAPI size_t php_raw_url_decode(char str, size_t len){ char dest = str; char data = str; while (len--) { if (data == '%' && len >= 2 && isxdigit((int) (data + 1)) && isxdigit((int) (data + 2))) {#ifndef CHARSET_EBCDIC dest = (char) php_htoi(data + 1);#else dest = os_toebcdic[(char) php_htoi(data + 1)];#endif data += 2; len -= 2; } else { dest = data; } data++; dest++; } dest = '\0'; return dest - str;}
通过源码可以看到便是对+处理没有了。
要求的编码谈论
GET
当我们在 url 通报+的时候,浏览器不会默认为我们实行urlencode操作,但是 php 做事端取值的时候(还是上面那段代码)会实行urldecode,导致url中的+被去掉。这一点也非常好检测。
var_dump($_GET);curl http://localhost:8084/a.php\?a=\bbb+carray(1) { [\公众a\"大众]=> string(5) \"大众bbb c\公众}
POST
当我们的做表单提交post要求的时候,默认表单的编码规范便是application/x-www-form-urlencoded,这样浏览器会自动的对我们的数据就行一次urlencode编码,之后 php 做事端收到$_POST数据会再进行urldecode。
<form action=\公众a.php\公众 method=\"大众post\"大众 > <input type=\公众text\公众 name=\公众postData\"大众 value=\公众\"大众> <input type=\"大众submit\公众></form>
当我在表单里提交了一段ab+cd的内容,要求数据如下
POST /a.php HTTP/1.1...Host: localhost:8084Content-Type: application/x-www-form-urlencoded...Cookie: test2=ab+cd; test1=ab%2BcdpostData=ab%2Bcd
做事端
# a.phpvar_dump($_POST);var_dump(file_get_contents(\"大众php://input\公众));
输出结果
array(1) { [\"大众postData\公众]=> string(5) \"大众ab+cd\"大众}string(16) \"大众postData=ab%2Bcd\"大众
另一种情形,如果我们post的表单实行编码为multipart/form-data,浏览器则不会对数据进行编码,做事端也不会对数据就行解码。
以是当我们在配置 url 参数和 cookie 的时候,一定要把稳url编码的问题。