SpringBoot基于Redis實(shí)現(xiàn)短信登錄的操作
前言
使用 Redis 進(jìn)行登錄適用于以下情況:
- 分布式系統(tǒng):
當(dāng)系統(tǒng)需要支持多個(gè)節(jié)點(diǎn)的分布式部署時(shí),使用 Redis 存儲(chǔ)登錄信息能夠更好地支持多節(jié)點(diǎn)間的共享和同步,確保用戶的登錄狀態(tài)能夠在整個(gè)系統(tǒng)中得到有效的傳遞和管理。 - 高并發(fā)訪問:
面對(duì)大規(guī)模的并發(fā)訪問,使用 Redis 可以提供更好的性能表現(xiàn)。Redis 是一個(gè)基于內(nèi)存的高性能 Key-Value 數(shù)據(jù)庫(kù),能夠更快速地讀取和寫入數(shù)據(jù),因此適用于需要處理大量并發(fā)請(qǐng)求的場(chǎng)景。 - 靈活的數(shù)據(jù)結(jié)構(gòu)需求:
如果系統(tǒng)需要根據(jù)業(yè)務(wù)需求選擇最佳的數(shù)據(jù)結(jié)構(gòu),并且對(duì)存儲(chǔ)和操作登錄信息有更多的靈活性,那么使用 Redis 將會(huì)是一個(gè)不錯(cuò)的選擇。Redis 支持多種數(shù)據(jù)類型的存儲(chǔ)和操作,包括字符串、哈希表、列表、集合和有序集合等,能夠滿足不同的業(yè)務(wù)需求。 - 需要持久化支持:
如果系統(tǒng)需要對(duì)登錄信息進(jìn)行持久化存儲(chǔ),以防止數(shù)據(jù)丟失,Redis 的持久化功能可以很好地滿足這一需求。
總的來說,使用 Redis 進(jìn)行登錄適用于需要支持分布式部署、面對(duì)高并發(fā)訪問、有靈活的數(shù)據(jù)結(jié)構(gòu)需求以及需要持久化支持的系統(tǒng)場(chǎng)景。通過合理地利用 Redis 的特性,可以更好地滿足上述情況下的需求,提高系統(tǒng)的可擴(kuò)展性、性能和穩(wěn)定性。
雖然 Spring Boot 應(yīng)用通常是單體應(yīng)用,但是在實(shí)際運(yùn)行中,我們也經(jīng)常會(huì)遇到多個(gè)實(shí)例同時(shí)運(yùn)行的情況,這時(shí)候就需要使用 Redis 進(jìn)行分布式 Session 管理。
StringRedisTemplate
StringRedisTemplate是Spring Data Redis提供的一個(gè)類,它是一個(gè)具體的對(duì)象,用于操作Redis數(shù)據(jù)庫(kù)中的字符串類型數(shù)據(jù)。
StringRedisTemplate封裝了Redis的操作,并提供了一系列方法來對(duì)Redis中的字符串進(jìn)行讀取、寫入和刪除操作。它是RedisTemplate的一個(gè)子類,專門用于處理字符串類型的數(shù)據(jù)。
??使用StringRedisTemplate
首先引入依賴,引入StringRedisTemplate的依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
?常用的方法
StringRedisTemplate提供了多個(gè)方法來操作Redis中的字符串類型數(shù)據(jù)。下面是一些常用的方法:
opsForValue().set(key, value):將一個(gè)字符串類型的值value存儲(chǔ)到Redis中,并指定鍵key。
- opsForValue().get(key):根據(jù)鍵key獲取對(duì)應(yīng)的字符串類型的值。
- opsForValue().increment(key, delta):將鍵key所對(duì)應(yīng)的值增加delta,delta可以為負(fù)數(shù)。
- opsForValue().size(key):獲取值的長(zhǎng)度。
為什么我們要使用Redis代替Session進(jìn)行登錄操作
集群session存在共享問題,會(huì)導(dǎo)致數(shù)據(jù)丟失
- 保存相同的數(shù)據(jù),大家互相copy,會(huì)有內(nèi)存空間的浪費(fèi)
- 我們copy數(shù)據(jù)的時(shí)候,是需要有一定的時(shí)間的,會(huì)有延遲,如果在這個(gè)延遲之內(nèi),如果有人來訪問,仍然會(huì)造成數(shù)據(jù)不一致的情況
如果我們使用Redis的話。 - Redis是在tomcat外面的存儲(chǔ),如果任意一臺(tái)tomcat都能訪問到Redis,可以實(shí)現(xiàn)
數(shù)據(jù)共享
,儲(chǔ)存在Redis里面的數(shù)據(jù),任何tomcat都可以看到,使用就不存在數(shù)據(jù)丟失的問題 - Redis讀寫延遲非常低,方便進(jìn)行內(nèi)存存儲(chǔ)
- Redis是key-value結(jié)構(gòu)
??具體使用
?編寫攔截器
RefreshTokenInterceptor.java
在攔截器中配置攔截操作
package com.hmdp.utils; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.util.StrUtil; import com.hmdp.dto.UserDTO; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Map; import java.util.concurrent.TimeUnit; import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY; import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL; 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_USER_KEY + 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(); } }
刷新token的目的
用戶每訪問一次,這個(gè)token就會(huì)刷新一次,主要用戶一直在操作,這個(gè)token就不會(huì)消失
但是如果僅僅攔截的是需要登錄的路徑,用戶 訪問 不需要登錄 的路徑 的時(shí)候(比如首頁),這個(gè)攔截器就不生效,此時(shí)token就不會(huì)刷新,這樣子,過了token的有效期后,盡管用戶還在訪問,用戶的登錄狀態(tài)卻消失了,這樣肯定不太合理
那么我們就需要在原來的攔截器基礎(chǔ)上再加上一個(gè)攔截器
LoginInterceptor.java
在攔截器中配置攔截操作
package com.hmdp.utils; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.判斷是否需要攔截(ThreadLocal中是否有用戶) if (UserHolder.getUser() == null) { // 沒有,需要攔截,設(shè)置狀態(tài)碼 response.setStatus(401); // 攔截 return false; } // 有用戶,則放行 return true; } }
?配置攔截器
我們上面編寫了攔截器,我們還需要配置攔截器,使這個(gè)攔截器生效
MvcConfig.java
package com.hmdp.config; import com.hmdp.utils.LoginInterceptor; import com.hmdp.utils.RefreshTokenInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.annotation.Resource; @Configuration public class MvcConfig 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); // token刷新的攔截器 registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0); } }
??基于Redis實(shí)現(xiàn)發(fā)送手機(jī)驗(yàn)證碼操作
??總體思路
??具體步驟
我們首先引入上面說的依賴,然后在application.yml文件(或yaml文件)中進(jìn)行配置
,如下
下面我們編寫發(fā)送手機(jī)驗(yàn)證碼的核心代碼
@Slf4j @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result sendCode(String phone, HttpSession session) { // 1.校驗(yàn)手機(jī)號(hào) if (RegexUtils.isPhoneInvalid(phone)) { // 2.如果不符合,返回錯(cuò)誤信息 return Result.fail("手機(jī)號(hào)格式錯(cuò)誤!"); } // 3.符合,生成驗(yàn)證碼 String code = RandomUtil.randomNumbers(6); // 4.保存驗(yàn)證碼到 redis stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES); // 5.發(fā)送驗(yàn)證碼 log.debug("發(fā)送短信驗(yàn)證碼成功,驗(yàn)證碼:{}", code); // 返回ok return Result.ok(); } }
上面代碼里面的RegexUtils.isPhoneInvalid(phone)這段代碼是什么用法
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);這段代碼有什么用
這段代碼的作用是將一個(gè)驗(yàn)證碼(即code)存儲(chǔ)到Redis中
,并設(shè)置了過期時(shí)間為L(zhǎng)OGIN_CODE_TTL分鐘。
以便在一定時(shí)間后自動(dòng)刪除該鍵值對(duì)。
??基于Redis實(shí)現(xiàn)短信登錄并注冊(cè)的操作
??總體思路
??具體步驟
我們首先引入上面說的依賴,并且在application.yml文件(或yaml文件)中進(jìn)行配置(同上)
然后我們來編寫核心代碼
@Slf4j @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result login(LoginFormDTO loginForm, HttpSession session) { // 1.校驗(yàn)手機(jī)號(hào) String phone = loginForm.getPhone(); if (RegexUtils.isPhoneInvalid(phone)) { // 2.如果不符合,返回錯(cuò)誤信息 return Result.fail("手機(jī)號(hào)格式錯(cuò)誤!"); } // 3.從redis獲取驗(yàn)證碼并校驗(yàn) String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone); String code = loginForm.getCode(); if (cacheCode == null || !cacheCode.equals(code)) { // 不一致,報(bào)錯(cuò) return Result.fail("驗(yàn)證碼錯(cuò)誤"); } // 4.一致,根據(jù)手機(jī)號(hào)查詢用戶 select * from tb_user where phone = ? User user = query().eq("phone", phone).one(); // 5.判斷用戶是否存在 if (user == null) { // 6.不存在,創(chuàng)建新用戶并保存 user = createUserWithPhone(phone); } // 7.保存用戶信息到 redis中 // 7.1.隨機(jī)生成token,作為登錄令牌 String token = UUID.randomUUID().toString(true); // 7.2.將User對(duì)象轉(zhuǎn)為HashMap存儲(chǔ) UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create() .setIgnoreNullValue(true) .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); // 7.3.存儲(chǔ) String tokenKey = LOGIN_USER_KEY + token; stringRedisTemplate.opsForHash().putAll(tokenKey, userMap); // 7.4.設(shè)置token有效期 stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES); // 8.返回token return Result.ok(token); } private User createUserWithPhone(String phone) { // 1.創(chuàng)建用戶 User user = new User(); user.setPhone(phone); user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)); // 2.保存用戶 save(user); return user; } }
User user = query().eq(“phone”, phone).one();這段代碼使用了mybatisplus,相當(dāng)于select * from tb_user where phone = ?
為什么要使用HashMap進(jìn)行存儲(chǔ)
在這段代碼中,使用HashMap進(jìn)行存儲(chǔ)是為了將用戶對(duì)象轉(zhuǎn)換成鍵值對(duì)形式,便于統(tǒng)一保存到Redis中,并且可以方便地進(jìn)行序列化和反序列化操作。具體來說:
- 便于存儲(chǔ)和讀?。簩⒂脩魧?duì)象轉(zhuǎn)為HashMap后,可以方便地通過stringRedisTemplate.opsForHash().putAll()方法一次性將整個(gè)用戶對(duì)象存儲(chǔ)到Redis的Hash數(shù)據(jù)結(jié)構(gòu)中,而不需要對(duì)用戶對(duì)象的每個(gè)字段分別進(jìn)行存儲(chǔ)。
- 數(shù)據(jù)結(jié)構(gòu)清晰:使用HashMap可以清晰地表示用戶對(duì)象的各個(gè)字段和對(duì)應(yīng)的數(shù)值,便于管理和維護(hù)。
- 方便序列化和反序列化:HashMap作為Java中的常用數(shù)據(jù)結(jié)構(gòu),可以方便地進(jìn)行
序列化(將數(shù)據(jù)轉(zhuǎn)換為字節(jié)序列)
和反序列化(將字節(jié)序列轉(zhuǎn)換為數(shù)據(jù))
操作,便于在存儲(chǔ)到Redis或者從Redis中讀取時(shí)進(jìn)行數(shù)據(jù)格式的轉(zhuǎn)換。
總之,使用HashMap進(jìn)行存儲(chǔ)能夠簡(jiǎn)化代碼邏輯,提高數(shù)據(jù)存儲(chǔ)和讀取的效率,并且方便進(jìn)行數(shù)據(jù)結(jié)構(gòu)的轉(zhuǎn)換和管理。
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
這段代碼為什么要這樣寫,這些參數(shù)有什么用
其中,beanToMap是一個(gè)方法,用于將Java對(duì)象(Bean)轉(zhuǎn)換為Map類型的數(shù)據(jù)結(jié)構(gòu)。
在這段代碼中,BeanUtil.beanToMap()方法被使用,它是一個(gè)工具類方法,可以通過反射機(jī)制將Java對(duì)象的屬性和對(duì)應(yīng)的值轉(zhuǎn)換為鍵值對(duì)形式,并存儲(chǔ)到一個(gè)Map對(duì)象中。
具體來說,beanToMap方法接收三個(gè)參數(shù):
userDTO:表示要轉(zhuǎn)換的源對(duì)象,即需要將其轉(zhuǎn)換為Map的對(duì)象。
new HashMap<>():表示用于存儲(chǔ)轉(zhuǎn)換結(jié)果的目標(biāo)HashMap對(duì)象,這里使用了一個(gè)新的空HashMap,用于接收轉(zhuǎn)換后的鍵值對(duì)數(shù)據(jù)。
CopyOptions.create().setIgnoreNullValue(true):這是使用BeanUtil進(jìn)行對(duì)象轉(zhuǎn)換時(shí)的配置選項(xiàng)。setIgnoreNullValue(true)表示
忽略源對(duì)象中值為null的屬性
,不將其放入目標(biāo)Map中。.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()):這個(gè)配置項(xiàng)表示對(duì)轉(zhuǎn)換過程中的字段值進(jìn)行編輯處理。在這里,它的作用是將字段值轉(zhuǎn)換為字符串類型,確保最終存儲(chǔ)到Map中的值都是字符串類型。
綜合起來,這段代碼的目的是將UserDTO對(duì)象轉(zhuǎn)換為Map類型,同時(shí)忽略空值屬性,并確保所有屬性值都被轉(zhuǎn)換為字符串類型
。這樣做的原因可能是為了在存儲(chǔ)到Redis中時(shí),確保數(shù)據(jù)的統(tǒng)一性和一致性
,便于后續(xù)從Redis中讀取并進(jìn)行處理。
到此這篇關(guān)于SpringBoot基于Redis實(shí)現(xiàn)短信登錄的操作的文章就介紹到這了,更多相關(guān)SpringBoot Redis短信登錄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java同步器AQS架構(gòu)AbstractQueuedSynchronizer原理解析下
這篇文章主要為大家介紹了java同步器AQS架構(gòu)AbstractQueuedSynchronizer原理解析下,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-03-03Java 使用openoffice進(jìn)行word轉(zhuǎn)換為pdf的方法步驟
這篇文章主要介紹了Java 使用openoffice進(jìn)行word轉(zhuǎn)換為pdf的方法步驟,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04Java反射(JDK)與動(dòng)態(tài)代理(CGLIB)詳解
下面小編就為大家?guī)硪黄獪\談Java反射與動(dòng)態(tài)代理。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2021-08-08使用java采集京東商城區(qū)劃數(shù)據(jù)示例
這篇文章主要介紹了java采集京東的全國(guó)區(qū)劃數(shù)據(jù)示例,保存成json形式,如想轉(zhuǎn)換到數(shù)據(jù)庫(kù)只需反序列化為對(duì)象保存到數(shù)據(jù)庫(kù)即可2014-03-03Spring Boot實(shí)現(xiàn)通用的接口參數(shù)校驗(yàn)
本文介紹基于 Spring Boot 和 JDK8 編寫一個(gè) AOP ,結(jié)合自定義注解實(shí)現(xiàn)通用的接口參數(shù)校驗(yàn)。具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-05-05詳解MyBatis-Plus updateById方法更新不了空字符串/null解決方法
這篇文章主要介紹了詳解MyBatis-Plus updateById方法更新不了空字符串/null解決方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09