網(wǎng)站首頁 編程語言 正文
1. 引言
在傳統(tǒng)的項目中,用戶登錄成功,將用戶信息保存在session中,這種方式在微服務(wù)架構(gòu)中會產(chǎn)生一系列問題。例如在購物車服務(wù)具有多臺服務(wù)器,當(dāng)一個請求落在購物車1號服務(wù)器后,其session保存了用戶信息,另一個請求落在了購物車2號服務(wù)器,發(fā)現(xiàn)沒有用戶信息,則重新需要進行登錄。服務(wù)器之間有session不共享的問題。為了解決這一問題,tomcat提出了內(nèi)存拷貝,即只需要配置一些信息即可實現(xiàn)多臺服務(wù)器之間的session拷貝,但是這種解決方案也有缺陷,例如:
- 浪費空間
- 拷貝有延時,如果在延時內(nèi)有請求訪問,則還會出現(xiàn)上述問題
為了解決此類問題,我們需要使用多個服務(wù)共享的信息平臺,例如Redis
2. 流程圖及代碼實現(xiàn)
直接上流程圖
流程圖簡潔明了,其中需要注意的是
Redis中存入驗證碼的key是手機號拼接的字符串,為什么保存用戶到Redis的key要使用隨機token,而不是手機號拼接的字符串呢?
因為在用戶登錄注冊時,服務(wù)器會獲取到手機號,所以可以使用手機號作為key,進行驗證手機號和驗證碼時也方便進行匹對,那么在保存用戶信息到Redis時為什么要使用隨機token呢?因為在用戶獨立成功后,用戶的每次請求都會攜帶cookie,如果將保存用戶信息的key設(shè)置為含手機號的,那么用戶的請求中的cookie也需要攜帶手機號,這樣就會有一定的安全風(fēng)險,所以在用戶登錄成功后,我們隨機生成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("請勿重復(fù)獲取驗證碼");
return Result.fail("請勿重復(fù)獲取驗證碼");
}
//4. 符合,生成驗證碼
String code = RandomUtil.randomNumbers(6);
//5. 保存驗證碼到redis,并設(shè)置有效期1分鐘,在設(shè)置key的時候,可以提前設(shè)置一個常量,然后在這里引用即可
stringRedisTemplate.opsForValue().set("loginCode:"+phone,code,1, TimeUnit.MINUTES);
//5. 發(fā)送驗證碼 模擬發(fā)送
log.debug("發(fā)送短信驗證碼成功,驗證碼:{}",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.一致,根據(jù)手機號查詢用戶
User user = query().eq("phone", phone).one();
//5. 判斷用戶是否存在
if (user == null){
//6. 不存在,創(chuàng)建新用戶
user = createUserWithPhone(phone);
}
//7.保存用戶信息到session
// 生成token
String token = UUID.randomUUID().toString();
// 將User轉(zhuǎn)為Map
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userDtoMap = BeanUtil.beanToMap(userDTO);
// 存儲
stringRedisTemplate.opsForHash().putAll("login:token:"+token,userDtoMap);
// 設(shè)置有效期30分鐘
stringRedisTemplate.expire("login:token:"+token,30, TimeUnit.MINUTES);
// 返回token給前端
return Result.ok(token);
}
2.3 請求攔截器
有些請求是需要用戶登錄才能進行訪問的,所以我們設(shè)置一個登錄攔截器先攔截請求,判斷用戶是否登錄,如果登錄了就進行放行即可。
2.3.1 實現(xiàn)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數(shù)據(jù)轉(zhuǎn)為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之前運行,在這個方法里面可以進行驗證登錄狀態(tài)的操作。
- 為什么要將用戶保存到ThreadLocal?因為每一個線程都是獨立的,如果將用戶信息保存到公共變量中,會造成線程安全問題,每一個線程都具備一個ThreadLocal內(nèi)存,我們將用戶信息保存到ThreadLocal中即可實現(xiàn)線程獨享一份用戶信息
- 為什么要刷新Redis中用戶信息的有效時長?因為在session中,其機制是當(dāng)用戶不在使用session中的數(shù)據(jù)超過30分鐘就會剔除session的數(shù)據(jù),所以在攔截器的前置攔截中進行刷新即可
- 上述代碼的第三步驟,為什么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);
}
}
這里配置兩個攔截器,兩個攔截器是有先后順序的,上述已經(jīng)說明,通過設(shè)置order屬性即可配置先后順序,值越小,優(yōu)先級越高。
3. 總結(jié)
用戶獲取驗證碼存放到redis
登錄請求拿著驗證碼和手機號去進行匹配,匹配成功后將用戶信息存入redis
需要登錄的請求會訪問攔截器,兩個攔截器,第一個攔截器RefreshTokenInterceptor負責(zé)刷新redis中用戶信息的TTL,并且如果Redis中有用戶信息,將存入ThreadLocal,第二個攔截器LoginInterceptor 用于檢測ThreadLocal是否具有用戶信息,如果沒有,則前往登錄界面,如果有就放行
原文鏈接:https://blog.csdn.net/weixin_45690465/article/details/124375977
相關(guān)推薦
- 2022-05-22 C語言鏈接屬性的實踐應(yīng)用_C 語言
- 2022-09-29 go日志庫logrus的安裝及快速使用_Golang
- 2022-05-08 python刪除列表元素del,pop(),remove()及clear()_python
- 2022-05-13 Missing essential plugin: org.jetbrains.androidPle
- 2022-09-22 transition transform屬性造成文字抖動及模糊的解決方法
- 2022-04-25 ASP.NET?Core?MVC中過濾器工作原理介紹_實用技巧
- 2022-12-12 python?使用?with?open()?as?讀寫文件的操作方法_python
- 2023-03-20 c#中Invoke與BeginInvoke的用法及說明_C#教程
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支