Web-請求資料+號丟失問題

羊37發表於2024-05-27

1.背景

先來複習下URL請求的基本知識

HTTP的早期設計主要考慮了基本的文件檢索需求以及表單提交功能,這直接影響了後來對POST請求和內容型別的發展。

1.1 請求方法

HTTP(超文字傳輸協議)最初設計的目的是為了支援全球資訊網上的文件檢索,這涉及到請求HTML頁面、影像、影片等靜態資源。

  • GET

    設計用於請求資料,它將請求資訊編碼在URL中,適用於檢索操作。由於URL長度有限制,並且資料公開可見,這種方法適合非敏感資料的簡單檢索。

  • POST

    • 隨著Web的發展,需要一種方式能在保護使用者隱私的前提下傳送大量資料。POST應運而生,用於傳送資料到伺服器以建立或更新資源。
    • 最初,POST主要用來提交表單資料,這些資料不適合透過URL傳輸(例如,由於安全、大小或隱私考慮)。

1.2 請求內容型別

隨著Internet的應用日益廣泛,對錶單處理能力的需求增加,引入了幾種內容型別來支援更復雜的互動:

  • application/x-www-form-urlencoded

    • 這是最早用於支援HTML表單資料提交的內容型別。由於HTML表單是早期Web互動的核心,因此這種內容型別設計來處理簡單的文字資料。
    • 它簡單地將表單資料編碼為名/值對,與URL查詢字串使用相同的格式,不過是放在了請求體中。
  • multipart/form-data

    • 隨著檔案上傳需求的出現,application/x-www-form-urlencoded方式由於其編碼方式(特別是空間效率問題)並不適合處理檔案或大量資料。
    • 因此,multipart/form-data被引入,支援表單中的檔案上傳以及包含二進位制資料的需求。
  • application/json

    • 隨著Web服務和API的普及,尤其是RESTful架構的推廣,需要一種更靈活、能支援複雜資料結構(如物件和陣列)的資料交換格式。
    • JSON因其易於人類讀寫和機器解析生成的特性,成為資料互動的首選格式,尤其在Web應用與伺服器之間。

2.URL保留字元

在URL中,某些字元有特殊的意義,被稱為“保留字元”。這些字元通常用於分隔URL的不同部分,如路徑、查詢引數和片段識別符號。

字元 用途 URL編碼
: 分隔協議和地址、埠號 %3A
/ 分隔URL路徑的各個部分 %2F
? 標記URL的查詢部分的開始 %3F
# 標記URL的片段識別符號的開始 %23
[ 在URL中用於特定的語法結構 %5B
] 在URL中用於特定的語法結構 %5D
@ 在URL中指定使用者名稱和密碼 %40
! 子分隔符,有時用於注入特殊意義 %21
$ 子分隔符,用於處理特定資料 %24
& 分隔URL中的查詢引數 %26
' 子分隔符,用於封裝資料 %27
( 子分隔符,用於更復雜的資料結構 %28
) 子分隔符,用於更復雜的資料結構 %29
* 子分隔符,有時用於URL中的萬用字元 %2A
+ 子分隔符,用於空格的替代符,尤其是在表單資料中 %2B
, 子分隔符,用於分隔資料 %2C
; 分隔URL引數 %3B
= 分隔URL中的引數名稱和值 %3D

好,現在問題就來了,在不進行URL編碼的時候,+號預設譯為空格。

3.問題場景

根據上面的分析,我們可以看到,常見的問題點在2個場景。

3.1 URL路徑

跟方法get/post無關,關鍵是路徑中直接使用+號,會被預設解釋為空格。

@Slf4j
@RestController
@RequestMapping("/index")
public class IndexController {

    @RequestMapping("/a")
    public String index1(@RequestParam String data) {
        log.info("data: {}", data);
        return "{\"ret\":\"ok\"}";
    }
}

image-20240527123257744

image-20240527123312969

3.2 內容型別

application/x-www-form-urlencoded使用的也是簡單的字串拼接方式,問題主要發生在這裡

其餘兩個型別內容轉換的方法不一樣,不會有這個問題。

<script setup></script>

<template>
  <div id="app">
    <form @submit.prevent="handleSubmit">
      <input v-model="inputData" type="text" placeholder="++++++++++++++++" />
      <button type="submit">Submit</button>
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      inputData: ''
    }
  },
  methods: {
    handleSubmit() {
      console.log('Before encoding: ', this.inputData)
      const sourceData = this.inputData
      const encodedData = encodeURIComponent(this.inputData)

      // 源字串
      console.log('Source data: ', sourceData)
      // URL編碼
      console.log('URL Encoded data: ', encodedData)

      // 直接傳送的是源字串
      this.sendData(sourceData)
    },

    sendData(sendData) {
      fetch('http://127.0.0.1:8090/index/b', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: `data=${sendData}`
      })
        .then((response) => response.json())
        .then((data) => console.log('Server response: ', data))
        .catch((error) => console.error('Error:', error))
    }
  }
}
</script>

package cn.yang37.za.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

/**
 * @description:
 * @class: IndexController
 * @author: yang37z@qq.com
 * @date: 2024/5/27 11:40
 * @version: 1.0
 */
@Slf4j
@RestController
@RequestMapping("/index")
public class IndexController {
    @RequestMapping("/b")
    @CrossOrigin(origins = "http://localhost:5173")
    public String index2(@RequestParam String data) {
        log.info("data: {}", data);
        return "{\"ret\":\"ok\"}";
    }
}

這裡,我們直接使用未編碼的sourceData進行傳送,即this.sendData(sourceData)

image-20240527123529125

image-20240527123607585

4.解決方案

上面可以看到,問題的根源都在於沒有進行URL編碼。

4.1 URL路徑

// 源引數
const userInput = "這 是 一 堆 特 殊 字 符 / = &";

// url編碼
const encodedInput = encodeURIComponent(userInput);

// 路徑裡使用編碼後的
const url = `https://example.com/data/${encodedInput}`;

同理啊,有沒有想過,平時看到的是?key=value&key2=value2的格式。

那假設傳送的key或者value有特殊符號呢,例如key是name?n&666,value是value&123

https://example.com/api?name?n&666=value&123

你看,你自己都分不清,換成這樣你才理解。

https://example.com/api?name%3Fn%26666=value%26123
const baseUrl = "https://example.com/api";
const key = "name?n&666";
const value = "value&123";

const encodedKey = encodeURIComponent(key);
const encodedValue = encodeURIComponent(value);

const url = `${baseUrl}?${encodedKey}=${encodedValue}`;
console.log(url);

當然一般不會這樣,但又不是不允許。

所以,碰到特殊符號,記得想起來先編碼。

4.2 內容型別

思路一致,application/x-www-form-urlencoded把要傳送的內容,進行URL編碼。

  methods: {
    handleSubmit() {
      console.log('Before encoding: ', this.inputData)
      const sourceData = this.inputData
      const encodedData = encodeURIComponent(this.inputData)

      // URL編碼
      console.log('URL Encoded data: ', encodedData)
      // 用編碼後的資料
      this.sendData(encodedData)
    },

    sendData(sendData) {
      fetch('http://127.0.0.1:8090/index/b', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: `data=${sendData}`
      })
     
    // ...

相關文章