基于Redis實(shí)現(xiàn)短信驗(yàn)證碼登錄項(xiàng)目示例(附源碼)
Redis短信登錄流程描述
短信驗(yàn)證碼的發(fā)送
用戶提交手機(jī)號(hào),系統(tǒng)驗(yàn)證手機(jī)號(hào)是否有效,畢竟無效手機(jī)號(hào)會(huì)消耗你的短信驗(yàn)證次數(shù)還會(huì)導(dǎo)致系統(tǒng)的性能下降。如果手機(jī)號(hào)為無效的話就讓用戶重新提交手機(jī)號(hào),如果有效就生成驗(yàn)證碼并將該驗(yàn)證碼作為value保存到redis中對(duì)應(yīng)的key是手機(jī)號(hào),之所以這么做的原因是保證key的唯一性,如果使用固定字符串作為可以的話會(huì)被后面的數(shù)據(jù)所覆蓋。然后在控制臺(tái)輸出驗(yàn)證碼模擬發(fā)送驗(yàn)證碼的過程
短信驗(yàn)證碼的驗(yàn)證
用戶的手機(jī)號(hào)接收到驗(yàn)證碼后在平臺(tái)上提交驗(yàn)證碼,系統(tǒng)從redis中根據(jù)手機(jī)號(hào)讀取驗(yàn)證碼并進(jìn)行校驗(yàn),如果驗(yàn)證通過的話就根據(jù)用戶驗(yàn)證使用的手機(jī)號(hào)去數(shù)據(jù)庫中進(jìn)行查詢用戶信息。如果存在就將查詢到的用戶信息保存到redis中,完成登錄;如果不存在的話就創(chuàng)建一個(gè)新用戶,并將該用戶的信息分別保存到sql數(shù)據(jù)庫和redis中,生成隨機(jī)token作為key、使用hash結(jié)構(gòu)存儲(chǔ)user數(shù)據(jù)作為value,并將這個(gè)token返回給客戶端,至此完成登錄注冊(cè)
是否登錄的驗(yàn)證
用戶訪問系統(tǒng)業(yè)務(wù)邏輯的時(shí)候需要校驗(yàn)他是否已經(jīng)登錄,如果登錄可以訪問否則就去登錄,那么該如何完成是否登錄的校驗(yàn)?zāi)??這就要了解session的相關(guān)知識(shí)了,每一個(gè)session都有一個(gè)sessionId信息保存在瀏覽器的cookie中,當(dāng)用戶使用瀏覽器發(fā)送請(qǐng)求的時(shí)候會(huì)攜帶上cookie信息,此時(shí)系統(tǒng)就可以使用cookie中的sessionId獲取到session信息,并通過session獲取到登錄時(shí)存儲(chǔ)的用戶信息。如果此時(shí)用戶在數(shù)據(jù)庫中存在的話就將該用戶的信息緩存在ThreadLocal(方便后續(xù)驗(yàn)證)中,并放行該訪問;否則就說明發(fā)送請(qǐng)求的用戶未登錄或不合法,就要攔截到他的請(qǐng)求前往登錄
源碼分析
模擬發(fā)送短信驗(yàn)證碼
UserController定義與前端交互
@Resource private IUserService userService; /** ?* 發(fā)送手機(jī)驗(yàn)證碼 ?*/ @PostMapping("code") public Result sendCode(@RequestParam("phone") String phone, HttpSession session) { ? ? // 發(fā)送短信驗(yàn)證碼并保存驗(yàn)證碼 ? ? return userService.sendCode(phone, session); }
上面使用到了sendCode方法,在userService里定義一下接口,然后在對(duì)應(yīng)實(shí)現(xiàn)類中按照上面的流程重寫該方法的業(yè)務(wù)邏輯代碼
@Override public Result sendCode(String phone, HttpSession session) { ? ? // 校驗(yàn)手機(jī)號(hào) ? ? if (RegexUtils.isPhoneInvalid(phone)) { ? ? ? ? // 無效手機(jī)號(hào),返回錯(cuò)誤信息 ? ? ? ? return Result.fail("手機(jī)號(hào)格式有誤!"); ? ? } ? ? // 有效生成驗(yàn)證碼 ? ? String code = RandomUtil.randomNumbers(6); ? ? // 保存 (固定前綴+手機(jī)號(hào)) 和驗(yàn)證碼到Redis中,設(shè)置驗(yàn)證碼的有效期為2分鐘 ? ? // RedisConstants.LOGIN_CODE_KEY = “l(fā)ogin:code:” ? ? // RedisConstants.LOGIN_CODE_TTL = 2L ? ? stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES); ? ? // 模擬發(fā)送驗(yàn)證碼 ? ? log.debug("驗(yàn)證碼:{}", code); ? ? // 返回 ? ? return Result.ok(); }
手機(jī)號(hào)格式校驗(yàn)使用到的RegexUtils類中的工具方法
/** ?* 手機(jī)號(hào)正則 ?*/ 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}$"; /** * 是否是無效手機(jī)格式 ?* @param phone 要校驗(yàn)的手機(jī)號(hào) ?* @return true:符合,false:不符合 ?*/ public static boolean isPhoneInvalid(String phone){ ? ? return mismatch(phone, RegexPatterns.PHONE_REGEX); } // 校驗(yàn)是否不符合正則格式 private static boolean mismatch(String str, String regex){ ? ? if (StrUtil.isBlank(str)) { ? ? ? ? return true; ? ? } ? ? return !str.matches(regex); }
短信驗(yàn)證碼的驗(yàn)證
UserController定義與前端交互,其中參數(shù)LoginFormDTO 是前端使用手機(jī)號(hào)+驗(yàn)證碼登錄或者手機(jī)號(hào)+密碼登錄是傳遞過來的JSON數(shù)據(jù)
/** * 登錄功能 * @param loginForm 登錄參數(shù),包含手機(jī)號(hào)、驗(yàn)證碼;或者手機(jī)號(hào)、密碼 */ @PostMapping("/login") public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){ // 實(shí)現(xiàn)登錄功能 return userService.login(loginForm, session); }
上面使用到了login方法,在userService里定義一下接口,然后在對(duì)應(yīng)實(shí)現(xiàn)類中按照上賣弄的流程描述重寫該方法的業(yè)務(wù)邏輯代碼
@Override public Result login(LoginFormDTO loginForm, HttpSession session) { ? ? String phone = loginForm.getPhone(); ? ? // 驗(yàn)證碼校驗(yàn) ? ? String code = loginForm.getCode(); ? ? String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone); ? ? if (cacheCode == null || !code.equals(cacheCode)) { ? ? ? ? return Result.fail("驗(yàn)證碼錯(cuò)誤!"); ? ? } ? ? // 根據(jù)手機(jī)號(hào)查詢用戶信息 ? ? User user = query().eq("phone", phone).one(); ? ? if (user == null) { ? ? ? ? // 不存在就創(chuàng)建一個(gè)新用戶 ? ? ? ? user = createUserWithPhone(phone); ? ? } ? ? // 保存用戶信息到redis中 ? ? // 生成隨機(jī)token ? ? String token = UUID.randomUUID().toString(true); ? ? // user先轉(zhuǎn)userDTO再轉(zhuǎn)hashMap存儲(chǔ) ?轉(zhuǎn)HashMap時(shí)的第三個(gè)參數(shù)的意思是忽略null值將值都轉(zhuǎn)換成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); ? ? // 設(shè)置失效時(shí)間為30分鐘 ? ? // RedisConstants.LOGIN_USER_TTL = 30L ? ? stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES); ? ? // 返回前端token ? ? return 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; }
保存的時(shí)候使用BeanUtil將User轉(zhuǎn)換成UserDTO進(jìn)行存儲(chǔ),UserDTO的結(jié)構(gòu)如下,只保存一部分的數(shù)據(jù),一方面可以不用來回傳遞用戶有關(guān)的隱私數(shù)據(jù),一方面也節(jié)省內(nèi)存提高性能。由于這里的id是數(shù)值類型,但是stringRedisTemplate存儲(chǔ)時(shí)需要hash的鍵值都是String型,所以說應(yīng)該在存儲(chǔ)之前將id的值轉(zhuǎn)換成String類型,就在上面代碼塊的24~27行完成了這個(gè)操作
@Data public class UserDTO { ? ? private Long id; ? ? private String nickName; ? ? private String icon; }
校驗(yàn)是否登錄
用戶發(fā)送請(qǐng)求不止一次,所以說登錄驗(yàn)證也不止進(jìn)行一次,于是可以使用攔截器完成驗(yàn)證,攔截器的使用可分為兩步:
創(chuàng)建攔截器
/** ?* @author : mereign ?* @date : 2022/5/5 - 10:31 ?* @desc : 攔截器,實(shí)現(xiàn)請(qǐng)求攔截,判斷登錄信息 ?*/ @Component public class LoginInterceptor implements HandlerInterceptor { ? ? @Autowired ? ? private StringRedisTemplate stringRedisTemplate; ? ? @Override ? ? public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ? ? ? ? // 獲取請(qǐng)求頭中的token信息 ? ? ? ? String token = request.getHeader("authorization"); ? ? ? ? if (StrUtil.isBlank(token)) { ? ? ? ? ? ? // token為空,返回401未授權(quán)狀態(tài)碼,攔截 ? ? ? ? ? ? response.setStatus(401); ? ? ? ? ? ? return false; ? ? ? ? } ? ? ? ? // 根據(jù)token獲取redis中的用戶value ? ? ? ? Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token); ? ? ? ? HttpSession session = request.getSession(); ? ? ? ? // 判斷用戶是否存在 ? ? ? ? if (userMap.isEmpty()) { ? ? ? ? ? ? // 用戶不存在,返回401未授權(quán)狀態(tài)碼,攔截 ? ? ? ? ? ? response.setStatus(401); ? ? ? ? ? ? return false; ? ? ? ? } ? ? ? ? // 用戶存在,將hash數(shù)據(jù)轉(zhuǎn)換為userDTO,存信息到ThreadLocal ? ? ? ? UserDTO 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; ? ? } ? ? @Override ? ? public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { ? ? } ? ? @Override ? ? public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { ? ? ? ? UserHolder.removeUser(); ? ? } }
注冊(cè)攔截器
/** ?* @author : mereign ?* @date : 2022/5/5 - 10:43 ?* @desc : ?*/ @Configuration public class MvcConfig implements WebMvcConfigurer { ? ? @Autowired ? ? private LoginInterceptor loginInterceptor; ? ? @Override ? ? public 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(){ // 獲取當(dāng)前登錄的用戶并返回 UserDTO user = UserHolder.getUser(); return Result.ok(user); }
登錄驗(yàn)證優(yōu)化
由上面的登錄驗(yàn)證可知,我們對(duì)一些需要用戶登錄驗(yàn)證的功能設(shè)置了攔截器,如果驗(yàn)證通過會(huì)刷新token的有效期,這樣的話只要用戶一直訪問我們攔截的功能就可以一直保持token是有效的。但是,如果用戶登陸之后的操作一直是不需要驗(yàn)證的,那也就意味著token的有效期一直不會(huì)刷新,這樣的話30分鐘之后token就會(huì)失效用戶驗(yàn)證就會(huì)失敗,這樣顯然是不合理的
于是我們可以使用兩個(gè)攔截器完成,最前面的負(fù)責(zé)攔截所有的請(qǐng)求,獲取token、從redis中查詢用戶,將查詢結(jié)果放到ThreadLocal(可能存null)、刷新token有效期,最后直接放行;后面的攔截器只負(fù)責(zé)判斷有沒有從redis中查詢到用戶,他從ThreadLocal獲取查詢結(jié)果,判斷有則放行無則攔截
創(chuàng)建兩個(gè)攔截器
/** ?* @author : mereign ?* @date : 2022/5/5 - 10:31 ?* @desc : 前置攔截器,攔截所有請(qǐng)求,前置工作 ?*/ @Component public class RefreshTokenInterceptor implements HandlerInterceptor { ? ? @Autowired ? ? private StringRedisTemplate stringRedisTemplate; ? ? @Override ? ? public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ? ? ? ? // 獲取請(qǐng)求頭中的token信息 ? ? ? ? String token = request.getHeader("authorization"); ? ? ? ? if (StrUtil.isBlank(token)) { ? ? ? ? ? ? // token為空 直接放行 ? ? ? ? ? ? return true; ? ? ? ? } ? ? ? ? // 根據(jù)token獲取redis中的用戶value ? ? ? ? Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token); ? ? ? ? HttpSession session = request.getSession(); ? ? ? ? // 判斷用戶是否存在 ? ? ? ? if (userMap.isEmpty()) { ? ? ? ? ? ? // 用戶不存在 直接放行 ? ? ? ? ? ? return true; ? ? ? ? } ? ? ? ? // 用戶存在,將hash數(shù)據(jù)轉(zhuǎn)換為userDTO,存信息到ThreadLocal ? ? ? ? UserDTO 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; ? ? } ? ? @Override ? ? public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { ? ? } ? ? @Override ? ? public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { ? ? ? ? UserHolder.removeUser(); ? ? } }
/** ?* @author : mereign ?* @date : 2022/5/5 - 10:31 ?* @desc : 登錄攔截器,攔截需要攔截的請(qǐng)求,判斷登錄信息 ?*/ @Component public class LoginInterceptor implements HandlerInterceptor { ? ? @Override ? ? public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ? ? ? ? // 判斷登錄 ? ? ? ? if (UserHolder.getUser() == null) { ? ? ? ? ? ? response.setStatus(401); ? ? ? ? ? ? return false; ? ? ? ? } ? ? ? ? return true; ? ? } ? ? @Override ? ? public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { ? ? } ? ? @Override ? ? public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { ? ? } }
創(chuàng)建完攔截器之后要將兩個(gè)攔截器通過配置類配置到容器中生效,多個(gè)攔截器的優(yōu)先級(jí),默認(rèn)按照添加順序執(zhí)行優(yōu)先級(jí),但是也可以使用order方法指定優(yōu)先級(jí),按參數(shù)的大小排序優(yōu)先級(jí),參數(shù)越小優(yōu)先級(jí)越高
/** ?* @author : mereign ?* @date : 2022/5/5 - 10:43 ?* @desc : 配置類注冊(cè)攔截器 ?*/ @Configuration public class MvcConfig implements WebMvcConfigurer { ? ? @Autowired ? ? private RefreshTokenInterceptor refreshTokenInterceptor; ? ? @Autowired ? ? private LoginInterceptor loginInterceptor; ? ? @Override ? ? public 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); ? ? } }
到此這篇關(guān)于基于Redis實(shí)現(xiàn)短信驗(yàn)證碼登錄項(xiàng)目示例(附源碼)的文章就介紹到這了,更多相關(guān)Redis 短信驗(yàn)證碼登錄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis數(shù)據(jù)庫分布式設(shè)計(jì)方案介紹
大家好,本篇文章主要講的是Redis數(shù)據(jù)庫分布式設(shè)計(jì)方案介紹,感興趣的同學(xué)趕快來看一看吧,對(duì)你有幫助的話記得收藏一下2022-01-01深入了解Redis連接數(shù)問題的現(xiàn)象和解法
一般情況?Redis?連接數(shù)問題并不常見,但是當(dāng)你業(yè)務(wù)服務(wù)增加、對(duì)?Redis?的依賴持續(xù)增強(qiáng)的過程中,可能會(huì)遇到很多?Redis?的問題,這個(gè)時(shí)候,Redis?連接數(shù)可能就成了一個(gè)常見的問題,在本章節(jié),希望能夠帶大家了解Redis連接數(shù)問題的現(xiàn)象和解法,需要的朋友可以參考下2023-12-12redis連接報(bào)錯(cuò)error:NOAUTH Authentication required
本文主要介紹了redis連接報(bào)錯(cuò)error:NOAUTH Authentication required,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05淺析Redis底層數(shù)據(jù)結(jié)構(gòu)Dict
Redis是一個(gè)鍵值型的數(shù)據(jù)庫,我們可以根據(jù)鍵實(shí)現(xiàn)快速的增刪改查,而鍵與值的映射關(guān)系正是通過Dict來實(shí)現(xiàn)的,當(dāng)然?Dict?也是?Set?Hash?的實(shí)現(xiàn)方式,本文就詳細(xì)帶大家介紹一下Redis底層數(shù)據(jù)結(jié)構(gòu)?Dict,,需要的朋友可以參考下2023-05-05談?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-08Redis 單機(jī)安裝和哨兵模式集群安裝的實(shí)現(xiàn)
本文主要介紹了Redis 單機(jī)安裝和哨兵模式集群安裝的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07將音頻文件轉(zhuǎn)二進(jìn)制分包存儲(chǔ)到Redis的實(shí)現(xiàn)方法(奇淫技巧操作)
這篇文章主要介紹了將音頻文件轉(zhuǎn)二進(jìn)制分包存儲(chǔ)到Redis的實(shí)現(xiàn)方法(奇淫技巧操作),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07