課程筆記:注冊郵箱驗證
一、概述
從本小節開始,將學習如何進行注冊郵箱驗證。主要任務是給項目配置一個公共郵箱(可自己注冊或由公司提供),用于向用戶發送驗證碼,幫助用戶完成注冊流程。課程中以QQ郵箱為例,介紹在Spring Boot中發送郵件(包括正文、標題等)的方法。用戶收到驗證碼后填寫回來,與系統發送的進行比對,一致則驗證成功,反之則失敗。
二、注冊郵箱驗證的兩種主流方式
-
驗證碼驗證(國內常用,課程采用方式)
- 流程:用戶填寫郵箱后點擊發送驗證碼按鈕,系統向該郵箱發送隨機生成的驗證碼。用戶收到后填寫回來,系統進行比對校驗。
- 原理:驗證碼生成、校驗及重復校驗。若后續需進行短信驗證,只需將發郵件改為發短信,其他校驗原理一致。
-
唯一鏈接驗證(國外常用)
- 原理:給用戶郵箱發送一個帶有長鏈接的郵件,鏈接訪問某接口并帶參數(含個人信息)。每個郵箱的鏈接唯一,能訪問該鏈接說明用戶是郵箱主人。
- 流程:用戶點擊唯一鏈接后,系統驗證通過,確認是真實用戶。
三、課程采用的驗證碼驗證流程
- 用戶在網頁點擊發送驗證碼按鈕,系統檢查該郵箱是否已注冊。
- 未注冊:將用戶信息放入緩存,向郵箱發送驗證碼。
- 已注冊:拒絕注冊(不允許兩個用戶使用同一郵箱)。
- 用戶在郵箱中收到驗證碼,復制到網站繼續注冊,提交驗證碼。
- 系統在緩存中校驗驗證碼是否正確、是否過期。
- 不正確或過期:拒絕注冊。
- 正確:用戶注冊成功。
課程筆記:郵件發送功能的開發與完善
一、郵箱配置(以 QQ 郵箱為例)
- 進入郵箱設置:點擊郵箱界面的“設置”選項。
- 找到第三方服務:在設置頁面的“常規”選項下,找到“第三方服務”相關設置。
- 開啟服務并獲取授權碼:
- 如果服務未開啟,點擊“開啟服務”。
- 開啟后,系統會生成一個授權碼,該授權碼專門用于程序驗證,不同于普通登錄密碼。
- 授權碼相當于獨立密碼,用于提高賬號安全性,需妥善保存,后續在程序配置中會用到。
- 其他郵箱配置類似:如 163 郵箱等,配置過程大致相同,找到類似“第三方服務”或“授權碼”相關設置,進行相應配置。
二、給 User 類添加字段
- 進入 mybatis 配置類:找到對應的配置類,注釋掉其他表,僅保留 User 表。
- 更新數據庫表:在數據庫中找到對應的 User 表,添加 email 字段,類型為 varchar(100),注釋為“郵箱地址”。
- 重新生成舊類:運行 mybatis generator,生成新的 User 類。
- 備份自定義代碼:提前復制保存 mapper 中自動生成代碼后添加的自定義方法和 SQL 語句,避免重新生成后被覆蓋。
- 恢復自定義代碼:生成新類后,將備份的代碼粘貼回來,并引入相關包。
三、引入郵件發送依賴
在項目中添加 spring-boot-starter-mail 依賴,指定版本為 2.3.4.RELEASE。
四、開發發送郵件接口
-
在 UserController 中添加接口:
- 方法頭:發送郵件,路徑為 sendEmail,參數為 emailaddress(郵件地址)。
- 返回格式匹配。
-
實現方法:
- 校驗郵件地址是否有效:
- 使用 utu 包中的 InternetAddress 類的 validate 方法。
- 創建 Email 工具類,新增 isValidEmailAddress 方法,返回 boolean 值。
- 在該方法中,使用 try-catch 包裹 InternetAddress(email).validate(),若無異常返回 true,否則返回 false。
- 使用該方法檢查傳入的 emailaddress,若無效,返回錯誤響應(枚舉值為 INVALID_EMAIL,中文為“非法的郵件地址”)。
- 檢查是否已注冊:若郵件地址有效,進一步檢查是否已注冊。
- 發送郵件:若以上校驗均通過,執行郵件發送邏輯。
- 校驗郵件地址是否有效:
五、檢查郵件地址是否已注冊
-
在 User Service 中新增方法:
- 方法名:
public boolean checkEmailRegistered(String emailaddress)
- 使用 UserMapper 的
selectOneByEmailaddress
方法,根據傳入的郵件地址查詢用戶。 - 若返回的 User 對象不為空,說明郵件地址已被注冊,返回 false;否則返回 true。
- 方法名:
-
在 UserMapper 中補充 SQL 語句:
- 方法名:
selectOneByEmailaddress(String emailaddress)
- SQL 語句:
SELECT * FROM imcomo_user WHERE emailaddress = #{emailaddress} LIMIT 1
- 方法名:
六、郵件發送邏輯實現
-
在 UserController 中調用檢查方法:
- 在通過郵件地址合法性驗證后,調用
userService.checkEmailRegistered(emailaddress)
。 - 若返回值為 false(郵件地址已被注冊),返回錯誤響應(枚舉值為 EMAIL_ALREADY_BEEN_REGISTERED,中文為“email 地址已被注冊”)。
- 在通過郵件地址合法性驗證后,調用
-
創建 EmailService 接口及實現類:
- 接口方法:
void sendSimpleMessage(String to, String subject, String text)
- 實現類:
EmailServiceImpl
,使用 JavaMailSender 發送郵件。
- 接口方法:
-
配置郵件相關屬性:
- 在配置文件中設置郵件主機、端口號、用戶名、授權碼、編碼及驗證相關屬性。
-
完善郵件發送方法:
- 在
sendSimpleMessage
方法中,創建 SimpleMailMessage 對象,設置發件人(從常量中獲取)、收件人、主題和正文。 - 使用 JavaMailSender 的 send 方法發送郵件。
- 在
-
在 UserController 中調用郵件發送服務:
- 引入 EmailService。
- 調用
emailService.sendSimpleMessage
,傳入郵件地址、主題(從常量中獲取)和驗證碼相關正文。
七、生成隨機驗證碼
-
在 Email 工具類中新增方法:
- 方法名:
public static String generateVerificationCode()
- 創建包含數字、大寫字母、小寫字母的字符列表。
- 使用
Collections.shuffle
打亂列表順序。 - 取列表前六位字符作為驗證碼。
- 返回生成的驗證碼字符串。
- 方法名:
-
測試驗證碼生成方法:編寫測試方法,打印生成的驗證碼,驗證其隨機性和正確性。
八、限制重復發送郵件
-
引入 Redis 依賴:添加
redisson
依賴,用于操作 Redis。 -
在 EmailService 中新增方法:
- 方法名:
public boolean saveEmailToRedis(String emailaddress, String verificationCode)
- 獲取 Redis 客戶端,連接到本地 Redis。
- 使用
getBucket
方法傳入郵箱地址作為 key,獲取對應的 bucket。 - 檢查 bucket 中是否存在值:
- 不存在:使用
set
方法存入驗證碼,設置過期時間為 60 秒,單位為秒,返回 true。 - 存在:返回 false,表示 60 秒內已發送過郵件。
- 不存在:使用
- 方法名:
-
在 UserController 中調用并處理:
- 調用
emailService.saveEmailToRedis
,傳入郵箱地址和驗證碼。 - 根據返回值判斷:
- 返回 true:發送郵件,返回成功響應。
- 返回 false:返回錯誤響應,提示郵件已發送,請稍后再試。
- 調用
九、完善郵件發送內容
將生成的驗證碼添加到郵件正文內容中。
十、測試驗證
- 測試非法郵件地址:發送格式錯誤的郵件地址,驗證是否被攔截。
- 測試已注冊郵箱:將郵箱地址設置為已注冊的用戶,驗證是否被攔截。
- 測試重復發送:短時間內反復發送郵件,驗證是否被攔截。
十一、總結
通過校驗郵件地址合法性、檢查是否已注冊、限制重復發送以及使用 Redis 緩存驗證碼,完善了郵件發送接口的功能,提高了系統的安全性和穩定性。
課程筆記:注冊接口升級與郵箱驗證總結
一、注冊接口升級
-
調整入參:
- 增加
emailaddress
(郵箱地址)和verificationcode
(驗證碼)兩個參數。
- 增加
-
增加校驗:
- 非空校驗:對郵箱和驗證碼進行非空校驗,新建異常類
26email不能為空
和27驗證碼不能為空
。 - 郵箱是否已注冊校驗:調用
checkEmailRegister
方法,判斷郵箱是否已被注冊。 - 郵箱和驗證碼匹配校驗:在
emailService
中新增checkEmailAndCode
方法,通過 Redis 獲取存儲的驗證碼并與用戶傳入的驗證碼進行比對。
- 非空校驗:對郵箱和驗證碼進行非空校驗,新建異常類
-
數據存儲:
- 注冊成功時,將郵箱地址存入數據庫用戶表中。
二、校驗流程
- 非空校驗:確保用戶名、密碼、郵箱和驗證碼均不為空。
- 郵箱注冊狀態校驗:防止重復注冊。
- 驗證碼匹配校驗:確保用戶提供的驗證碼與 Redis 中存儲的驗證碼一致。
三、驗證碼存儲選擇
- 為什么選擇 Redis 而不是數據庫:
- 臨時性:驗證碼僅一次有效,注冊成功后即無作用,無需長期存儲。
- 過期機制:Redis 提供方便的過期時間設置,自動處理驗證碼過期,而數據庫難以實現自動過期。
四、總結
通過升級注冊接口,增加郵箱和驗證碼參數及相關校驗,確保了用戶注冊流程的安全性和完整性。使用 Redis 存儲驗證碼,利用其過期機制,有效防止了惡意重復注冊和驗證碼濫用。整個流程包括發送驗證碼、校驗郵箱是否已注冊、驗證碼匹配校驗以及最終的用戶注冊,各步驟緊密銜接,確保了用戶注冊信息的準確性和系統安全性。
課程筆記:登錄狀態的保存和驗證
一、學習背景
在企業級權限認證中,登錄是第一步,不僅需要驗證用戶身份,還要保存登錄狀態,以便用戶后續操作時系統能夠識別其身份。
二、HTTP 無狀態特性
HTTP 協議是無狀態的,意味著每個請求都是獨立的,請求之間不攜帶狀態信息。即使前一個請求通過驗證,下一個請求仍需重新驗證。這要求我們采取措施保存憑證,以解決無狀態帶來的問題。
三、憑證保存機制
1. 第一次登錄驗證
用戶第一次登錄時,需要提供用戶名和密碼等信息進行嚴格的身份驗證。
2. Session 的創建
驗證通過后,服務器為用戶創建一個 Session,Session 是保存在服務器端的數據結構,用于跟蹤用戶狀態。
3. Cookie 的作用
服務器返回給客戶端一個 Cookie,其中包含 Session ID。客戶端(如瀏覽器)在后續請求中攜帶此 Cookie,服務器通過 Session ID 找到對應的 Session,從而識別用戶身份。
四、Session 和 Cookie 的工作流程
- 用戶發送登錄請求:包含用戶名和密碼。
- 服務器驗證并創建 Session:驗證通過后,生成 Session 并返回包含 Session ID 的 Cookie。
- 客戶端保存 Cookie:瀏覽器保存 Cookie,在后續請求中自動攜帶。
- 服務器識別用戶:通過 Cookie 中的 Session ID 找到對應的 Session,獲取用戶狀態和數據。
五、特殊情況處理
如果客戶端禁用 Cookie,可以采用 URL 重寫的方式,在每個請求的 URL 后附加必要的身份參數,以便服務器進行校驗。
六、總結
本小節介紹了登錄狀態保存和驗證的基本原理和流程,重點講解了 HTTP 無狀態特性下如何通過 Session 和 Cookie 解決用戶身份識別問題。下一個小節將深入探討 Session 的細節和應用。
課程筆記:深入理解Session
一、Session 的安全性
-
用戶空間的獨立性:
- 每個用戶的Session空間是獨立的,即使使用相同的key存儲數據,也不會相互影響。
- Tomcat會為每個用戶分配獨立的Session ID,每個Session ID對應自己的空間。
-
Session ID 的生成規則:
- 目標:保證唯一性,防止重復。
- 方法:通常結合隨機數、當前時間(過濾大部分同時觸發的情況)和JVM的ID值(區分不同服務器)。
二、Session 劫持與防護
-
Session 劫持:
- 概念:攻擊者竊取用戶的Session ID,冒充用戶進行操作。
- 風險:可能導致用戶數據泄露、被惡意操作等。
-
防護措施:
- HttpOnly 標記:服務器告知客戶端,Session Cookie不允許通過前端代碼讀取,僅允許瀏覽器讀取。
- Secure 標記:如果項目支持HTTPS,標記Session Cookie僅在HTTPS協議下傳輸。
三、Session 的缺點
-
擴展性差:
- 分布式環境下,多臺機器需要同步和復制Session,處理復雜。
-
服務端存儲壓力:
- 用戶量大時,存儲大量Session數據(無論在內存、Redis還是數據庫中)都會帶來挑戰。
四、實際操作演示
-
Postman 測試流程:
- 默認Cookie中包含Session ID,每個用戶的Session ID唯一。
- 刪除Cookie后請求,服務端會創建新的Session并要求設置Cookie。
- 設置Cookie后,后續請求攜帶該Session ID,服務端據此識別用戶。
-
瀏覽器中的Session Cookie:
- 查看請求中的Cookie,包含JSession Cookie,代表用戶唯一標識。
- 注意保護Session ID,防止泄露。
五、總結
Session用于保存用戶狀態,其安全性至關重要。通過理解Session的工作原理、Session ID的生成規則以及劫持與防護措施,可以更好地保障用戶數據安全。Session存在擴展性和存儲壓力的缺點,后續將學習JWT來克服這些問題。
課程筆記:JWT 介紹與原理
一、JWT 的重要性
- 獨特優勢:相比 Session 和 Cookie,JWT 有自身的特點和優勢。
- 項目應用:后續小節將把項目中原有的 Session 驗證方式升級為 JWT。
二、JWT 的基本概念
- 定義:JWT(JSON Web Token)是一種流行的用于網站身份驗證的認證方案。
- 官網:jwt.io ,提供調試工具用于編碼和解碼。
三、JWT 的組成
JWT 由三部分組成,每部分之間用英文半角句號(.)分隔:
-
Header(頭部):
- 包含兩個功能:簽名算法和令牌類型。
- 示例:
{"alg": "HS256", "typ": "JWT"}
,其中alg
是簽名算法,typ
是令牌類型。
-
Payload(消息體):
- 是 JWT 中最重要的部分,用于放置業務相關數據。
- 示例:
{"sub": "1234567890", "name": "John Doe", "iat": 1516239022}
,其中name
可改為用戶名、身份、用戶 ID 等。
-
Signature(簽名):
- 用于驗證消息是否被更改過,保證數據安全。
- 生成方式:將 Header 和 Payload 編碼后,用 Secret 進行簽名。
四、JWT 的生成與驗證
-
生成:
- Header 和 Payload 分別經過 Base64 URL 編碼。
- 使用 Secret 對編碼后的 Header 和 Payload 進行簽名,生成 Signature。
- 三部分組合成完整的 JWT。
-
驗證:
- 服務端解碼 JWT,驗證 Signature 的有效性。
- 如果 Signature 無效,說明 JWT 被篡改。
五、Session 與 JWT 的對比
流程對比
-
Session 流程:
- 客戶端發起 HTTP 請求,服務端創建 Session,生成唯一的 Session ID。
- 服務端要求客戶端保存 Session ID 到 Cookie。
- 后續請求客戶端攜帶 Cookie,服務端通過 Session ID 識別用戶。
-
JWT 流程:
- 客戶端發起 HTTP 請求,服務端校驗用戶名和密碼。
- 校驗通過后,服務端將用戶信息轉換為 JWT 并發送給客戶端。
- 后續請求客戶端攜帶 JWT,服務端解碼 JWT 獲取用戶信息。
優缺點對比
-
Session:
- 優點:簡單方便,適合小規模網站。
- 缺點:
- 擴展性差:用戶量大時,分布式架構下存儲和同步 Session 成本高。
- 需要存儲數據:服務端需為每個用戶開辟空間存儲 Session。
-
JWT:
- 優點:
- 減少存儲開銷:服務端無需存儲用戶信息,直接將信息編碼到 JWT 中。
- 可擴展性強:分布式架構下,各服務器可獨立校驗 JWT。
- 可用于交換信息:直接攜帶用戶相關業務數據。
- 防止篡改:簽名機制保證 JWT 的完整性。
- 缺點:
- 默認不加密:不適合保存敏感信息,如密碼。
- 無法臨時廢止:一旦發出,無法主動使其失效,需設置合理過期時間。
- 有效期評估難:過長或過短的過期時間都會帶來問題。
- 網絡開銷高:相比 Session ID,JWT 字符串較長。
- 優點:
六、總結
JWT 憑借其減少存儲開銷和良好的擴展性,在互聯網公司中應用越來越廣泛。盡管存在一些缺點,但通過合理設置和使用,可以充分發揮其優勢。后續小節將進入 JWT 的實際開發應用。
課程筆記:項目實戰 - 用戶校驗從Session升級為JWT
一、項目實戰目標
將用戶校驗從傳統的Session Cookie升級為JWT。
二、主要修改內容
- 登錄接口升級:不再保存Session,改為在登錄時生成JWT Token并返回給用戶。
- 過濾器修改:包括用戶過濾器和管理員過濾器,以適應JWT校驗。
- 用戶獲取方式升級:調整從請求中獲取用戶信息的方式。
三、實戰步驟
1. 引入JWT依賴
在pom.xml
中添加JWT依賴:
<dependency><groupId>com.off0</groupId><artifactId>java-jwt</artifactId><version>3.14.0</version>
</dependency>
手動刷新項目,確保依賴正確下載。
2. 修改登錄接口
(1) 引入必要的包和工具
確保項目中已引入JWT相關的包和工具。
(2) 來到UserController
找到login
接口,準備進行改造。
(3) 保留原有登錄邏輯
保留用戶名和密碼的判空邏輯,以及通過用戶名和密碼獲取用戶信息的邏輯。
(4) 生成JWT Token
在原有登錄邏輯的基礎上,添加JWT Token的生成代碼:
// 定義算法
Algorithm algorithm = Algorithm.HMAC256(Constant.JWT_KEY);// 創建JWT
String token = JWT.create().withClaim(Constant.USERNAME, user.getUsername()).withClaim(Constant.USER_ID, user.getId()).withClaim(Constant.USER_ROW, user.getRow()).withExpiresAt(new Date(System.currentTimeMillis() + Constant.JWT_EXPIRE_TIME)).sign(algorithm);// 返回token
return APIResponse.success(token);
3. 定義常量
在Constant
類中添加以下常量:
public static final String JWT_KEY = "your_secret_key"; // JWT密鑰
public static final String USERNAME = "username"; // 用戶名常量
public static final String USER_ID = "user_id"; // 用戶ID常量
public static final String USER_ROW = "user_row"; // 用戶行常量
public static final long JWT_EXPIRE_TIME = 1000L * 60 * 60 * 24 * 1000; // JWT過期時間(1000天)
四、總結
通過上述步驟,完成了將用戶校驗從Session升級為JWT的主要工作。登錄接口已改造為生成并返回JWT Token,后續請求中客戶端需將Token放在請求頭中,由服務器進行校驗。接下來,還需對過濾器和用戶獲取方式進行相應升級,以全面支持JWT校驗。
課程筆記:項目實戰 - JWT 校驗與過濾器升級
一、項目重啟與環境準備
- 重啟項目:清理緩存、刪除target目錄,重新生成項目文件。
- 解決包不存在問題:
- 刷新Maven依賴,手動觸發下載。
- 調整IDEA設置(打開override、importing選項,檢測JDK版本,配置process resources)。
- 清空IDEA緩存(File -> Invalidate Caches)。
二、獲取JWT Token
- 測試新接口:使用用戶名和密碼調用login for jwt接口。
- 驗證Token:將返回的Token復制到jwt.io網站,解析查看內容是否包含用戶名、用戶ID、用戶角色和過期時間等信息。
三、過濾器升級
-
修改用戶過濾器:
- 刪除原從Session獲取用戶信息的代碼。
- 從請求頭獲取JWT Token(約定Header的Key為jwt token)。
- 校驗Token:
- 使用Algorithm.HMAC256(Constant.JWT_KEY)生成Algorithm對象。
- 通過JWT.require(algorithm).build()生成Verifier。
- 使用Verifier.verify(token)解碼Token,獲取用戶信息并設置到CurrentUser對象中。
- 處理解碼異常:
- Token過期異常(TokenExpiredException)。
- Token解碼失敗異常(JWTDecodeException)。
-
完善用戶過濾器配置:
- 修改方法名為user filter config。
- 增加對更新用戶信息接口的攔截。
四、總結
完成了JWT Token的生成與校驗,以及過濾器的相應升級。用戶登錄后,通過在請求頭中攜帶JWT Token進行身份校驗,取代了原有的Session方式。在實際操作中,要注意處理Maven依賴和IDEA緩存等問題,確保項目順利運行。