【Redis場景1】使用者登入註冊
細節回顧:
關於cookie
和session
不熟悉的朋友;
建議閱讀該部落格:https://www.cnblogs.com/ityouknow/p/10856177.html
執行流程:
在單體模式下,一般採用這種模式來儲存,傳遞、認證使用者登入、註冊等資訊;
如果瀏覽器禁用Cookies,****如何保障整個機制的正常運轉。
-
url
拼接或者POST
請求:每個請求都攜帶SessionID
-
Token
機制:在使用者登入或者註冊的時候,與使用者資訊繫結一個隨機字串,用於使用者狀態管理
在分散式下的Session問題:
為了支撐更大的流量,後臺往往需要在多臺伺服器中部署,那如果使用者在 A 伺服器登入了,第二次請求跑到服務 B 就會出現登入失效問題如何解決?
-
Nginx ip_hash 策略,服務端使用
Nginx
代理,每個請求按訪問IP
的hash
分配,這樣來自同一 IP 固定訪問一個後臺伺服器,避免了在伺服器 A 建立Session
,第二次分發到伺服器 B 的現象。 -
Session 複製,任何一個伺服器上的
Session
發生改變(增刪改),該節點會把這個Session
的所有內容序列化,然後廣播給所有其它節點。 -
共享 Session,服務端無狀態話,將使用者的
Session
等資訊使用快取中介軟體來統一管理,保障分發到每一個伺服器的響應結果都一致。
第一種策略(Nginx ip_hash 策略)可以看該部落格:https://www.cnblogs.com/xbhog/p/16929786.html
我們主要實現第三種:通過快取中介軟體來統一管理。
解決痛點:
在原來場景中存在的Session不互通的問題(Session資料拷貝),該解決方式有以下問題:
- 每臺伺服器中都有完整的一份
session
資料,伺服器壓力過大。 -
session
拷貝資料時,可能會出現延遲
所以我們需要採用的中介軟體需要有以下特徵(Redis):
- 資料共享
- 基於記憶體讀取
- 滿足資料儲存格式(
KEY:VALUE
)
實現場景:
該場景實現流程:以下分析結合部分程式碼(聚焦於redis的實現);
完整後端程式碼可在Github中
獲取:https://github.com/xbhog/hm-dianping
開發流程:
【獲取驗證碼流程】
sendCode
方法:
- 校驗手機號合法性
- 生成驗證碼
- 儲存驗證碼到
redis
中 - 傳送驗證碼(模擬實現,未呼叫第三方平臺)
- 結束
在第3步的實現如下:
stringRedisTemplate.opsForValue().set(PHONE_CODE_KEY+phone,code,2L,TimeUnit.MINUTES);
使用的stringRedisTemplate
繼承於RedisTemplate
,侷限:key和value
必須是String
型別:
public class StringRedisTemplate extends RedisTemplate<String, String> {
......
}
設定驗證碼Key值:phone:code:
,code
為隨機6位字串。
設定key
的過期時間。
【登入功能】前端根據手機號和驗證碼傳送登入功能(如上圖):觸發login
方法:
-
校驗手機號合法性
-
校驗驗證碼合法性:從
redis
中獲取 -
資料庫通過手機號查詢使用者
-
- 不存在:建立使用者
- 存在:執行後續邏輯(4)
-
UUID
生成隨機TOKEN
-
將使用者資訊存入
Redis
中,設定過期時間 -
返回前端
Token
第2步的實現如下:
String redisCode = stringRedisTemplate.opsForValue().get(PHONE_CODE_KEY + loginForm.getPhone());
if(StringUtils.isBlank(loginForm.getCode()) || !loginForm.getCode().equals(redisCode)){
return Result.fail("驗證碼錯誤");
}
第4、5步的實現如下:
//隨機生成token,作為登入令牌
String token = UUID.randomUUID().toString(true);
//儲存到redis中
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey,userBeanToMap);
//設定過期時間
stringRedisTemplate.expire(tokenKey,30L, TimeUnit.MINUTES);
設定使用者KEY
:"login:token:"
,這裡redis的使用的資料結構是Map
,方便對單一欄位操作。
使用了hashmap
結構,需要單獨對tokenKey
設定過期時間(30m)
;
場景問題:
Redis Key續期問題:
在設定token
的時候,在redis
給的過期時間是30分鐘,這裡就有個問題,使用者在30分鐘內,結束請求,那沒有問題,但只要使用者的線上時間超過30分鐘,redis
刪除token
,直接給使用者強制下線了;這個實在是不符合實際場景。
解決方式:
在請求的過程總給加入一層攔截器,用來重新整理Token
的存活時間。
子問題:
對於攔截器,我們不能攔截所有的路徑,比如獲取驗證碼請求,使用者登入,首頁等;
-
- 使用者在所攔截的範圍內:則可執行重新整理
Token
操作 - 使用者不在攔截的範圍內:無法執行重新整理
Token
操作
- 使用者在所攔截的範圍內:則可執行重新整理
子問題解決:
增加兩層攔截器,第一層攔截全部請求路徑,第二層攔截器基於第一層資訊,對未登入的請求進行攔截。
需要設定兩層攔截器的優先順序,
order():指定要使用執行器順序。預設值為0(最高)
@Configuration
public class MybatisConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
registry.addInterceptor(new RefreshTokeInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}
【攔截器1實現】所有路徑攔截,重新整理登入Token令牌存活時間。
-
獲取
token
-
查詢
redis
使用者 -
- 存在,不攔截(執行3)
- 不存在,攔截
-
使用者資訊儲存到
threadLocal
中 -
重新整理
Token
有效期 -
放行
相關程式碼:
/**
* @author xbhog
* @describe: 攔截器實現:校驗使用者登入狀態
* @date 2022/12/7
*/
public class RefreshTokeInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokeInterceptor(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("authorization");
//如果頁面沒有登入,則沒有token,直接放行給下一個攔截器
if(StringUtils.isEmpty(token)){
return true;
}
String tokenKey = LOGIN_USER_KEY + token;
Map<Object, Object> userRedis = stringRedisTemplate.opsForHash().entries(tokenKey);
if(userRedis.isEmpty()){
return true;
}
UserDTO userDTO = BeanUtil.fillBeanWithMap(userRedis, new UserDTO(), false);
//使用者存在,放到threadLocal
UserHolder.saveUser(userDTO);
//登入續期
stringRedisTemplate.expire(tokenKey,30L, TimeUnit.MINUTES);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
【攔截器2實現】需要登入的路徑攔截;
實現程式碼:
/**
* @author xbhog
* @describe: 攔截器實現:校驗使用者登入狀態
* @date 2022/12/7
*/
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.判斷是否需要攔截(ThreadLocal中是否有使用者)
if (UserHolder.getUser() == null) {
// 沒有,需要攔截,設定狀態碼
response.setStatus(401);
// 攔截
return false;
}
// 有使用者,則放行
return true;
}
}