基于Redis實(shí)現(xiàn)短信驗(yàn)證碼登錄功能
1 基于Session實(shí)現(xiàn)短信驗(yàn)證碼登錄

/**
* 發(fā)送驗(yàn)證碼
*/
@Override
public Result sendCode(String phone, HttpSession session) {
// 1、判斷手機(jī)號(hào)是否合法
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手機(jī)號(hào)格式不正確");
}
// 2、手機(jī)號(hào)合法,生成驗(yàn)證碼,并保存到Session中
String code = RandomUtil.randomNumbers(6);
session.setAttribute(SystemConstants.VERIFY_CODE, code);
// 3、發(fā)送驗(yàn)證碼
log.info("驗(yàn)證碼:{}", code);
return Result.ok();
}
/**
* 用戶登錄
*/
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
String code = loginForm.getCode();
// 1、判斷手機(jī)號(hào)是否合法
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手機(jī)號(hào)格式不正確");
}
// 2、判斷驗(yàn)證碼是否正確
String sessionCode = (String) session.getAttribute(LOGIN_CODE);
if (code == null || !code.equals(sessionCode)) {
return Result.fail("驗(yàn)證碼不正確");
}
// 3、判斷手機(jī)號(hào)是否是已存在的用戶
User user = this.getOne(new LambdaQueryWrapper<User>()
.eq(User::getPassword, phone));
if (Objects.isNull(user)) {
// 用戶不存在,需要注冊(cè)
user = createUserWithPhone(phone);
}
// 4、保存用戶信息到Session中,便于后面邏輯的判斷(比如登錄判斷、隨時(shí)取用戶信息,減少對(duì)數(shù)據(jù)庫(kù)的查詢)
session.setAttribute(LOGIN_USER, user);
return Result.ok();
}
/**
* 根據(jù)手機(jī)號(hào)創(chuàng)建用戶
*/
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
this.save(user);
return user;
}2 配置登錄攔截器

public class LoginInterceptor implements HandlerInterceptor {
/**
* 前置攔截器,用于判斷用戶是否登錄
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
// 1、判斷用戶是否存在
User user = (User) session.getAttribute(LOGIN_USER);
if (Objects.isNull(user)){
// 用戶不存在,直接攔截
response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
return false;
}
// 2、用戶存在,則將用戶信息保存到ThreadLocal中,方便后續(xù)邏輯處理
// 比如:方便獲取和使用用戶信息,session獲取用戶信息是具有侵入性的
ThreadLocalUtls.saveUser(user);
return HandlerInterceptor.super.preHandle(request, response, handler);
}
}3 配置完攔截器還需將自定義攔截器添加到SpringMVC的攔截器列表中 才能生效
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加登錄攔截器
registry.addInterceptor(new LoginInterceptor())
// 設(shè)置放行請(qǐng)求
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
);
}
}
4 Session集群共享問(wèn)題
(1)什么是Session集群共享問(wèn)題?
在分布式集群環(huán)境中,會(huì)話(Session)共享是一個(gè)常見的挑戰(zhàn)。默認(rèn)情況下,Web 應(yīng)用程序的會(huì)話是保存在單個(gè)服務(wù)器上的,當(dāng)請(qǐng)求不經(jīng)過(guò)該服務(wù)器時(shí),會(huì)話信息無(wú)法被訪問(wèn)。
(2) Session集群共享問(wèn)題造成哪些問(wèn)題?
服務(wù)器之間無(wú)法實(shí)現(xiàn)會(huì)話狀態(tài)的共享。比如:在當(dāng)前這個(gè)服務(wù)器上用戶已經(jīng)完成了登錄,Session中存儲(chǔ)了用戶的信息,能夠判斷用戶已登錄,但是在另一個(gè)服務(wù)器的Session中沒有用戶信息,無(wú)法調(diào)用顯示沒有登錄的服務(wù)器上的服務(wù)
(3)如何解決Session集群共享問(wèn)題?
方案一:Session拷貝(不推薦)
Tomcat提供了Session拷貝功能,通過(guò)配置Tomcat可以實(shí)現(xiàn)Session的拷貝,但是這會(huì)增加服務(wù)器的額外內(nèi)存開銷,同時(shí)會(huì)帶來(lái)數(shù)據(jù)一致性問(wèn)題
方案二:Redis緩存(推薦)
Redis緩存具有Session存儲(chǔ)一樣的特點(diǎn),基于內(nèi)存、存儲(chǔ)結(jié)構(gòu)可以是key-value結(jié)構(gòu)、數(shù)據(jù)共享
(4)Redis緩存相較于傳統(tǒng)Session存儲(chǔ)的優(yōu)點(diǎn)
1 高性能和可伸縮性:Redis 是一個(gè)內(nèi)存數(shù)據(jù)庫(kù),具有快速的讀寫能力。相比于傳統(tǒng)的 Session 存儲(chǔ)方式,將會(huì)話數(shù)據(jù)存儲(chǔ)在 Redis 中可以大大提高讀寫速度和處理能力。此外,Redis 還支持集群和分片技術(shù),可以實(shí)現(xiàn)水平擴(kuò)展,處理大規(guī)模的并發(fā)請(qǐng)求。
2 可靠性和持久性:Redis 提供了持久化機(jī)制,可以將內(nèi)存中的數(shù)據(jù)定期或異步地寫入磁盤,以保證數(shù)據(jù)的持久性。這樣即使發(fā)生服務(wù)器崩潰或重啟,會(huì)話數(shù)據(jù)也可以被恢復(fù)。
3 豐富的數(shù)據(jù)結(jié)構(gòu):Redis 不僅僅是一個(gè)鍵值存儲(chǔ)數(shù)據(jù)庫(kù),它還支持多種數(shù)據(jù)結(jié)構(gòu),如字符串、列表、哈希、集合和有序集合等。這些數(shù)據(jù)結(jié)構(gòu)的靈活性使得可以更方便地存儲(chǔ)和操作復(fù)雜的會(huì)話數(shù)據(jù)。
4 分布式緩存功能:Redis 作為一個(gè)高效的緩存解決方案,可以用于緩存會(huì)話數(shù)據(jù),減輕后端服務(wù)器的負(fù)載。與傳統(tǒng)的 Session 存儲(chǔ)方式相比,使用 Redis 緩存會(huì)話數(shù)據(jù)可以大幅提高系統(tǒng)的性能和可擴(kuò)展性。
5 可用性和可部署性:Redis 是一個(gè)強(qiáng)大而成熟的開源工具,有豐富的社區(qū)支持和活躍的開發(fā)者社區(qū)。它可以輕松地與各種編程語(yǔ)言和框架集成,并且可以在多個(gè)操作系統(tǒng)上運(yùn)行。
PS:但是Redis費(fèi)錢,而且增加了系統(tǒng)的復(fù)雜度
5 基于Redis實(shí)現(xiàn)短信驗(yàn)證碼登錄

6 Hash 結(jié)構(gòu)與 String 結(jié)構(gòu)類型的比較
- String 數(shù)據(jù)結(jié)構(gòu)是以 JSON 字符串的形式保存,更加直觀,操作也更加簡(jiǎn)單,但是 JSON 結(jié)構(gòu)會(huì)有很多非必須的內(nèi)存開銷,比如雙引號(hào)、大括號(hào),內(nèi)存占用比 Hash 更高
- Hash 數(shù)據(jù)結(jié)構(gòu)是以 Hash 表的形式保存,可以對(duì)單個(gè)字段進(jìn)行CRUD,更加靈活
7 Redis替代Session需要考慮的問(wèn)題
(1)選擇合適的數(shù)據(jù)結(jié)構(gòu),了解 Hash 比 String 的區(qū)別
(2)選擇合適的key,為key設(shè)置一個(gè)業(yè)務(wù)前綴,方便區(qū)分和分組,為key拼接一個(gè)UUID,避免key沖突防止數(shù)據(jù)覆蓋
(3)選擇合適的存儲(chǔ)粒度,對(duì)于驗(yàn)證碼這類數(shù)據(jù),一般設(shè)置TTL為3min即可,防止大量緩存數(shù)據(jù)的堆積,而對(duì)于用戶信息這類數(shù)據(jù)可以稍微設(shè)置長(zhǎng)一點(diǎn),比如30min,防止頻繁對(duì)Redis進(jìn)行IO操作
8 基于redis短信驗(yàn)證登錄
/**
* 發(fā)送驗(yàn)證碼
*
* @param phone
* @param session
* @return
*/
@Override
public Result sendCode(String phone, HttpSession session) {
// 1、判斷手機(jī)號(hào)是否合法
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手機(jī)號(hào)格式不正確");
}
// 2、手機(jī)號(hào)合法,生成驗(yàn)證碼,并保存到Redis中
String code = RandomUtil.randomNumbers(6);
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code,
RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 3、發(fā)送驗(yàn)證碼
log.info("驗(yàn)證碼:{}", code);
return Result.ok();
}
/**
* 用戶登錄
*
* @param loginForm
* @param session
* @return
*/
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
String code = loginForm.getCode();
// 1、判斷手機(jī)號(hào)是否合法
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手機(jī)號(hào)格式不正確");
}
// 2、判斷驗(yàn)證碼是否正確
String redisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
if (code == null || !code.equals(redisCode)) {
return Result.fail("驗(yàn)證碼不正確");
}
// 3、判斷手機(jī)號(hào)是否是已存在的用戶
User user = this.getOne(new LambdaQueryWrapper<User>()
.eq(User::getPhone, phone));
if (Objects.isNull(user)) {
// 用戶不存在,需要注冊(cè)
user = createUserWithPhone(phone);
}
// 4、保存用戶信息到Redis中,便于后面邏輯的判斷(比如登錄判斷、隨時(shí)取用戶信息,減少對(duì)數(shù)據(jù)庫(kù)的查詢)
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 將對(duì)象中字段全部轉(zhuǎn)成string類型,StringRedisTemplate只能存字符串類型的數(shù)據(jù)
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create().setIgnoreNullValue(true).
setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
String token = UUID.randomUUID().toString(true);
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
return Result.ok(token);
}
/**
* 根據(jù)手機(jī)號(hào)創(chuàng)建用戶并保存
*
* @param phone
* @return
*/
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
this.save(user);
return user;
}9 配置登錄攔截器

單獨(dú)配置一個(gè)攔截器用戶刷新Redis中的token:在基于Session實(shí)現(xiàn)短信驗(yàn)證碼登錄時(shí),我們只配置了一個(gè)攔截器,這里需要另外再配置一個(gè)攔截器專門用于刷新存入Redis中的 token,因?yàn)槲覀儸F(xiàn)在改用Redis了,為了防止用戶在操作網(wǎng)站時(shí)突然由于Redis中的 token 過(guò)期,導(dǎo)致直接退出網(wǎng)站,嚴(yán)重影響用戶體驗(yàn)。那為什么不把刷新的操作放到一個(gè)攔截器中呢,因?yàn)橹暗哪莻€(gè)攔截器只是用來(lái)攔截一些需要進(jìn)行登錄校驗(yàn)的請(qǐng)求,對(duì)于哪些不需要登錄校驗(yàn)的請(qǐng)求是不會(huì)走攔截器的,刷新操作顯然是要針對(duì)所有請(qǐng)求比較合理,所以單獨(dú)創(chuàng)建一個(gè)攔截器攔截一切請(qǐng)求,刷新Redis中的Key
登錄攔截器:
public class LoginInterceptor implements HandlerInterceptor {
/**
* 前置攔截器,用于判斷用戶是否登錄
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判斷當(dāng)前用戶是否已登錄
if (ThreadLocalUtls.getUser() == null){
// 當(dāng)前用戶未登錄,直接攔截
response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
return false;
}
// 用戶存在,直接放行
return true;
}
}刷新token的攔截器
public class RefreshTokenInterceptor implements HandlerInterceptor {
// new出來(lái)的對(duì)象是無(wú)法直接注入IOC容器的(LoginInterceptor是直接new出來(lái)的)
// 所以這里需要再配置類中注入,然后通過(guò)構(gòu)造器傳入到當(dāng)前類中
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1、獲取token,并判斷token是否存在
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)){
// token不存在,說(shuō)明當(dāng)前用戶未登錄,不需要刷新直接放行
return true;
}
// 2、判斷用戶是否存在
String tokenKey = LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
if (userMap.isEmpty()){
// 用戶不存在,說(shuō)明當(dāng)前用戶未登錄,不需要刷新直接放行
return true;
}
// 3、用戶存在,則將用戶信息保存到ThreadLocal中,方便后續(xù)邏輯處理,比如:方便獲取和使用用戶信息,Redis獲取用戶信息是具有侵入性的
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
ThreadLocalUtls.saveUser(BeanUtil.copyProperties(userMap, UserDTO.class));
// 4、刷新token有效期
stringRedisTemplate.expire(token, LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
}將自定義的攔截器添加到SpringMVC的攔截器表中,使其生效:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
// new出來(lái)的對(duì)象是無(wú)法直接注入IOC容器的(LoginInterceptor是直接new出來(lái)的)
// 所以這里需要再配置類中注入,然后通過(guò)構(gòu)造器傳入到當(dāng)前類中
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加登錄攔截器
registry.addInterceptor(new LoginInterceptor())
// 設(shè)置放行請(qǐng)求
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
).order(1); // 優(yōu)先級(jí)默認(rèn)都是0,值越大優(yōu)先級(jí)越低
// 添加刷新token的攔截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}RefreshTokenInterceptor先執(zhí)行,主要用來(lái)檢查 token 的有效性并刷新 token 的有效期,同時(shí)將用戶信息存入ThreadLocal。LoginInterceptor后執(zhí)行,驗(yàn)證ThreadLocal中是否有用戶信息,以確認(rèn)用戶是否登錄。
以上就是基于Redis實(shí)現(xiàn)短信驗(yàn)證碼登錄功能的詳細(xì)內(nèi)容,更多關(guān)于Redis短信驗(yàn)證碼登錄的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
redis實(shí)現(xiàn)分布式延時(shí)隊(duì)列的示例代碼
延時(shí)隊(duì)列是一種特殊的消息隊(duì)列,它允許將消息在一定的延遲時(shí)間后再進(jìn)行消費(fèi),延時(shí)隊(duì)列的實(shí)現(xiàn)方式可以有多種,本文主要來(lái)介紹一種redis實(shí)現(xiàn)的分布式延時(shí)隊(duì)列,希望對(duì)大家有所幫助2023-10-10
Redis實(shí)現(xiàn)信息已讀未讀狀態(tài)提示
這篇文章主要介紹了Redis實(shí)現(xiàn)信息已讀未讀狀態(tài)提示的相關(guān)資料,需要的朋友可以參考下2016-04-04
Redis與自定義注解實(shí)現(xiàn)重復(fù)方式
文章介紹了如何使用Redis和自定義注解`SubmitLock`來(lái)防止重復(fù)提交,首先,創(chuàng)建了一個(gè)注解`SubmitLock`,然后在需要防止重復(fù)提交的方法上使用該注解,最后,通過(guò)一個(gè)controller測(cè)試驗(yàn)證了該功能的有效性2025-12-12
在Redis中存儲(chǔ)和操作JSON數(shù)據(jù)的方法詳解
RedisJSON 是 Redis 的一個(gè)模塊,為 Redis 提供了 JSON 數(shù)據(jù)類型的支持,允許直接存儲(chǔ)、更新和查詢 JSON 文檔,本文給大家詳細(xì)介紹了RedisJSON 安裝部署及使用示例,需要的朋友可以參考下2025-07-07
Redis消息隊(duì)列實(shí)現(xiàn)異步秒殺功能
在高并發(fā)場(chǎng)景下,為了提高秒殺業(yè)務(wù)的性能,可將部分工作交給 Redis 處理,并通過(guò)異步方式執(zhí)行,Redis 提供了多種數(shù)據(jù)結(jié)構(gòu)來(lái)實(shí)現(xiàn)消息隊(duì)列,總結(jié)三種,本文詳細(xì)介紹Redis消息隊(duì)列實(shí)現(xiàn)異步秒殺功能,感興趣的朋友一起看看吧2025-04-04
SpringSession通過(guò)Redis統(tǒng)計(jì)在線用戶數(shù)量的實(shí)現(xiàn)代碼
這篇文章主要介紹了SpringSession通過(guò)Redis統(tǒng)計(jì)在線用戶數(shù)量,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-04-04
Redis 如何批量設(shè)置過(guò)期時(shí)間(PIPLINE的使用)
有時(shí)候我們并不希望redis的key一直存在。例如緩存,驗(yàn)證碼等數(shù)據(jù),我們希望它們能在一定時(shí)間內(nèi)自動(dòng)的被銷毀。本文就詳細(xì)的介紹一下Redis 如何批量設(shè)置過(guò)期時(shí)間,感興趣的可以了解一下2021-11-11

