由於 Java 和 Php 在獲取客戶端 cookie 方式不一樣引起的 bug

遇到個 Java 和 Php 在獲取客戶端 cookie 方式不一樣致使跨系統的問題。因此寫了這篇博客梳理下相關知識。php

實驗

下面經過兩個簡單的實驗,來看Java和Php在獲取web請求中的cookie的不一樣之處,我下面貼出http請求的相關信息,和服務端輸出的結果。html

Java

請求信息git

GET / HTTP/1.1
Host: localhost:7003
...
Cookie: test2=ab+cd; test1=ab%2Bcd

服務端web

@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";
    }
}

控制檯輸出api

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 在解析外部變量時的一個 BUGcookie

Php 源碼分析

主要查看兩處源碼app

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操做,下面附帶其源碼,且加上一段測試代碼curl

#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值不同了就。

編碼擴展討論

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 = '\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編碼的問題。

 

本文做者:周夢康

原文連接 

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索