短信登錄
導入黑馬點評項目
導入資料中提供的SQL文件
其中的核心表有:
-
tb_user :用戶表
-
tb_user_info :用戶詳情表
-
tb_shop:用戶信息表
-
tb_shop_type:商戶類型表
-
tb_blog:用戶日記表(達人探店日記)
-
tb_follow:用戶關注表
-
tb_voucher: 優惠劵表
-
tb_voucher:優惠劵的訂單表
-
tb_voucher_order
注意事項:MySQL版本最好采用5.7及以上版本
該項目為單體項目,采用前后端分離的模式,將后端部署在Tomcat上,前端會將其放置在Nginx服務器上。
在PC端或者移動端在請求頁面時,其實是想Nginx發起請求得到的靜態資源,頁面在通過Ajax像我們的服務端發起請求去查詢數據,這些數據可能來自于Redis集群,或者來自MySQL集群。然后再將查詢到的數據返回給前端,前端完成渲染即可。是一種前后端分離的架構。
還會考慮項目的并發能力,項目還具備水平拓展的能力,即項目部署在Tomcat上后,如果單臺Tomcat上支撐不住,還可以做一個水平的擴展形成一個負載均衡的集群,在多臺Tomcat上部署代碼,一旦步入集群之后,將來就會存在一些集群間的數據共享的問題。
!
導入后端模板
從資料中拿到項目源碼,將其復制到IDEA中,然后利用IDEA打開,注意springboot與MyBatis-plus的版本沖突。
啟動項目后,在瀏覽器訪問:http://localhost:8081/shop-type/list,如果看到數據則證明運行沒有問題
注意事項:不要忘記修改application.yml的MySQL,redis地址信息。
導入前端項目:
該項目將整個前端代碼部署到Nginx里,
在資料中提供了Nginx文件夾,將其復制到任意目錄,確保該目錄不包含中文、特殊字符和空格
在該文件夾下打開命令行界面,輸入 start nginx.exe
然后在瀏覽器中點擊F12,打開手機模式,并訪問 http://localhost:8080
基于session實現登錄
短信登錄包括短信的發送,然后是基于短信驗證碼的登錄,最后是對登錄狀態的一個校驗
發送短信驗證碼
用戶會提交自己的手機號碼,服務端接收到手機號后,我們要去驗證用戶的手機號是否合法。
校驗失敗則重新提交,校驗成功則生成驗證碼,還要去保存驗證碼(我們發送驗證碼的目的是讓用戶去做登錄,需要校驗驗證碼,需要現將其保存到session中)。
最后發送驗證碼。
流程圖如下
實現步驟:
首先查看請求,請求方式為POST,請求路徑為/user/code,請求參數為phone,電話號碼,無返回值
先寫發送手機驗證碼的代碼,業務代碼需要在服務層編譯,所以先在Controller層寫出方法名以及要調用的參數,phone,以及session(將驗證碼保存在session中)
代碼展示:
?
public Result sendCode(String phone, HttpSession session) {//1.校驗手機號if (RegexUtils.isPhoneInvalid(phone)) {//2.如果不符合,返回錯誤信息return Result.fail("手機號格式錯誤");}//3.如果符合,生成驗證碼String code = RandomUtil.randomNumbers(6);//4.保存驗證碼到sessionsession.setAttribute("code",code);//5.發送驗證碼(模擬發送驗證碼,該業務并未實現)log.debug("發送短信驗證碼成功,驗證碼:{}",code);// 6.返回結果return Result.ok();}
效果展示:
驗證碼:
短信驗證碼登錄、注冊
用戶收到驗證碼則用來做驗證或者登錄。
首先需要提交手機號和驗證碼到后臺,后臺則去驗證提交的兩個數據,先需要校驗驗證碼(將驗證碼與上一步保存在session中的驗證碼做一個比較)。
不一致則會回退上一步,重新提交。如果一樣則根據手機號到數據庫中去查詢該用戶是否存在。
如果不存在,則說明從未訪問過服務器,手機號是全新的,這是需要去將其注冊成一個新用戶,填充一些基本信息,將新用戶寫入到數據庫中。
如果存在,則可以登錄,服務器需要去保存用戶信息到session中。
登錄與注冊是在一個工程中完成的,
流程圖如下
實現步驟:先輸入驗證碼,發送請求并查看,發現為post請求,且負載信息為json字符串,并且傳入的是驗證碼以及電話號碼
思路及代碼展示:
?@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {//1.校驗手機號String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {//2.如果不符合,返回錯誤信息return Result.fail("手機號格式錯誤");}//2.校驗驗證碼Object cacheCode = session.getAttribute("code");String code = loginForm.getCode();// 一般校驗時,從反向校驗,這種校驗不需要if嵌套,否則會嵌套if,避免if嵌套過深if (cacheCode == null || !cacheCode.toString().equals(code)){//3.不一致,返回錯誤信息return Result.fail("驗證碼錯誤");}//4.一致,根據手機號查詢用戶 select ? * from user where phone = ?User user = query().eq("phone", phone).one();//5.判斷用戶是否存在if (user == null){//6.不存在,創建新用戶并保存// 方法定義在函數中 創建用戶//在創建完用戶到數據庫后還需要保存在session中,所以直接賦值給useruser = createUserWithPhone(phone);}//7.保存用戶信息到session并返回結果session.setAttribute("user",user);//這里不需要返回一個登錄憑證,因為用戶信息已經保存在session中,所以不需要返回一個登錄憑證//session的原理是在服務器端保存用戶信息,并在客戶端保存一個標識符。// 這個標識符就是sessionId,這個標識符在客戶端的cookie中保存,這樣客戶端就可以通過這個標識符來獲取服務器端保存的用戶信息。return Result.ok();}
效果展示:
查詢數據庫得用戶插入成功
完成。
校驗登錄狀態
用戶登錄成功后,訪問一些關鍵的數據時需要去校驗登錄狀態。
服務器將用戶信息保存在session中,那將來用戶訪問時,需要基于session進行校驗。
session是基于cookie的,每一個session都會有一個對應的sessionID保存在瀏覽器的cookie中,所以當用戶訪問服務時一定會攜帶cookie(包含sessionID)。
這時服務器就可以基于cookie中的sessionID拿到session,在從session中拿到用戶,服務器只需要驗證session中是否有用戶信息即可。
如果經過判斷沒有用戶,直接攔截請求。
如果判斷存在,就將用戶信息緩存起來到ThreadLocal
(線程域對象,核心作用是讓每一個線程都有自己的數據副本,避免共享資源引發的競態條件。 在實際開發業務中,每一個請求到達微服務時,都是一個獨立的線程,如果沒有使用ThreadLocal,而是將用戶信息存放在本地變量中,可能出現多線程并發修改的安全問題,ThreadLocal會將數據保存到每一個線程的內部,在縣城內部創建一個ThreadLocalMap去保存,每一個線程都有自己獨立的存儲空間,相互之間沒有干擾)
中(在之后的業務中一定會用到當前登錄的用戶信息,將其緩存方便后續的業務使用),然后服務器放行。
流程圖如下:
實現步驟:
在前面已經完成了發送短信驗證碼以及短信驗證碼登錄注冊的功能,但登錄校驗的功能并沒有完成。
登錄校驗的URL為http://localhost:8080/user/me,用來查詢當前登錄的用戶信息,然后服務端返回正確的用戶信息,則登錄校驗成功,并將其跳轉到首頁。
再流程圖中用戶的請求會帶上cookie,而cookie中含有登錄憑證SessionID,而服務端只需要根據sessionID得到session,在從session中獲取用戶,判斷用戶是否存在。
但是其中有些問題:
我們的登錄校驗是在UserController中編譯的,前端會向userController發起請求,而服務端在UserController的是在對應業務中編譯業務邏輯代碼,而在后期,越來越多的業務需要去校驗用戶的登錄,我們不能在每一個Controller中都去編譯一段登錄校驗的業務代碼,
而在springmvc中的攔截器是在所有的controller執行之前去執行,有了攔截器之后,用戶的請求就不能直接去訪問服務端的controller,所有請求必須要先經過攔截器,再由攔截器判斷該不該放行,那就可以將登錄校驗的業務代碼中放到攔截器中去執行。
而攔截器確實可以實現對用戶登錄的校驗,校驗之后,業務可能需要該用戶信息。
但用戶信息卻在攔截器中,Controller并不能拿到。因此需要將攔截器中的用戶信息傳遞到controller中。
需要考慮線程安全問題,把用戶信息存儲在session中,則需要在跨各種處理組件或頁面時都要從session中去取得用戶信息,這增加了訪問session的開銷。
所以我們這里用ThreadLocal。
ThreadLocal就可以解決該問題,ThreadLocal是一個線程域對象,每一個進入Tomcat的請求都是一個獨立的線程,而ThreadLocal的核心作用就是讓每一個線程都有自己的數據副本,避免共享資源引發的競態條件,這樣就可以讓每一個線程都有對應的獨立內存空間取保存對應用戶,線程之間互不干擾,因此無論幾條請求訪問哪些Controller都可以做到獨立線程,都有自己的獨立用戶信息,當需要用戶信息時,則Controller從ThreadLocal中取出用戶即可。
因此我們要在攔截器中實現校驗登錄,以及將用戶信息存入到ThreadLocal。
隱藏用戶敏感信息
服務端在登錄校驗成功后返回的信息較多,包括用戶密碼,時間,電話等敏感信息,存在泄漏風險,在UserController中,登錄校驗的業務代碼拿到的是用戶的全部信息存入session,session是Tomcat的內存空間,這里面的信息越多,對于服務來講,壓力越大。因此不需要存儲全部信息 。而我們在登錄業務中直接將user的全部信息放入session中,因此需要在DTO包下新建UserDTO,里面只有一些不重要的信息屬性字段,因此只需要在登錄業務中在存入session調用hutool中的工具類將User屬性拷貝給UserDTO。
?session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
然后將攔截器中取出的User對象改為USerDTO對象即可。也需要將UserHolder(用于新建ThreadLocal并調用的工具類)中的User對象改為UserDTO
?public class UserHolder {private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();?public static void saveUser(UserDTO user){tl.set(user);}?public static UserDTO getUser(){return tl.get();}?public static void removeUser(){tl.remove();}}
代碼展示:
在utils包下新建Logininterceptor類(攔截器)
?package com.hmdp.utils;?import com.hmdp.dto.UserDTO;import com.hmdp.entity.User;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import jakarta.servlet.http.HttpSession;import org.springframework.web.servlet.HandlerInterceptor;?public class LoginInterceptor implements HandlerInterceptor {// 前置攔截器@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.獲取請求頭中的sessionHttpSession session = request.getSession();// 2.獲取session的用戶Object user = session.getAttribute("user");// 3.判斷用戶是否存在if (user == null) {// 4.不存在 攔截器攔截 返回401狀態碼 未授權response.setStatus(401);return false;}// 5.存在,保存用戶信息到ThreadLocal 并放行// 在工具類中定義了一個UserHolder 是一個線程安全的ThreadLocal變量,用于保存當前線程的用戶信息。// 其中有三個方法:saveUser( 保存),getUser(拿到),removeUser(移除)。UserHolder.saveUser((UserDTO) user);return true;}?// 攔截器 后處理@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用戶 避免用戶泄漏UserHolder.removeUser();}}
在配置包下新建MVCConfig類
代碼如下:
?package com.hmdp.config;?import com.hmdp.utils.LoginInterceptor;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configurationpublic class MvcConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor())// 除了這些路徑,其他路徑都進行攔截.excludePathPatterns("/user/code","/user/login","/blog/hot","/voucher/**","/shop/**","/shop-type/**","/upload/**","/blog/query/hot","/druid/**");?}}
至此,校驗登錄狀態業務完成。
測試:
登錄校驗成功,并且數據也未泄露。
希望對大家有所幫助!!