Redis實(shí)現(xiàn)登錄注冊(cè)的示例代碼
1. 引言
在傳統(tǒng)的項(xiàng)目中,用戶登錄成功,將用戶信息保存在session中,這種方式在微服務(wù)架構(gòu)中會(huì)產(chǎn)生一系列問題。例如在購物車服務(wù)具有多臺(tái)服務(wù)器,當(dāng)一個(gè)請(qǐng)求落在購物車1號(hào)服務(wù)器后,其session保存了用戶信息,另一個(gè)請(qǐng)求落在了購物車2號(hào)服務(wù)器,發(fā)現(xiàn)沒有用戶信息,則重新需要進(jìn)行登錄。服務(wù)器之間有session不共享的問題。為了解決這一問題,tomcat提出了內(nèi)存拷貝,即只需要配置一些信息即可實(shí)現(xiàn)多臺(tái)服務(wù)器之間的session拷貝,但是這種解決方案也有缺陷,例如:
- 浪費(fèi)空間
- 拷貝有延時(shí),如果在延時(shí)內(nèi)有請(qǐng)求訪問,則還會(huì)出現(xiàn)上述問題
為了解決此類問題,我們需要使用多個(gè)服務(wù)共享的信息平臺(tái),例如Redis
2. 流程圖及代碼實(shí)現(xiàn)
直接上流程圖

流程圖簡潔明了,其中需要注意的是
Redis中存入驗(yàn)證碼的key是手機(jī)號(hào)拼接的字符串,為什么保存用戶到Redis的key要使用隨機(jī)token,而不是手機(jī)號(hào)拼接的字符串呢?
因?yàn)樵谟脩舻卿涀?cè)時(shí),服務(wù)器會(huì)獲取到手機(jī)號(hào),所以可以使用手機(jī)號(hào)作為key,進(jìn)行驗(yàn)證手機(jī)號(hào)和驗(yàn)證碼時(shí)也方便進(jìn)行匹對(duì),那么在保存用戶信息到Redis時(shí)為什么要使用隨機(jī)token呢?因?yàn)樵谟脩舄?dú)立成功后,用戶的每次請(qǐng)求都會(huì)攜帶cookie,如果將保存用戶信息的key設(shè)置為含手機(jī)號(hào)的,那么用戶的請(qǐng)求中的cookie也需要攜帶手機(jī)號(hào),這樣就會(huì)有一定的安全風(fēng)險(xiǎn),所以在用戶登錄成功后,我們隨機(jī)生成token,用token作為key,并且返回給前端token,這樣前端請(qǐng)求時(shí)就會(huì)攜帶token,也避免了安全隱患。
2.1 生成驗(yàn)證碼保存到Redis
@Override
public Result sedCode(String phone, HttpSession session) {
//1. 校驗(yàn)手機(jī)號(hào)
if (RegexUtils.isPhoneInvalid(phone)) {
//2.如果不符合,返回錯(cuò)誤信息
return Result.fail("手機(jī)號(hào)格式錯(cuò)誤");
}
// 3.從redis里獲取驗(yàn)證碼是否存在
if(null==stringRedisTemplate.opsForValue().get("loginCode" + phone)){
log.info("請(qǐng)勿重復(fù)獲取驗(yàn)證碼");
return Result.fail("請(qǐng)勿重復(fù)獲取驗(yàn)證碼");
}
//4. 符合,生成驗(yàn)證碼
String code = RandomUtil.randomNumbers(6);
//5. 保存驗(yàn)證碼到redis,并設(shè)置有效期1分鐘,在設(shè)置key的時(shí)候,可以提前設(shè)置一個(gè)常量,然后在這里引用即可
stringRedisTemplate.opsForValue().set("loginCode:"+phone,code,1, TimeUnit.MINUTES);
//5. 發(fā)送驗(yàn)證碼 模擬發(fā)送
log.debug("發(fā)送短信驗(yàn)證碼成功,驗(yàn)證碼:{}",code);
//返回ok
return Result.ok();
}
2.2 登錄驗(yàn)證
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1. 校驗(yàn)手機(jī)號(hào)
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手機(jī)號(hào)格式錯(cuò)誤");
}
//2. 獲取Redis中的校驗(yàn)驗(yàn)證碼
String cacheCode = stringRedisTemplate.opsForValue().get("loginCode" + phone);
// 3.獲取表單中的驗(yàn)證碼
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.toString().equals(code)){
//3. 不一致,報(bào)錯(cuò)
return Result.fail("驗(yàn)證碼錯(cuò)誤");
}
//4.一致,根據(jù)手機(jī)號(hào)查詢用戶
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);
// 存儲(chǔ)
stringRedisTemplate.opsForHash().putAll("login:token:"+token,userDtoMap);
// 設(shè)置有效期30分鐘
stringRedisTemplate.expire("login:token:"+token,30, TimeUnit.MINUTES);
// 返回token給前端
return Result.ok(token);
}
2.3 請(qǐng)求攔截器
有些請(qǐng)求是需要用戶登錄才能進(jìn)行訪問的,所以我們?cè)O(shè)置一個(gè)登錄攔截器先攔截請(qǐng)求,判斷用戶是否登錄,如果登錄了就進(jìn)行放行即可。

2.3.1 實(shí)現(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.獲取請(qǐng)求頭中的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之前運(yùn)行,在這個(gè)方法里面可以進(jìn)行驗(yàn)證登錄狀態(tài)的操作。
- 為什么要將用戶保存到ThreadLocal?因?yàn)槊恳粋€(gè)線程都是獨(dú)立的,如果將用戶信息保存到公共變量中,會(huì)造成線程安全問題,每一個(gè)線程都具備一個(gè)ThreadLocal內(nèi)存,我們將用戶信息保存到ThreadLocal中即可實(shí)現(xiàn)線程獨(dú)享一份用戶信息
- 為什么要刷新Redis中用戶信息的有效時(shí)長?因?yàn)樵趕ession中,其機(jī)制是當(dāng)用戶不在使用session中的數(shù)據(jù)超過30分鐘就會(huì)剔除session的數(shù)據(jù),所以在攔截器的前置攔截中進(jìn)行刷新即可
- 上述代碼的第三步驟,為什么userMap為空了還要放行呢?因?yàn)檫@個(gè)攔截器只是做Redis用戶信息刷新存活時(shí)間的功能,真正攔截的是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();
}
}
兩個(gè)攔截器配置了,但是沒有生效,需要在配置類里進(jìn)行配置
@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);
}
}
這里配置兩個(gè)攔截器,兩個(gè)攔截器是有先后順序的,上述已經(jīng)說明,通過設(shè)置order屬性即可配置先后順序,值越小,優(yōu)先級(jí)越高。
3. 總結(jié)
用戶獲取驗(yàn)證碼存放到redis
登錄請(qǐng)求拿著驗(yàn)證碼和手機(jī)號(hào)去進(jìn)行匹配,匹配成功后將用戶信息存入redis
需要登錄的請(qǐng)求會(huì)訪問攔截器,兩個(gè)攔截器,第一個(gè)攔截器RefreshTokenInterceptor負(fù)責(zé)刷新redis中用戶信息的TTL,并且如果Redis中有用戶信息,將存入ThreadLocal,第二個(gè)攔截器LoginInterceptor 用于檢測ThreadLocal是否具有用戶信息,如果沒有,則前往登錄界面,如果有就放行
到此這篇關(guān)于Redis實(shí)現(xiàn)登錄注冊(cè)的示例代碼的文章就介紹到這了,更多相關(guān)Redis 登錄注冊(cè)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解讀Redis秒殺優(yōu)化方案(阻塞隊(duì)列+基于Stream流的消息隊(duì)列)
該文章介紹了使用Redis的阻塞隊(duì)列和Stream流的消息隊(duì)列來優(yōu)化秒殺系統(tǒng)的方案,通過將秒殺流程拆分為兩條流水線,使用Redis緩存緩解數(shù)據(jù)庫壓力,并結(jié)合Lua腳本進(jìn)行原子性判斷,使用阻塞隊(duì)列和消息隊(duì)列異步處理訂單,有效提高了系統(tǒng)的并發(fā)處理能力和可用性2025-02-02
Redis教程(十二):服務(wù)器管理命令總結(jié)
這篇文章主要介紹了Redis教程(十二):服務(wù)器管理命令總結(jié),本文講解了CONFIGGETparameter、CONFIG SETparameter value、FLUSHALL等命令,需要的朋友可以參考下2015-04-04
使用Jedis線程池returnResource異常注意事項(xiàng)
這篇文章主要介紹了使用Jedis線程池returnResource異常注意事項(xiàng),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03
Redis遠(yuǎn)程字典服務(wù)器?hash類型示例詳解
這篇文章主要介紹了Redis遠(yuǎn)程字典服務(wù)器?hash類型示例詳解,本文通過示例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-08-08
redis的hGetAll函數(shù)的性能問題(記Redis那坑人的HGETALL)
這篇文章主要介紹了redis的hGetAll函數(shù)的性能問題,需要的朋友可以參考下2016-02-02
談?wù)凴edis分布式鎖的正確實(shí)現(xiàn)方法
這篇文章主要給大家介紹了關(guān)于Redis分布式鎖的正確實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Redis具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
Linux中設(shè)置Redis開機(jī)啟動(dòng)的方法
這篇文章主要給大家介紹了關(guān)于Linux中設(shè)置Redis開機(jī)啟動(dòng)的方法,主要包括在CentOS7.0系統(tǒng)和Debian 8.0系統(tǒng)下實(shí)現(xiàn)方法,文中介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來一起看看吧。2017-04-04

