校驗輸入驗證碼接口 check( )
5. 為什么要用靜態內部類接收配置文件中的 Seisson 對象?
為什么我們接收配置文件的 Session 對象時,使用靜態內部類給 Session 對象的 key,date 屬性賦值呢?不加 static 可以嗎?
在
CaptchaProperties
類中,Session
被定義為 靜態內部類(static nested class),而不是普通的內部類(non-static inner class)。這兩種方式有一些關鍵區別,會影響配置綁定的行為。
1. 為什么使用
static
內部類?
? 原因 1:Spring Boot 配置綁定要求
- Spring Boot 的
@ConfigurationProperties
綁定機制要求嵌套類必須是static
,否則無法正確注入屬性。- 如果
Session
不是static
,Spring 在解析 YAML/Properties 時無法實例化它,導致key
和date
始終為null
。
? 原因 2:避免隱含的外部類依賴
- 非靜態內部類(non-static inner class)會
隱式持有外部類的引用
(CaptchaProperties.this
),這在配置綁定時是不必要的,甚至可能導致問題。- 靜態內部類是獨立的,不依賴外部類實例,更適合作為純數據載體(DTO)。
2. 不加
static
可以嗎?
? 不可以(在
@ConfigurationProperties
場景下)
如果去掉
static
,Spring 將無法正確綁定嵌套屬性,你會得到類似這樣的錯誤:captcha:session: # 無法注入,因為 Session 不是靜態的key: CAPTCHA_KEYdate: CAPTCHA_DATE
即使沒有報錯,
session.key
和session.date
也會是null
,因為Spring 無法實例化非靜態內部類
(它需要先實例化外部類)。
3. 驗證實驗
你可以嘗試去掉
static
,然后啟動應用:@Data public class Session { // 去掉 staticprivate String key;private String date; }
結果:
- Spring Boot 啟動時會報錯,或
session
的屬性無法注入。- 使用
@Autowired CaptchaProperties
時,session
可能是null
。
4. 最佳實踐
? 推薦方式
保持
static
,這是 Spring Boot 配置綁定的標準做法:@ConfigurationProperties(prefix = "captcha") @Data public class CaptchaProperties {private Integer width;private Integer height;private Session session; // 依賴靜態內部類@Datapublic static class Session { // 必須是 staticprivate String key;private String date;} }
? 替代方案(不推薦)
如果因為某些原因你不想用靜態內部類,可以用 獨立的類:
// 單獨定義 Session 類 @Data public class SessionConfig {private String key;private String date; }// 在 CaptchaProperties 中引用 @ConfigurationProperties(prefix = "captcha") @Data public class CaptchaProperties {private Integer width;private Integer height;private SessionConfig session; // 使用獨立類 }
但這樣會讓代碼更分散,不如靜態內部類簡潔。
5. 總結
方案 是否推薦 原因 靜態內部類 ( static class
)? 推薦 Spring Boot 配置綁定的標準方式,簡潔且無副作用。 非靜態內部類 (去掉 static
)? 不推薦 Spring 無法綁定嵌套屬性, session
會為null
。獨立類 ?? 可用 可行,但會讓配置類分散,不如靜態內部類直觀。 因此,在你的場景中必須加
static
,否則配置無法正確綁定!
6. 重新調整向 Seisson 中設置驗證碼和生成時間的代碼
1. 問題:用戶A和用戶B同時獲取驗證碼,會互相影響嗎?
情景步驟圖:用戶A和用戶B同時獲取驗證碼,是否互相影響?
用戶A和用戶B并發獲取驗證碼流程(含Session/Cookie機制)
流程關鍵點解析
-
Session創建時機:
- 當瀏覽器首次訪問且無
JSESSIONID
Cookie時,服務器會立即創建新Session - 創建時會生成唯一SessionID(示例中123和456)
- 當瀏覽器首次訪問且無
-
Set-Cookie機制:
- 只在首次響應時通過
Set-Cookie
頭下發JSESSIONID - 瀏覽器后續請求會自動攜帶該Cookie
- 只在首次響應時通過
-
數據隔離原理:
用戶 SessionID 存儲的驗證碼 使用的Cookie 用戶A 123 ABCD JSESSIONID=123 用戶B 456 WXYZ JSESSIONID=456 -
驗證過程:
- 服務器始終根據請求中的
JSESSIONID
值查找對應Session - 不同用戶的Session存儲空間完全獨立
- 服務器始終根據請求中的
為什么不會互相影響?
- Cookie隔離:瀏覽器之間不會共享Cookie
- 服務端Session隔離:SessionID不同導致數據存儲位置不同
- 自動關聯機制:Spring自動通過Cookie中的JSESSIONID關聯對應Session
即使key名稱相同(如都叫"CAPTCHA_CODE"),但因存儲在不同的Session對象中,實際上相當于
session123.get("CAPTCHA_CODE")
和session456.get("CAPTCHA_CODE")
的區別。
情景復現(修正版):
-
用戶A 訪問
/getCaptcha
:- 服務器創建 SessionA,存驗證碼
CodeA
,并返回Set-Cookie: JSESSIONID=SessionA
。 - 用戶A的瀏覽器保存這個 Cookie,之后的請求都會帶上
JSESSIONID=SessionA
。
- 服務器創建 SessionA,存驗證碼
-
用戶B 訪問
/getCaptcha
:- 服務器創建 SessionB,存驗證碼
CodeB
,并返回Set-Cookie: JSESSIONID=SessionB
。 - 用戶B的瀏覽器保存這個 Cookie,之后的請求都會帶上
JSESSIONID=SessionB
。
- 服務器創建 SessionB,存驗證碼
-
用戶A 提交驗證碼(訪問
/check
):- 瀏覽器自動帶上
JSESSIONID=SessionA
。 - 服務器從 SessionA 里取驗證碼(
CodeA
),和用戶A輸入的驗證碼比較。 - 不會讀到 SessionB 的內容!
- 瀏覽器自動帶上
-
用戶B 提交驗證碼(訪問
/check
):- 瀏覽器自動帶上
JSESSIONID=SessionB
。 - 服務器從 SessionB 里取驗證碼(
CodeB
),和用戶B輸入的驗證碼比較。 - 不會讀到 SessionA 的內容!
- 瀏覽器自動帶上
2. 核心概念:Session 如何區分不同用戶?
- Session 的本質:服務器為每個用戶創建的一個獨立存儲空間(類似一個私人保險箱)。
- 如何區分不同用戶?:靠 Cookie(JSESSIONID),瀏覽器每次請求會自動帶上這個 Cookie,告訴服務器“我是誰”。
關鍵流程:
-
用戶第一次訪問網站(比如
/getCaptcha
):- 服務器發現請求沒有
JSESSIONID
Cookie,就創建一個新 Session,并生成一個唯一 ID(如Session123
)。 - 服務器返回響應時,會通過 Set-Cookie 頭告訴瀏覽器:
JSESSIONID=Session123
。 - 瀏覽器之后每次請求都會自動帶上這個 Cookie。
- 服務器發現請求沒有
-
用戶第二次請求(比如
/check
):- 瀏覽器會自動在請求頭里帶上:
Cookie: JSESSIONID=Session123
。 - 服務器通過這個
JSESSIONID
找到對應的 Session(Session123
),然后從里面讀取之前存的驗證碼。
- 瀏覽器會自動在請求頭里帶上:
3. 為什么不會混亂?
- Session 的 key(CAPHCHA_SESSION_KEY)雖然一樣,但每個用戶的 Session 是獨立的。
- 類似:你和朋友都有一個叫“密碼”的抽屜,但你們的抽屜在不同的保險箱里(SessionA vs SessionB),互不影響。
- 瀏覽器通過
JSESSIONID
告訴服務器該用哪個 Session,所以服務器永遠不會搞混。
4. 你的代碼是安全的
check(String captcha, HttpSession session)
里的session
參數,是由 Spring 自動從當前請求的JSESSIONID
關聯的 Session 里獲取的,所以:- 用戶A的請求 → 拿到
SessionA
→ 取CodeA
- 用戶B的請求 → 拿到
SessionB
→ 取CodeB
- 用戶A的請求 → 拿到
- 即使 key 是固定的,Session 隔離保證了數據不會串。
5. 如果真想讓驗證碼混亂,得怎么做?
要讓用戶A和用戶B的驗證碼互相影響,必須:
- 讓用戶A和用戶B的請求使用同一個
JSESSIONID
(比如黑客篡改 Cookie)。 - 或者把驗證碼存在全局變量里(比如
static Map
),而不是 Session 里。
但你的代碼沒問題,因為 Session 天然隔離用戶。
總結
- Session 靠
JSESSIONID
Cookie 區分用戶,不同用戶的 Session 互不干擾。 - 你的代碼是線程安全的,因為每個用戶的驗證碼存在自己的 Session 里。
- 用戶A的驗證碼不會因為用戶B刷新而失效,因為服務器讀取的是用戶A自己的 Session。
希望這個解釋更清晰!如果還有疑問,可以繼續討論。 😊
12. check( ) 接口的傳參細節
參數 Session 是從哪里傳參的呢
?
只要我們打開 http://127.0.0.1:8080/captcha/check?captcha=u8hs 這個頁面,就會調用后端 check 接口,此時我們就需要通過 URL 手動對 check() 的參數 captcha 傳參
,那還有一個參數 Session 是從哪里傳參的呢
?
在訪問 http://127.0.0.1:8080/captcha/check?captcha=u8hs
時,Session
參數是 由 Spring 自動注入的
,不需要手動傳參。以下是詳細說明:
Session 參數的來源
-
自動注入機制:
Spring MVC 在調用控制器方法時,如果發現參數類型是HttpSession
,會自動從當前請求中獲取或創建 Session
,無需手動傳遞。 -
public boolean check(String captcha, HttpSession session) { ... }
captcha
:通過 URL 參數?captcha=u8hs
傳遞。session
:由 Spring 從請求的Cookie
中讀取JSESSIONID
關聯的 Session(若無則新建)。
-
Session 的關聯性:
確保兩次請求(/getCaptcha
和/check
)的JSESSIONID
相同,否則會找不到之前存儲的驗證碼。可通過以下方式檢查:@RequestMapping("/check") public boolean check(String captcha, HttpSession session) {System.out.println("當前 Session ID: " + session.getId()); // 打印 Session ID// ...其他邏輯 }
調整前端頁面代碼
2. 解決前端多次刷新頁面圖片生成不同步問題
出現上述問題的原因是,我們的瀏覽器存在一些緩存?
🔍 問題現象分析
首次加載頁面:
- 瀏覽器請求
/captcha/getCaptcha
,返回驗證碼圖片(狀態碼 200)。- 圖片被瀏覽器緩存(因為 GET 請求默認可緩存)。
第一次刷新頁面:
- 瀏覽器發現緩存中的圖片未過期(根據響應頭
Cache-Control
或Expires
),直接使用緩存(狀態碼 304 Not Modified)。- 驗證碼圖片未變化(因為未真正請求后端)。
第二次刷新頁面:
- 可能因緩存策略(如
max-age=0
)或手動強制刷新(Ctrl+F5),瀏覽器重新請求服務器(狀態碼 200)。- 此時后端生成新的驗證碼,圖片變化。
📌 你的理解修正
你的說法 修正/補充說明 “GET 請求是冪等的” ? 正確,GET 是冪等的(多次請求不影響資源狀態)。 “瀏覽器會對未變化的資源緩存” ? 正確,但需要明確是 根據響應頭決定是否緩存(如 Cache-Control
)。“第二次刷新不發送 GET 請求” ? 不完全正確:瀏覽器會發送請求,但可能返回 304(協商緩存),而非完全不發送。
🛠? 解決方案
1. 禁用瀏覽器緩存(推薦)
在
getCaptcha
方法中添加響應頭,禁止緩存:@RequestMapping("/getCaptcha") public void getCaptcha(HttpServletResponse response, HttpSession session) throws IOException {// 禁用緩存response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");response.setHeader("Pragma", "no-cache");response.setDateHeader("Expires", 0);// 生成驗證碼邏輯...ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(...);// ... }
2. 強制每次請求更新(前端修改)
在圖片 URL 后添加時間戳參數(避免緩存):
<img id="verificationCodeImg" src="/captcha/getCaptcha?dt=<%=new Date().getTime()%>" onclick="this.src='/captcha/getCaptcha?dt=' + new Date().getTime()" />
3. 修復前端代碼問題
你的前端代碼中,點擊事件寫錯了路徑(
/admin/captcha
應改為/captcha/getCaptcha
):$("#verificationCodeImg").click(function(){$(this).hide().attr('src', '/captcha/getCaptcha?dt=' + new Date().getTime()).fadeIn(); });
💡 關鍵點總結
- GET 請求默認緩存:瀏覽器會緩存 GET 請求的響應(除非顯式禁用)。
- 304 狀態碼:表示資源未修改,瀏覽器使用本地緩存。
- 解決方案:
- 后端:通過響應頭禁用緩存。
- 前端:添加隨機參數(如時間戳)繞過緩存。
? 最終效果
- 每次訪問
/captcha/getCaptcha
都會生成新驗證碼。- 圖片不會因緩存而重復顯示舊驗證碼。
對于上述禁用緩存策略,我們采取修改后端的方法: