目錄
一、基于Session實現登錄流程
1.發送驗證碼:
2.短信驗證碼登錄、注冊:
3.校驗登錄狀態:
4.session共享問題
4.1為什么會出現 Session 集群共享問題?
4.2常見解決方案
1. 基于 Cookie 的 Session(客戶端存儲)
2. Session 復制(服務器間同步)
3. 集中式 Session 存儲(推薦方案)
二、基于redis實現共享session登錄
1.選擇合適的存儲結構
2.修改發送短信的邏輯
3.解決狀態登錄刷新問題
一、基于Session實現登錄流程
1.發送驗證碼:
用戶在提交手機號后,會校驗手機號是否合法,如果不合法,則要求用戶重新輸入手機號
如果手機號合法,后臺此時生成對應的驗證碼,同時將驗證碼進行保存,然后再通過短信的方式將驗證碼發送給用戶
@Overridepublic 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);// 返回okreturn Result.ok();}
2.短信驗證碼登錄、注冊:
用戶將驗證碼和手機號進行輸入,后臺從session中拿到當前驗證碼,然后和用戶輸入的驗證碼進行校驗,如果不一致,則無法通過校驗,如果一致,則后臺根據手機號查詢用戶,如果用戶不存在,則為用戶創建賬號信息,保存到數據庫,無論是否存在,都會將用戶信息保存到session中,方便后續獲得當前登錄信息
@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {// 1.校驗手機號String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回錯誤信息return Result.fail("手機號格式錯誤!");}// 3.校驗驗證碼Object cacheCode = session.getAttribute("code");String code = loginForm.getCode();if(cacheCode == null || !cacheCode.toString().equals(code)){//3.不一致,報錯return Result.fail("驗證碼錯誤");}//一致,根據手機號查詢用戶User user = query().eq("phone", phone).one();//5.判斷用戶是否存在if(user == null){//不存在,則創建user = createUserWithPhone(phone);}//7.保存用戶信息到session中session.setAttribute("user",user);return Result.ok();}
3.校驗登錄狀態:
用戶在請求時候,會從cookie中攜帶者JsessionId到后臺,后臺通過JsessionId從session中拿到用戶信息,如果沒有session信息,則進行攔截,如果有session信息,則將用戶信息保存到threadLocal中,并且放行
攔截器代碼:
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.存在,保存用戶信息到ThreadlocalUserHolder.saveUser((User)user);//6.放行return true;}
}
讓攔截器生效
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 登錄攔截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);// token刷新的攔截器registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}
}
4.session共享問題
在分布式系統或集群環境中,Session 共享問題是指多個服務器節點之間如何共享用戶會話(Session)數據,以確保用戶在不同節點間切換時會話不丟失、狀態一致。
4.1為什么會出現 Session 集群共享問題?
-
無狀態協議特性
HTTP 是無狀態協議,服務器通過 Session 機制維持用戶狀態(如登錄信息、購物車數據等)。在單體應用中,Session 通常存儲在服務器內存中。
但在集群 / 分布式架構中,用戶請求可能被負載均衡器分配到不同節點,若節點間無法共享 Session,會導致:- 用戶重復登錄
- 會話數據丟失(如購物車內容丟失)
- 業務邏輯異常(如分布式事務狀態不一致)
-
集群節點的獨立性
每個節點的內存獨立,無法直接訪問其他節點的 Session 數據。
4.2常見解決方案
1. 基于 Cookie 的 Session(客戶端存儲)
- 原理:將 Session 數據序列化后存儲在客戶端 Cookie 中,每次請求攜帶 Cookie,服務器從 Cookie 中讀取 Session。
- 優點:
- 無需服務器間共享數據,完全分布式
- 減輕服務器存儲壓力
- 缺點:
- 安全性差(敏感數據需加密,否則易被篡改)
- Cookie 大小受限(通常不超過 4KB)
- 不適用于大量 Session 數據
適用場景:輕量級應用、非敏感數據場景。
2. Session 復制(服務器間同步)
- 原理:多個節點之間通過網絡實時復制 Session 數據,確保每個節點擁有全量 Session 副本。
- 優點:
- 透明性高,應用無需修改代碼
- 適用于中小型集群
- 缺點:
- 性能開銷大(Session 變更時需全量同步,節點越多延遲越高)
- 內存占用高(每個節點存儲所有 Session)
- 擴展性差,不適合大規模集群
適用場景:節點較少(≤5 個)、Session 更新不頻繁的場景。
3. 集中式 Session 存儲(推薦方案)
將 Session 數據存儲在獨立的第三方存儲系統中(如 Redis、Memcached、數據庫),所有節點通過訪問統一存儲獲取 Session。
- 優點:
- 高性能(內存存儲,Redis 支持集群和持久化)
- 可擴展性強(支持動態擴縮容)
- 數據安全(可配置密碼、加密傳輸)
- 缺點:
- 需要額外維護緩存集群
- 存在緩存穿透、緩存雪崩風險(需通過布隆過濾器、限流等機制防范)
適用場景:大型分布式系統、高并發場景。
這里我們使用redis
二、基于redis實現共享session登錄
1.選擇合適的存儲結構
首先我們要思考一下利用redis來存儲數據,那么到底使用哪種結構呢?由于存入的數據比較簡單,我們可以考慮使用String,或者是使用哈希,如上圖,如果使用String,注意他的value,用多占用一點空間,如果使用哈希,則他的value中只會存儲他數據本身,如果不是特別在意內存,其實使用String就可以啦。
String 結構(以 JSON 字符串存用戶信息)
優點
- 存儲直觀易懂:直接將整個用戶對象序列化為 JSON 字符串存到單個 key - value 中,業務側讀取時反序列化就能拿到完整用戶信息,開發調試時查看數據很方便,比如快速通過?
get heima:user:1
?命令看到?{name:"Jack", age:21}
?全貌 。- 整體讀寫高效:存和取都是針對整個用戶對象操作,Redis 處理單 key 的讀寫命令(如?
SET
/GET
?)本身就很高效,對于頻繁需要完整用戶信息的場景,一次操作就能滿足需求 。缺點
- 局部更新成本高:若只想修改用戶的?
age
?字段,得先?GET
?整個 JSON 字符串,反序列化成對象改字段,再序列化成 JSON 存回 Redis,多了序列化 / 反序列化以及全量寫的開銷,操作不靈活 。- 內存占用相對高:每個用戶對象要存完整的 JSON 字符串,包含字段名、分隔符等冗余信息,大量用戶數據存儲時,內存浪費會更明顯 。
Hash 結構(字段獨立存儲)
優點
- 操作靈活性強:支持對單個字段 CRUD,比如只想改?
age
,直接執行?HSET heima:user:1 age 22
?即可,無需處理整個對象的序列化 / 反序列化,更新局部字段效率高 。- 內存更節省:只存字段名和對應值,沒有額外的 JSON 格式冗余(如分隔符、字段名重復存儲),相同用戶數據量下,Hash 結構內存占用通常更少,適合大規模用戶數據存儲 。
缺點
- 數據查看稍復雜:想看完整用戶信息,得用?
HGETALL
?命令獲取所有字段再拼接,不像 String 結構?GET
?命令直接拿到直觀的 JSON 結果,對調試不太友好 。- 批量操作有局限:若業務經常需要一次性獲取 / 設置整個用戶對象的多個字段,Hash 得多次調用?
HSET
/HGET
?,相比 String 結構一次?SET
/GET
?,在這類場景下效率可能稍低 。
2.修改發送短信的邏輯
現在驗證碼要存入redis,用于之后的登錄注冊
發送驗證碼:
@Overridepublic Result sendCode(String phone) {// 1 校驗手機號if(RegexUtils.isPhoneInvalid(phone)){return Result.fail("手機號格式不合法");}// 2 符合,生成驗證碼,不符合返回String code = RandomUtil.randomNumbers(6);// 3 保存驗證碼到sessionstringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone,code,RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);// 4 發送驗證碼log.info("發送驗證碼成功:{}",code);return Result.ok();}
登錄注冊:
@Overridepublic Result login(LoginFormDTO loginForm) {String phone = loginForm.getPhone();// 1 校驗手機號if(RegexUtils.isPhoneInvalid(loginForm.getPhone())){return Result.fail("手機號格式不合法");}// 2 校驗驗證碼String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);String code = loginForm.getCode();if(cacheCode == null || !cacheCode.equals(code)){return Result.fail("驗證碼錯誤");}// 3 一致,根據用戶手機號查詢用戶User user = query().eq("phone", phone).one();if(user == null){user = createUserwithPhone(phone);}UserDTO userDTO = BeanUtil.copyProperties(user,UserDTO.class);Map<String, Object> userDtomap = BeanUtil.beanToMap(userDTO,new HashMap<>() ,CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString()));// 4 生成tokenString token = UUID.randomUUID().toString();stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token,userDtomap);stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token,RedisConstants.LOGIN_USER_TTL,TimeUnit.MINUTES);return Result.ok(token);}
當注冊完成后,用戶去登錄會去校驗用戶提交的手機號和驗證碼,是否一致,如果一致,則根據手機號查詢用戶信息,不存在則新建,最后將用戶數據保存到redis,并且生成token作為redis的key,當我們校驗用戶是否登錄時,會去攜帶著token進行訪問,從redis中取出token對應的value,判斷是否存在這個數據,如果沒有則攔截,如果存在則將其保存到threadLocal中,并且放行。
3.解決狀態登錄刷新問題
在這個方案中,他確實可以使用對應路徑的攔截,同時刷新登錄token令牌的存活時間,但是現在這個攔截器他只是攔截需要被攔截的路徑,假設當前用戶訪問了一些不需要攔截的路徑,那么這個攔截器就不會生效,所以此時令牌刷新的動作實際上就不會執行,所以這個方案他是存在問題的
優化方案
既然之前的攔截器無法對不需要攔截的路徑生效,那么我們可以添加一個攔截器,在第一個攔截器中攔截所有的路徑,把第二個攔截器做的事情放入到第一個攔截器中,同時刷新令牌,因為第一個攔截器有了threadLocal的數據,所以此時第二個攔截器只需要判斷攔截器中的user對象是否存在即可,完成整體刷新功能。
RefreshTokenInterceptor:
public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.獲取請求頭中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {return true;}// 2.基于TOKEN獲取redis中的用戶String key = LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);// 3.判斷用戶是否存在if (userMap.isEmpty()) {return true;}// 5.將查詢到的hash數據轉為UserDTOUserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6.存在,保存用戶信息到 ThreadLocalUserHolder.saveUser(userDTO);// 7.刷新token有效期stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用戶UserHolder.removeUser();}
}
LoginInterceptor:
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.判斷是否需要攔截(ThreadLocal中是否有用戶)if (UserHolder.getUser() == null) {// 沒有,需要攔截,設置狀態碼response.setStatus(401);// 攔截return false;}// 有用戶,則放行return true;}
}
看到這如果有用的話記得點贊關注哦,后續會更新更多內容的!!