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\"}";
}
}
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)
。
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}`
})
// ...