網站首頁 編程語言 正文
1. 引言
在傳統的項目中,用戶登錄成功,將用戶信息保存在session中,這種方式在微服務架構中會產生一系列問題。例如在購物車服務具有多臺服務器,當一個請求落在購物車1號服務器后,其session保存了用戶信息,另一個請求落在了購物車2號服務器,發現沒有用戶信息,則重新需要進行登錄。服務器之間有session不共享的問題。為了解決這一問題,tomcat提出了內存拷貝,即只需要配置一些信息即可實現多臺服務器之間的session拷貝,但是這種解決方案也有缺陷,例如:
- 浪費空間
- 拷貝有延時,如果在延時內有請求訪問,則還會出現上述問題
為了解決此類問題,我們需要使用多個服務共享的信息平臺,例如Redis
2. 流程圖及代碼實現
直接上流程圖
流程圖簡潔明了,其中需要注意的是
Redis中存入驗證碼的key是手機號拼接的字符串,為什么保存用戶到Redis的key要使用隨機token,而不是手機號拼接的字符串呢?
因為在用戶登錄注冊時,服務器會獲取到手機號,所以可以使用手機號作為key,進行驗證手機號和驗證碼時也方便進行匹對,那么在保存用戶信息到Redis時為什么要使用隨機token呢?因為在用戶獨立成功后,用戶的每次請求都會攜帶cookie,如果將保存用戶信息的key設置為含手機號的,那么用戶的請求中的cookie也需要攜帶手機號,這樣就會有一定的安全風險,所以在用戶登錄成功后,我們隨機生成token,用token作為key,并且返回給前端token,這樣前端請求時就會攜帶token,也避免了安全隱患。
2.1 生成驗證碼保存到Redis
@Override
public Result sedCode(String phone, HttpSession session) {
//1. 校驗手機號
if (RegexUtils.isPhoneInvalid(phone)) {
//2.如果不符合,返回錯誤信息
return Result.fail("手機號格式錯誤");
}
// 3.從redis里獲取驗證碼是否存在
if(null==stringRedisTemplate.opsForValue().get("loginCode" + phone)){
log.info("請勿重復獲取驗證碼");
return Result.fail("請勿重復獲取驗證碼");
}
//4. 符合,生成驗證碼
String code = RandomUtil.randomNumbers(6);
//5. 保存驗證碼到redis,并設置有效期1分鐘,在設置key的時候,可以提前設置一個常量,然后在這里引用即可
stringRedisTemplate.opsForValue().set("loginCode:"+phone,code,1, TimeUnit.MINUTES);
//5. 發送驗證碼 模擬發送
log.debug("發送短信驗證碼成功,驗證碼:{}",code);
//返回ok
return Result.ok();
}
2.2 登錄驗證
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1. 校驗手機號
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手機號格式錯誤");
}
//2. 獲取Redis中的校驗驗證碼
String cacheCode = stringRedisTemplate.opsForValue().get("loginCode" + phone);
// 3.獲取表單中的驗證碼
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.toString().equals(code)){
//3. 不一致,報錯
return Result.fail("驗證碼錯誤");
}
//4.一致,根據手機號查詢用戶
User user = query().eq("phone", phone).one();
//5. 判斷用戶是否存在
if (user == null){
//6. 不存在,創建新用戶
user = createUserWithPhone(phone);
}
//7.保存用戶信息到session
// 生成token
String token = UUID.randomUUID().toString();
// 將User轉為Map
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userDtoMap = BeanUtil.beanToMap(userDTO);
// 存儲
stringRedisTemplate.opsForHash().putAll("login:token:"+token,userDtoMap);
// 設置有效期30分鐘
stringRedisTemplate.expire("login:token:"+token,30, TimeUnit.MINUTES);
// 返回token給前端
return Result.ok(token);
}
2.3 請求攔截器
有些請求是需要用戶登錄才能進行訪問的,所以我們設置一個登錄攔截器先攔截請求,判斷用戶是否登錄,如果登錄了就進行放行即可。
2.3.1 實現HandlerInterceptor類
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.獲取請求頭中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
return true;
}
// 2.基于TOKEN獲取redis中的用戶
String key = "login:token:" + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
// 3.判斷用戶是否存在
if (userMap.isEmpty()) {
return true;
}
// 5.將查詢到的hash數據轉為UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 6.存在,保存用戶信息到 ThreadLocal
UserHolder.saveUser(userDTO);
// 7.刷新token有效期
stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用戶
UserHolder.removeUser();
}
}
preHandle方法是在controller之前運行,在這個方法里面可以進行驗證登錄狀態的操作。
- 為什么要將用戶保存到ThreadLocal?因為每一個線程都是獨立的,如果將用戶信息保存到公共變量中,會造成線程安全問題,每一個線程都具備一個ThreadLocal內存,我們將用戶信息保存到ThreadLocal中即可實現線程獨享一份用戶信息
- 為什么要刷新Redis中用戶信息的有效時長?因為在session中,其機制是當用戶不在使用session中的數據超過30分鐘就會剔除session的數據,所以在攔截器的前置攔截中進行刷新即可
- 上述代碼的第三步驟,為什么userMap為空了還要放行呢?因為這個攔截器只是做Redis用戶信息刷新存活時間的功能,真正攔截的是LoginInterceptor,LoginInterceptor代碼在下面展示
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// ThreadLocal中獲取用戶信息
if (UserHolder.getUser()==null){
response.setStatus(401);
return false;
}
// 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用戶
UserHolder.removeUser();
}
}
兩個攔截器配置了,但是沒有生效,需要在配置類里進行配置
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate redisTemplate;
@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 RefreshTokenInterceptor(redisTemplate))
.excludePathPatterns(
"/user/login",
"/user/code"
).order(0);
}
}
這里配置兩個攔截器,兩個攔截器是有先后順序的,上述已經說明,通過設置order屬性即可配置先后順序,值越小,優先級越高。
3. 總結
用戶獲取驗證碼存放到redis
登錄請求拿著驗證碼和手機號去進行匹配,匹配成功后將用戶信息存入redis
需要登錄的請求會訪問攔截器,兩個攔截器,第一個攔截器RefreshTokenInterceptor負責刷新redis中用戶信息的TTL,并且如果Redis中有用戶信息,將存入ThreadLocal,第二個攔截器LoginInterceptor 用于檢測ThreadLocal是否具有用戶信息,如果沒有,則前往登錄界面,如果有就放行
原文鏈接:https://blog.csdn.net/weixin_45690465/article/details/124375977
相關推薦
- 2022-06-17 C#中Parallel類For、ForEach和Invoke使用介紹_C#教程
- 2022-05-26 openwrt安裝docker并啟動的操作方法_docker
- 2022-10-18 Go語言TCP從原理到代碼實現詳解_Golang
- 2022-10-27 Python中對字典的幾個處理方法分享_python
- 2023-03-25 Python?flask?框架使用flask-login?模塊的詳細過程_python
- 2022-08-23 python文件讀取read及readlines兩種方法使用詳解_python
- 2023-07-06 css flex實現div固定在瀏覽器右下角
- 2022-11-16 anaconda打開閃退的解決過程_python
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支