zoukankan      html  css  js  c++  java
  • 因为 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 = ";"; //可以在我们浏览器里看到请求的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 == '') {
                    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++ = '';
                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 = '';
        return dest - str;
    }
    
    int main() {
        char a[6] = {"ab+cd"};
        php_url_decode(a, strlen(a));
        printf("%s
    ", a);
    
        return 0;
    
    }

    上面php_url_decode用到了php_htoi,这个是因为urlencode是按照rfc1738对字符串中除了 -_. 之外的所有非字母数字字符都将被替换成百分号(%)后跟两位十六进制数。htoi作用就是Converting Hexadecimal Digits Into Integers。然后把计算出来的整型转换为char,存回处理完之后的字符数组里。

    小结

    $_COOKIE的数据在 php 这边是经过urldecode的二手数据,这个导致和JAVA那边获取的cookie值不一样了就。

    编码扩展讨论

    rawurlencodeurlencode的区别是什么?
    手册上的解释是:

    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 = '';
        return dest - str;
    }

    通过源码可以看到就是对+处理没有了。

    请求的编码讨论

    GET

    当我们在 url 传递+的时候,浏览器不会默认为我们执行urlencode操作,但是 php 服务端取值的时候(还是上面那段代码)会执行urldecode,导致url中的+被去掉。这一点也非常好检测。

    var_dump($_GET);
    curl http://localhost:8084/a.php?a=bb+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编码的问题。

    本文作者:周梦康

    原文链接 

    本文为云栖社区原创内容,未经允许不得转载。

  • 相关阅读:
    P24—动态数组没有{}
    JavaB站学习————接口在开发中的作用
    JavaB站学习————extends和implements同时出现
    JavaB站学习————一个类可以实现多个接口以及接口的总结
    JavaB站学习————接口和多态联合使用。
    01日语五十音
    07 递归&&命名风格&&作业(结构体,malloc,函数,递归)
    JavaB站学习——作业16
    电子书
    破解压缩包
  • 原文地址:https://www.cnblogs.com/zhaowei121/p/10937491.html
Copyright © 2011-2022 走看看