因为 Java 和 Php 在获取客户端 cookie 方式不同引发的 bug
遇到个 Java 和 Php 在获取客户端 cookie 方式不同导致跨系统的问题。所以写了这篇博客梳理下相关知识。
实验
下面通过两个简单的实验,来看Java和Php在获取web请求中的cookie的不同之处,我下面贴出http请求的相关信息,和服务端输出的结果。
Java
请求信息
GET / HTTP/1.1
Host: localhost:7003
...
Cookie: test2=ab+cd; test1=ab%2Bcd
服务端
@Controller
@Slf4j
public 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+cd
2019-05-16 18:03:32.770 INFO 10114 --- [nio-7003-exec-1] net.mengkang.demo.MainController : test1=ab%2Bcd
Php
GET / HTTP/1.1
Host: 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.c
ext/standard/url.c
SAPI_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+c
array(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:8084
Content-Type: application/x-www-form-urlencoded
...
Cookie: test2=ab+cd; test1=ab%2Bcd
postData=ab%2Bcd
服务端
# a.php
var_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编码的问题。
本文作者:周梦康
本文为云栖社区原创内容,未经允许不得转载。
因为 Java 和 Php 在获取客户端 cookie 方式不同引发的 bug的更多相关文章
- java中几种获取项目路径方式
转自http://caodaoxi.iteye.com/blog/1234805 在jsp和class文件中调用的相对路径不同. 在jsp里,根目录是WebRoot 在class文件中,根目录 ...
- JAVA记录-JSP页面获取服务器路径方式
1.basePath方式 <% String path = request.getContextPath(); String basePath = request.getScheme()+&qu ...
- java web 获取客户端操作系统信息
package com.java.basic.pattern; import java.util.regex.Matcher; import java.util.regex.Pattern; /** ...
- js获取客户端time,cookie,url,ip,refer,user_agent信息:
<script src="http://pv.sohu.com/cityjson?ie=utf-8"></script> <script type=& ...
- Java 字符终端上获取输入三种方式
http://blog.csdn.net/hongweigg/article/details/14448731 在Java 字符终端上获取输入有三种方式: 1.java.lang.System.in ...
- JAVA获取客户端IP地址
在JSP里,获取客户端的IP地址的方法是:request.getRemoteAddr(),这种方法在大部分情况下都是有效的.但是在通过了Apache,Squid等反向代理软件就不能获取到客户端的真实I ...
- JAVA获取客户端IP地址和MAC地址
1.获取客户端IP地址 public String getIp(HttpServletRequest request) throws Exception { String ip = request.g ...
- Nginx反向代理后,java获取客户端真实IP地址
一般情况下,java获取客户端IP地址的方法为request.getRemoteAddr();但这只是在没有网关或者代理的情况下,如果客户端将请求发送到nginx,再由nginx进行反向代理到目标服务 ...
- Java Web 获取客户端真实IP
Java Web 获取客户端真实IP 发生的场景:服务器端接收客户端请求的时候,一般需要进行签名验证,客户端IP限定等情况,在进行客户端IP限定的时候,需要首先获取该真实的IP.一般分为两种情况: 方 ...
随机推荐
- 洛谷P1311 [NOIP2011提高组Day1T2]选择客栈
P1311 选择客栈 题目描述 丽江河边有n 家很有特色的客栈,客栈按照其位置顺序从 1 到n 编号.每家客栈都按照某一种色调进行装饰(总共 k 种,用整数 0 ~ k-1 表示),且每家客栈都设有一 ...
- django中模型
一.django需要使用数据库,则需要安装对应的驱动,比如mysql,则需要安装mysqlclient驱动: pip install mysqlclient 二.在settings.py文件中配置数据 ...
- ML面试1000题系列(1-20)
本文总结ML面试常见的问题集 转载来源:https://blog.csdn.net/v_july_v/article/details/78121924 1.简要介绍SVM 全称是support vec ...
- Header解析
不管是作为后端还是前端开发人员,对于web请求的过程和参数都是需要了解的. 下面是对一次简单的http请求的header分析,作为自己的一个总结,也希望对大家有所帮助. 以Chrome为例: 我们对h ...
- 完整版unity安卓发布流程(包括SDK有原生系统依赖关系的工程)
要3个东西!NDS,SDK,JDK, NDK官网下载:https://developer.android.google.cn/ndk/downloads/index.html(注意系统是不是64位) ...
- 基于mcp940反编译Minecraft源代码
引言 Minecraft中文叫"我的世界",没怎么深入玩过,来试试把它源代码反编译出来吧. 参考教程: https://minecraft.gamepedia.com/Mods/C ...
- POJ 2078
16ms 解法: #include <cstdio> //using namespace std; ][]; ][]; ]; ]; int n,min,max; void solve(in ...
- JVM学习篇章(一)
熟悉Java开发的同学,应该都知道jvm是什么,---它就是Java虚拟机,今天我们就学习一下: 内存分配原则: 常用的监控命令
- 2019-11-12-WPF-添加窗口消息钩子方法
title author date CreateTime categories WPF 添加窗口消息钩子方法 lindexi 2019-11-12 18:46:53 +0800 2019-06-05 ...
- phpcms多站点表单统一到主站点管理的解决方案
1.在主站点新建子站点的表单向导,与子站点的设置保持一致 2.在各个子站点的数据库的表单数据表添加一个写入触发器,将新增的表单数据同步到主站点的数据库对应表里,这样主站点就能展示所有站点的表单数据 3 ...