昨天我們完成了登入功能 JWT 的實作,此時服務應該可以順利啟動。
啟動後我們可以按照第九天的做法,對登入功能進行簡單的測試,看看回應是否都如我們預期。
本次一樣列出了正向與負向的測試,負向測試大致上可分成驗證失敗(使用者不存在、密碼錯誤),與驗證前就被檢核擋下來(信箱格式不正確、密碼為空)的情境。
在API Tester的頁面中,到側邊欄此專案下的服務 (auth-service) 下方新增一個 Request,設定內容如下:
POST
http://localhost:8081/users/login
(note:我本來使用 CRUD 來分類 Request,後來覺得還是改以服務名稱進行分類比較合適,已更名為 auth-service,所以跟先前的章節會有些出入。)
這個案例想驗證已透過帳號密碼註冊的使用者是否可成功登入且可取得 JWT,並使用我們在第九天創建的帳號密碼進行測試。
Request Body :
{
"email": "test@example.com",
"password": "password123"
}
預期結果:
HTTP Status Code:200 SUCCESSFUL
Response Body:
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJzdWIiOiIxIiwicm9sZXMiOlsiUk9MRV9VU0VSIl0sImlzcyI6ImF1dGgtc2VydmljZSIsImlhdCI6MTc1OTExNDY3NCwiZXhwIjoxNzU5MTE2NDc0fQ.h4RkjDAMf_FOhGdu2kJzY8FAn1h5LdVt4BvEaFjVxX7FnGCiom2OWMYQBuGF8V0C8IX20kw4TTnbDxIVWwsU2rL0bngcgZRDEZiJUK2cKciA3EOW1dqYdIip8kUw13h8HTBDanB-nNJtTAqI2lGM2XlcpTj7GAn1tbzcX09OvEmivfxfo_5rMCreHhYfSQNcx-iGvdq6GhPfHAI6vsENB_4iuso8WIgA_3uakK8kjsNBwXYfRVr7FiJFtrEsL5V4VSwenMe45v_8PMkj4hp31pe9X9yb3CZX5QckkwRmFaeuZ-SssZx6V3qCONN3HaA6H4e0uAwvVZdzuTwr4LAUkQ",
"id": 1,
"email": "test@example.com",
"roles":[
"ROLE_USER"
]
}
實際結果:
實際結果符合預期,回傳了我們在 LoginResponse
定義的內容。
我們可以試著將 token 拿到 jwt.io 看看解析出來的 header 與 payload 是否包含正確資訊。結果如下圖,我們將 token 貼上左邊的欄位後,右邊解析內容對應我們昨天在 JwtUtils 設定了 JWT 應該包含哪些 header 與 claim,看起來都正確,初步確認了產製的 JWT 沒有問題。
這個案例想驗證當使用者輸入錯誤的密碼時,登入失敗並回傳 401 Unauthorized。
Request Body :
{
"email": "test@example.com",
"password": "wrong-password"
}
預期結果:
實際結果:
實際結果與預期不符,收到 403 Forbidden,原因於下節分析。
這個案例想驗證當使用者輸入錯誤的密碼時,登入失敗並回傳 401 Unauthorized。
Request Body :
{
"email": "userNotExist@example.com",
"password": "password123"
}
預期結果:
實際結果:
實際結果與預期不符,收到 403 Forbidden,原因於下節分析。
這個案例想驗證當使用者輸入錯誤的Email格式時,應該先進行欄位格式檢核,到不了 Controller 就會回應錯誤,所以期待出現的應該是檢核格式錯誤,而非驗證錯誤。
Request Body :
{
"email": "not-a-valid-email",
"password": "password123"
}
預期結果:
HTTP Status Code:400 Bad Request
Response Body:根據我們在 GlobalException
定義 MethodArgumentNotValidException
的錯誤訊息,預期內容會是:”欄位:錯誤訊息”。
(我有修改過昨天 Dto 檔案中的 message,所以出現的是我的自訂錯誤訊息,所以有點小落差是正常的)
{
email:"Invalid email format"
}
實際結果:
符合預期。
這個案例想驗證當使用者沒有輸入密碼時,應該先進行欄位格式檢核,到不了 Controller 就會回應錯誤,所以期待出現的應該是檢核格式錯誤,而非驗證錯誤。
Request Body :
{
"email": "test@example.com",
"password": ""
}
預期結果:
HTTP Status Code:400 Bad Request
Response Body:根據我們在 GlobalException
定義 IllegalStateException
的錯誤訊息,預期內容會是:”欄位:錯誤訊息”。
{
password:"Password can not be blank"
}
實際結果:
符合預期。
可以看到五個案例中,有兩個不符合我們的預期,也就是案例2與案例3,這兩個案例都是在測試 身分驗證失敗 後的回應是否如我們預期。在這兩個案例中我們期望收到 401 Unauthorized ,讓使用者知道未通過驗證,但不需要知道太多細節。
但我們意外收到了 403 Forbidden 的回應,顯然與之前註冊功能遇到的問題類似,原因應該出在Spring Security的設定。
接下來會依序說明錯誤背後發生了什麼事導致我們收到預期外的回應:
DaoAuthenticationProvider
比對密碼後發現與資料庫資料不匹配,內部拋出了一個 BadCredentialsException
(由於 Exception 拋出後由 spring 框架處理,這裡目前看不到這個 Exception 出現):....DaoAuthenticationProvider : Failed to authenticate since password does not match stored value
AnonymousAuthenticationFilter
被觸發,賦予當前請求 匿名用戶 (Anonymous User) 的身份:...AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
/users/login
),因為我們在 securityFilterChain
中設定所有請求都須經驗證,因此被阻擋,回傳 403 Forbidden 錯誤。...HttpSessionRequestCache : Saved request http://localhost:8081/users/login?continue to session
...Http403ForbiddenEntryPoint : Pre-authenticated entry point called. Rejecting access
...anyRequest().authenticated() // securityFilterChain 中這個配置導致最後訪問資源權限不足的結果
...
使用者不存在的錯誤日誌幾乎和密碼錯誤相同:驗證失敗 -> 設為匿名用戶 -> 未經驗證因此存取被拒 -> 轉發至/error後依然因為未經驗證存取失敗 -> 回傳 403 Forbidden。
我們可以透過設定 AuthEntryPoint
來新增未經認證的請求的入口點,指定其於驗證失敗時,應該返還怎麼樣的錯誤資訊。
明天,我們就來驗證上述對於錯誤的推論是否正確,並介紹 AuthEntryPoint
如何解決我們的問題,讓我們的功能如我們預期的運作!