RFC 6265 規定 Cookie 值僅允許可見 ASCII 字符 (排除控制字符、空格、分號、逗號等)。中文屬於非 ASCII 字符,直接寫入會導致:
IllegalArgumentException: Control character in cookie valueimport javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
public void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
try {
// 關鍵:UTF-8 編碼,空格轉為 %20 (非 + 號)
String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.name())
.replace("+", "%20"); // 標準 URL 編碼空格為 %20
Cookie cookie = new Cookie(name, encodedValue);
cookie.setMaxAge(maxAge);
cookie.setPath("/"); // 全路徑可見
cookie.setHttpOnly(true); // 防 XSS
cookie.setSecure(true); // 僅 HTTPS 傳輸 (生產環境必開)
response.addCookie(cookie);
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("UTF-8 not supported", e); // 不會發生
}
}
// 使用示例
addCookie(response, "userName", "張三", 3600); // 1小時過期
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.Optional;
public Optional<String> getCookieValue(HttpServletRequest request, String name) {
if (request.getCookies() == null) return Optional.empty();
return Arrays.stream(request.getCookies())
.filter(c -> name.equals(c.getName()))
.findFirst()
.map(cookie -> {
try {
return URLDecoder.decode(cookie.getValue(), StandardCharsets.UTF_8.name());
} catch (Exception e) {
log.warn("Cookie decode failed: {}", cookie.getValue(), e);
return cookie.getValue(); // 降級返回原值
}
});
}
// 使用示例
String userName = getCookieValue(request, "userName").orElse("游客"); // 正確顯示「張三」
URL 編碼後包含大量 %,某些舊版防火牆/WAF 可能誤攔截。Base64 編碼字符集安全 (A-Z a-z 0-9 + / =) 且無特殊字符:
import java.util.Base64;
public void addCookieBase64(HttpServletResponse response, String name, String value, int maxAge) {
String encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(value.getBytes(StandardCharsets.UTF_8));
Cookie cookie = new Cookie(name, encoded);
cookie.setMaxAge(maxAge);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setSecure(true);
response.addCookie(cookie);
}
public Optional<String> getCookieValueBase64(HttpServletRequest request, String name) {
return getCookieValue(request, name).map(v -> {
try {
return new String(Base64.getUrlDecoder().decode(v), StandardCharsets.UTF_8);
} catch (IllegalArgumentException e) {
log.warn("Base64 decode failed, fallback to URLDecoder", e);
return URLDecoder.decode(v, StandardCharsets.UTF_8.name()); // 兼容舊 Cookie
}
});
}
@Component
public class CookieUtil {
private static final String UTF_8 = StandardCharsets.UTF_8.name();
private static final Base64.Encoder ENCODER = Base64.getUrlEncoder().withoutPadding();
private static final Base64.Decoder DECODER = Base64.getUrlDecoder();
public void set(HttpServletResponse response, String name, String value, int maxAgeSeconds) {
String encoded = ENCODER.encodeToString(value.getBytes(UTF_8));
ResponseCookie cookie = ResponseCookie.from(name, encoded)
.maxAge(maxAgeSeconds).path("/").httpOnly(true).secure(true).sameSite("Lax").build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
}
public Optional<String> get(HttpServletRequest request, String name) {
Cookie cookie = WebUtils.getCookie(request, name);
if (cookie == null) return Optional.empty();
try {
return Optional.of(new String(DECODER.decode(cookie.getValue()), UTF_8));
} catch (IllegalArgumentException e) {
// 兼容舊版 URLEncoder Cookie
return Optional.of(URLDecoder.decode(cookie.getValue(), UTF_8));
}
}
public void remove(HttpServletResponse response, String name) {
ResponseCookie cookie = ResponseCookie.from(name, "")
.maxAge(0).path("/").httpOnly(true).secure(true).build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
}
}
| 錯誤做法 | 後果 | 正確做法 |
|---|---|---|
直接存中文 new Cookie("k", "中文") | 容器拋異常或瀏覽器亂碼 | 必須編碼 |
用 URLEncoder.encode 但不替換 + | 空格變 +,解碼後成 + 而非空格 | .replace("+", "%20") 或用 Base64 |
| 編碼用 UTF-8,解碼用 ISO-8859-1 | 亂碼 () | 編解碼字符集統一 |
| Cookie 過大 (>4KB) | 瀏覽器拒絕/截斷 | 大數據存 Session/Redis,Cookie 只存 ID |
| 敏感信息 (Token、ID) 明文存 Cookie | XSS 竊取、CSRF 攻擊 | HttpOnly + Secure + SameSite=Lax/Strict |
Cookie 中文亂碼本質是字符集不匹配與規範限制。標準解法:寫入時 UTF-8 + URLEncoder (替換 + 號) 或 Base64,讀取時對應解碼。生產環境建議封裝 CookieUtil 統一管理,強制 HttpOnly、Secure、SameSite,兼容新舊編碼格式,徹底消除亂碼與安全隱患。