1.7 Redis代替session的業務流程
1.7.1、設計key的結構
首先我們要思考一下利用redis來存儲數據,那么到底使用哪種結構呢?由于存入的數據比較簡單,我們可以考慮使用String,或者是使用哈希,如下圖,如果使用String,同學們注意他的value,用多占用一點空間,如果使用哈希,則他的value中只會存儲他數據本身,如果不是特別在意內存,其實使用String就可以啦。
哈希表(Hash Table)
也叫散列表。
它是一種根據關鍵碼值(Key value)而直接進行訪問的數據結構。它通過一個哈希函數將關鍵碼映射到表中的一個位置來訪問記錄,以加快查找的速度。在理想情況下,不發生沖突時,可以實現常數時間復雜度的查找、插入和刪除等操作。
1.7.2、設計key的具體細節
所以我們可以使用String結構,就是一個簡單的key,value鍵值對的方式,但是關于key的處理,session他是每個用戶都有自己的session,但是redis的key是共享的,咱們就不能使用code了
在設計這個key的時候,我們之前講過需要滿足兩點
1、key要具有唯一性
2、key要方便攜帶
采用token作為key的原因
如果我們采用phone:手機號這個的數據來存儲當然是可以的,但是如果把這樣的敏感數據存儲到redis中并且從頁面中帶過來畢竟不太合適,所以我們在后臺生成一個隨機串token,然后讓前端帶來這個token就能完成我們的整體邏輯了
1.7.3、整體訪問流程
當注冊完成后,用戶去登錄會去校驗用戶提交的手機號和驗證碼,是否一致,如果一致,則根據手機號查詢用戶信息,不存在則新建,最后將用戶數據保存到redis,,并且生成token作為redis的key,當我們校驗用戶是否登錄時,會去攜帶著token進行訪問,從redis中取出token對應的value,判斷是否存在這個數據,如果沒有則攔截,如果存在則將其保存到threadLocal中,并且放行。
1.8 基于Redis實現短信登錄
這里具體邏輯就不分析了,之前咱們已經重點分析過這個邏輯啦。
DTO:數據傳輸對象data transfer Object
? 代碼——Redis實現短信登錄
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {// 1.校驗手機號String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回錯誤信息return Result.fail("手機號格式錯誤!");}// 3.從redis獲取驗證碼并校驗String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode();if (cacheCode == null || !cacheCode.equals(code)) {// 不一致,報錯return Result.fail("驗證碼錯誤");}// 4.一致,根據手機號查詢用戶 select * from tb_user where phone = ?User user = query().eq("phone", phone).one();// 5.判斷用戶是否存在if (user == null) {// 6.不存在,創建新用戶并保存user = createUserWithPhone(phone);}// 7.保存用戶信息到 redis中// 7.1.隨機生成token,作為登錄令牌String token = UUID.randomUUID().toString(true);// 7.2.將User對象轉為HashMap存儲UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));// 7.3.存儲String tokenKey = LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);// 7.4.設置token有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.返回tokenreturn Result.ok(token);
}
1.9 解決狀態登錄刷新問題
1.9.1 初始方案思路總結:
在這個方案中,他確實可以使用對應路徑的攔截,同時刷新登錄token令牌的存活時間,但是現在這個攔截器他只是攔截需要被攔截的路徑,假設當前用戶訪問了一些不需要攔截的路徑,那么這個攔截器就不會生效,所以此時令牌刷新的動作實際上就不會執行,所以這個方案他是存在問題的
1.9.2 優化方案
既然之前的攔截器無法對不需要攔截的路徑生效,那么我們可以再添加一個攔截器(沒有什么是再加一層解決不了的),在第一個攔截器中攔截所有的路徑,把第二個攔截器做的事情放入到第一個攔截器中,同時刷新令牌,因為第一個攔截器有了threadLocal的數據,所以此時第二個攔截器只需要判斷攔截器中的user對象是否存在即可,完成整體刷新功能。
攔截器:
攔截器就是在請求流轉的過程中,在特定的節點(比如請求到達某個資源或方法之前、之后等)進行統一的、預先定義好的處理操作。
它可以對請求進行一些共性的操作和控制,比如驗證、轉換、添加額外信息等,就像一個關卡對經過的請求進行特定的“盤查”或“加工”,這種理解是比較準確的。
1.9.3 代碼
? 代碼——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;}
}
之后再在MVC的配置層加上攔截器的配置就好:
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// token刷新的攔截器//先對所有的操作都加一層刷新token的動作。這樣用戶即使是在-逛一些不需要驗證登錄身份的模塊(如首頁等),都會將token刷新,這樣只要用戶在操作,那么就一直有30min的過期時間等待耗盡registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);// 登錄攔截器// 這里僅僅將需要用戶進行身份驗證的模塊進行攔截處理。就是驗證用戶是否登錄。registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);}
}