【項目實戰】基于Redis實現短信驗證碼登錄 (附源碼、思路)

在這里插入圖片描述

??各位小伙伴們大家好,歡迎來到這個小扎扎的Redis 6專欄,在這個系列專欄中我對B站黑馬的Redis教程進行一個總結,鑒于 看到就是學到、學到就是賺到 精神,這波依然是血賺 ┗|`O′|┛

💡Redis知識點速覽

  • 🍖 Redis短信登錄流程描述
    • 🥩 短信驗證碼的發送
    • 🥩 短信驗證碼的驗證
    • 🥩 是否登錄的驗證
  • 🍖 源碼分析
    • 🥩 模擬發送短信驗證碼
    • 🥩 短信驗證碼的驗證
    • 🥩 校驗是否登錄
    • 🥩 登錄驗證優化

🍖 Redis短信登錄流程描述

🥩 短信驗證碼的發送

??用戶提交手機號,系統驗證手機號是否有效,畢竟無效手機號會消耗你的短信驗證次數還會導致系統的性能下降。如果手機號為無效的話就讓用戶重新提交手機號,如果有效就生成驗證碼并將該驗證碼作為value保存到redis中對應的key是手機號,之所以這么做的原因是保證key的唯一性,如果使用固定字符串作為可以的話會被后面的數據所覆蓋。然后在控制臺輸出驗證碼模擬發送驗證碼的過程

🥩 短信驗證碼的驗證

??用戶的手機號接收到驗證碼后在平臺上提交驗證碼,系統從redis中根據手機號讀取驗證碼并進行校驗,如果驗證通過的話就根據用戶驗證使用的手機號去數據庫中進行查詢用戶信息。如果存在就將查詢到的用戶信息保存到redis中,完成登錄;如果不存在的話就創建一個新用戶,并將該用戶的信息分別保存到sql數據庫和redis中,生成隨機token作為key、使用hash結構存儲user數據作為value,并將這個token返回給客戶端,至此完成登錄注冊

🥩 是否登錄的驗證

??用戶訪問系統業務邏輯的時候需要校驗他是否已經登錄,如果登錄可以訪問否則就去登錄,那么該如何完成是否登錄的校驗呢?這就要了解session的相關知識了,每一個session都有一個sessionId信息保存在瀏覽器的cookie中,當用戶使用瀏覽器發送請求的時候會攜帶上cookie信息,此時系統就可以使用cookie中的sessionId獲取到session信息,并通過session獲取到登錄時存儲的用戶信息。如果此時用戶在數據庫中存在的話就將該用戶的信息緩存在ThreadLocal(方便后續驗證)中,并放行該訪問;否則就說明發送請求的用戶未登錄或不合法,就要攔截到他的請求前往登錄

🍖 源碼分析

🥩 模擬發送短信驗證碼

UserController定義與前端交互

@Resource
private IUserService userService;/*** 發送手機驗證碼*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {// 發送短信驗證碼并保存驗證碼return userService.sendCode(phone, session);
}

上面使用到了sendCode方法,在userService里定義一下接口,然后在對應實現類中按照上面的流程重寫該方法的業務邏輯代碼

@Override
public Result sendCode(String phone, HttpSession session) {// 校驗手機號if (RegexUtils.isPhoneInvalid(phone)) {// 無效手機號,返回錯誤信息return Result.fail("手機號格式有誤!");}// 有效生成驗證碼String code = RandomUtil.randomNumbers(6);// 保存 (固定前綴+手機號) 和驗證碼到Redis中,設置驗證碼的有效期為2分鐘// RedisConstants.LOGIN_CODE_KEY = “login:code:”// RedisConstants.LOGIN_CODE_TTL = 2LstringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);// 模擬發送驗證碼log.debug("驗證碼:{}", code);// 返回return Result.ok();
}

手機號格式校驗使用到的RegexUtils類中的工具方法

/*** 手機號正則*/
public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";/**
* 是否是無效手機格式* @param phone 要校驗的手機號* @return true:符合,false:不符合*/
public static boolean isPhoneInvalid(String phone){return mismatch(phone, RegexPatterns.PHONE_REGEX);
}// 校驗是否不符合正則格式
private static boolean mismatch(String str, String regex){if (StrUtil.isBlank(str)) {return true;}return !str.matches(regex);
}

🥩 短信驗證碼的驗證

??UserController定義與前端交互,其中參數LoginFormDTO 是前端使用手機號+驗證碼登錄或者手機號+密碼登錄是傳遞過來的JSON數據

/*** 登錄功能* @param loginForm 登錄參數,包含手機號、驗證碼;或者手機號、密碼*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){// 實現登錄功能return userService.login(loginForm, session);
}

??上面使用到了login方法,在userService里定義一下接口,然后在對應實現類中按照上賣弄的流程描述重寫該方法的業務邏輯代碼

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {String phone = loginForm.getPhone();// 驗證碼校驗String code = loginForm.getCode();String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);if (cacheCode == null || !code.equals(cacheCode)) {return Result.fail("驗證碼錯誤!");}// 根據手機號查詢用戶信息User user = query().eq("phone", phone).one();if (user == null) {// 不存在就創建一個新用戶user = createUserWithPhone(phone);}// 保存用戶信息到redis中// 生成隨機tokenString token = UUID.randomUUID().toString(true);// user先轉userDTO再轉hashMap存儲  轉HashMap時的第三個參數的意思是忽略null值將值都轉換成String類型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()));// RedisConstants.LOGIN_USER_KEY = "login:token:"stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap);// 設置失效時間為30分鐘// RedisConstants.LOGIN_USER_TTL = 30LstringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);// 返回前端tokenreturn Result.ok(token);
}private User createUserWithPhone(String phone) {User user = new User();user.setPhone(phone);// SystemConstants.USER_NICK_NAME_PREFIX = "user_"user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));save(user);return user;
}

??保存的時候使用BeanUtil將User轉換成UserDTO進行存儲,UserDTO的結構如下,只保存一部分的數據,一方面可以不用來回傳遞用戶有關的隱私數據,一方面也節省內存提高性能。由于這里的id是數值類型,但是stringRedisTemplate存儲時需要hash的鍵值都是String型,所以說應該在存儲之前將id的值轉換成String類型,就在上面代碼塊的24~27行完成了這個操作

@Data
public class UserDTO {private Long id;private String nickName;private String icon;
}

🥩 校驗是否登錄

用戶發送請求不止一次,所以說登錄驗證也不止進行一次,于是可以使用攔截器完成驗證,攔截器的使用可分為兩步:
創建攔截器

/*** @author : mereign* @date : 2022/5/5 - 10:31* @desc : 攔截器,實現請求攔截,判斷登錄信息*/
@Component
public class LoginInterceptor implements HandlerInterceptor {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 獲取請求頭中的token信息String token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {// token為空,返回401未授權狀態碼,攔截response.setStatus(401);return false;}// 根據token獲取redis中的用戶valueMap<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);HttpSession session = request.getSession();// 判斷用戶是否存在if (userMap.isEmpty()) {// 用戶不存在,返回401未授權狀態碼,攔截response.setStatus(401);return false;}// 用戶存在,將hash數據轉換為userDTO,存信息到ThreadLocalUserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);UserHolder.saveUser(userDTO);// 刷新token有效期,放行stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}

注冊攔截器

/*** @author : mereign* @date : 2022/5/5 - 10:43* @desc :*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).excludePathPatterns("/shop/**","/shop-type/**","/voucher/**","/upload/**","/blog/hot","/user/code","/user/login");}
}

緩存用戶的信息到ThreadLocal中的工具方法

public class UserHolder {private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();public static void saveUser(UserDTO user){tl.set(user);}public static UserDTO getUser(){return tl.get();}public static void removeUser(){tl.remove();}
}

UserController定義與前端交互

@GetMapping("/me")
public Result me(){// 獲取當前登錄的用戶并返回UserDTO user = UserHolder.getUser();return Result.ok(user);
}

🥩 登錄驗證優化

??由上面的登錄驗證可知,我們對一些需要用戶登錄驗證的功能設置了攔截器,如果驗證通過會刷新token的有效期,這樣的話只要用戶一直訪問我們攔截的功能就可以一直保持token是有效的。但是,如果用戶登陸之后的操作一直是不需要驗證的,那也就意味著token的有效期一直不會刷新,這樣的話30分鐘之后token就會失效用戶驗證就會失敗,這樣顯然是不合理的
??于是我們可以使用兩個攔截器完成,最前面的負責攔截所有的請求,獲取token、從redis中查詢用戶,將查詢結果放到ThreadLocal(可能存null)、刷新token有效期,最后直接放行;后面的攔截器只負責判斷有沒有從redis中查詢到用戶,他從ThreadLocal獲取查詢結果,判斷有則放行無則攔截

創建兩個攔截器

/*** @author : mereign* @date : 2022/5/5 - 10:31* @desc : 前置攔截器,攔截所有請求,前置工作*/
@Component
public class RefreshTokenInterceptor implements HandlerInterceptor {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 獲取請求頭中的token信息String token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {// token為空 直接放行return true;}// 根據token獲取redis中的用戶valueMap<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);HttpSession session = request.getSession();// 判斷用戶是否存在if (userMap.isEmpty()) {// 用戶不存在 直接放行return true;}// 用戶存在,將hash數據轉換為userDTO,存信息到ThreadLocalUserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);UserHolder.saveUser(userDTO);// 刷新token有效期,放行stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}
/*** @author : mereign* @date : 2022/5/5 - 10:31* @desc : 登錄攔截器,攔截需要攔截的請求,判斷登錄信息*/
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 判斷登錄if (UserHolder.getUser() == null) {response.setStatus(401);return false;}return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
}

??創建完攔截器之后要將兩個攔截器通過配置類配置到容器中生效,多個攔截器的優先級,默認按照添加順序執行優先級,但是也可以使用order方法指定優先級,按參數的大小排序優先級,參數越小優先級越高

/*** @author : mereign* @date : 2022/5/5 - 10:43* @desc : 配置類注冊攔截器*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Autowiredprivate RefreshTokenInterceptor refreshTokenInterceptor;@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 前置攔截器registry.addInterceptor(refreshTokenInterceptor).addPathPatterns("/**").order(0);// 后置攔截器registry.addInterceptor(loginInterceptor).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);}
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/535024.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/535024.shtml
英文地址,請注明出處:http://en.pswp.cn/news/535024.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

加工中心刻字宏程序_FANUC OI系列圖書——車床、銑床及加工中心編程

? 這是金屬加工(mw1950pub)發布的第10035篇文章導讀今天跟大家分享FANUC OI系列圖書&#xff0c;包括車床、銑床及加工中心編程&#xff0c;快來看看吧&#xff01;《FANUC 0i數控車床/加工中心編程技巧與實例》選擇在企業里應用*廣泛、編程*具代表性的日本FANUCSeries0i-TC/T…

Redis 的緩存策略

各位小伙伴們大家好&#xff0c;歡迎來到這個小扎扎的Redis 6專欄&#xff0c;在這個系列專欄中我對B站黑馬的Redis教程進行一個總結&#xff0c;鑒于 看到就是學到、學到就是賺到 精神&#xff0c;這波依然是血賺 ┗|&#xff40;O′|┛ &#x1f4a1;Redis知識點速覽&#…

python顯示數據長度_python – 獲取CSV的長度以顯示進度

我正在處理大量CSV文件,每個文件都包含大量行.我的目標是逐行獲取數據并使用 Python將其寫入數據庫.但是,由于存在大量數據,我希望能夠跟蹤已寫入的數據量.為此,我計算了排隊的文件數量,并在每次文件完成時繼續添加一個文件. 我想為CSV文件做類似的事情并顯示我在哪一行,以及總…

關閉后天 樹莓派_陪你一起玩樹莓派-系統安裝

從今天就開始我們的樹莓派之旅&#xff0c;心情是不是有點小激動&#xff1f;我們拿到一個樹莓派是一個裸機。我們要準備一張16G的高速TF閃存卡&#xff0c;一個5V/2A的USB電源和一根micro B的 usb線。裝機步驟&#xff1a;一、下載樹莓派系統1、瀏覽器打開樹莓派官方網站 http…

簡單的簽到代碼_PHP實現一個小小的簽到功能,到底用MySQL還是Redis?

來源 | http://suo.im/5EWN3k今天&#xff0c;看下簽到功能怎么選擇&#xff1f;現在的網站和app開發中&#xff0c;簽到是一個很常見的功能&#xff0c;如微博簽到送積分&#xff0c;簽到排行榜~微博簽到如移動app &#xff0c;簽到送流量等活動&#xff0c;移動app簽到用戶簽…

【Redis 6】緩存穿透、緩存雪崩、緩存擊穿(附解決方案、代碼)

各位小伙伴們大家好&#xff0c;歡迎來到這個小扎扎的Redis 6專欄&#xff0c;在這個系列專欄中我對B站黑馬的Redis教程進行一個總結&#xff0c;鑒于 看到就是學到、學到就是賺到 精神&#xff0c;這波依然是血賺 ┗|&#xff40;O′|┛ &#x1f4a1;Redis知識點速覽&#…

8軟件遇到的問題及解決方法_Excel工作表中的8個常見問題,你一定遇到過,附解決方法...

在Excel工作表中&#xff0c;最常用的還是一些技巧&#xff0c;如果能夠熟練掌握&#xff0c;對于工作效率的提高絕對不是一點點哦&#xff0c;結合工作實際&#xff0c;小編對工作中常見的問題進行了總結&#xff0c;一共有8類&#xff0c;你一定也遇到過……一、Excel工作表常…

unity著色器和屏幕特效開發秘笈_Oculus研發分享:開發移動VR內容時應避免的PC渲染技術...

查看引用/信息源請點擊&#xff1a;映維網開發移動VR內容時應避免的PC渲染技術&#xff08;映維網 2019年11月25日&#xff09;有不少開發者都是以與PC相同的方式來開發Quest游戲&#xff0c;但這可能會導致優化性能方面出現大量困難。Oculus軟件工程師特雷弗達什&#xff08;T…

Java包裝類、java中的方法傳參機制:按值調用

各位小伙伴們大家好&#xff0c;歡迎來到這個小扎扎的《Java核心技術 卷Ⅰ》筆記專欄&#xff0c;在這個系列專欄中我將記錄淺學這本書所得收獲&#xff0c;鑒于 看到就是學到、學到就是賺到 精神&#xff0c;這波簡直就是血賺 &#x1f4a1;涉及的知識點速通&#x1f6eb; 方法…

尤克里里怎么樣_尤克里里和吉他區別?尤克里里與吉他相比有什么不可替代的優勢...

尤克里里和吉他有什么區別&#xff1f;想必大家都見過尤克里里吧&#xff0c;就是類似吉他形狀的一種小型弦撥樂器。我們可以簡單的認為&#xff1a;尤克里里是簡化版本的吉他&#xff0c;更加簡單&#xff0c;更加便宜。小編彈的就是尤克里里2.從外觀上&#xff1a;吉他很大&a…

餓漢懶漢單例設計模式的使用及區別、java中的import關鍵字

各位小伙伴們大家好&#xff0c;歡迎來到這個小扎扎的《Java核心技術 卷Ⅰ》筆記專欄&#xff0c;在這個系列專欄中我將記錄淺學這本書所得收獲&#xff0c;鑒于 看到就是學到、學到就是賺到 精神&#xff0c;這波簡直就是血賺 &#x1f4a1;涉及的知識點速通&#x1f6eb; 關于…

廣電運通不好進嗎_我可以說鄭州新風的安裝大部分都是垃圾嗎?

說鄭州的新風安裝都是垃圾&#xff0c;這話很無禮&#xff0c;很自大&#xff0c;很傲慢&#xff0c;但是我能說確實是這樣嘛&#xff1f;其實包括我以前安裝的也不合格——雖然我不是故意的。這幾年見過許多家同行安裝的新風&#xff0c;可以說目前見到的很多家都不合格&#…

Java迭代器和Collection接口

各位小伙伴們大家好&#xff0c;歡迎來到這個小扎扎的《Java核心技術 卷Ⅰ》筆記專欄&#xff0c;在這個系列專欄中我將記錄淺學這本書所得收獲&#xff0c;鑒于 看到就是學到、學到就是賺到 精神&#xff0c;這波簡直就是血賺 &#x1f4a1;涉及的知識點速通&#x1f6eb; 關于…

atlas安裝需要kafka嗎_Atlas 2.1.0 實踐(2)—— 安裝Atlas

在完成Atlas編譯以后&#xff0c;就可以進行Atlas的安裝了。Atlas的安裝主要是安裝Atlas的Server端&#xff0c;也就Atlas的管理頁面&#xff0c;并確保Atlas與Kafka Hbase Solr等組件的集成。Atlas的系統架構如下&#xff0c;在確保 底層在完成Atlas編譯以后&#xff0c;就可以…

ue4中隱藏燈光和相機圖標_[HDRP]物理燈光是什么?科普向

HDRP拋棄了Bulitin的燈光&#xff0c;改用物理單位以及物理屬性。那么物理燈光到底是什么&#xff1f;請點贊評論來支持作者&#xff0c;提前祝大家新年快樂。https://connect.unity.com/p/hdrp-wu-li-deng-guang-shi-shi-yao-ke-pu-xiang?connect.unity.com同步更新unity con…

私鑰經過哈希計算可以產生公鑰_「區塊鏈基礎概念100」:公鑰和私鑰 | 027

免責聲明&#xff1a;本文旨在傳遞更多市場信息&#xff0c;不構成任何投資建議。文章僅代表作者觀點&#xff0c;不代表火星財經官方立場。小編&#xff1a;記得關注哦投資區塊鏈&#xff0c;猛戳&#xff1a;火星財經App下載來源&#xff1a;學習區塊鏈原文標題&#xff1a;「…

關于List集合類ArrayList、LinkedList、Vector詳解

各位小伙伴們大家好&#xff0c;歡迎來到這個小扎扎的《Java核心技術 卷Ⅰ》筆記專欄&#xff0c;在這個系列專欄中我將記錄淺學這本書所得收獲&#xff0c;鑒于 看到就是學到、學到就是賺到 精神&#xff0c;這波簡直就是血賺 &#x1f4a1;涉及的知識點速通&#x1f6eb; 關于…

1562a檢測軟件_洛達1562a空間音頻版評測!!!

哈嘍大家好&#xff01;這里是小澤&#xff0c;一個不專業的Beatboxer~~~今天給大家帶來一期HQB最新空間音頻版洛達1562a耳機的評測視頻&#xff0c;建議先贊再看&#xff01;&#xff08;滑稽&#xff09;聽說你不點贊&#xff1f;昨晚拍了一晚&#xff0c;但是因為是第一次搞…

python 趣味編程課_青少年編程:Python趣味編程基礎入門課程

課程目錄 章節1:編程課前說明試看 課時1 編程課前說明07:49可試看 章節2:第一章 Python基礎-認識環境試看 課時2 1、什么是計算機程序和編程&#xff1f;08:48可試看 課時3 2、為什么學習編程&#xff1f;03:10可試看 課時4 3、Python的安裝11:48可試看 課時5 4、用Python編寫第…

shell181網格劃分_ANSYS中Shell181單元介紹誰知道

再畢業設計做模型中要應用到SHELL181單元&#xff0c;那么這個單元有什么好處誰能具體的告訴我下&#xff0c;我有英文但是有的地方翻譯不通。或者誰能幫我翻譯一下~謝謝~SHELL181issuitableforanalyzingth...再畢業設計做模型中要應用到SHELL181單元&#xff0c;那么這個單元有…