Redis 基础 - 短信验证码登录
本文摘要
先簡單列出用session方式。然后提出session方式的問題,并簡單換為用Redis的方式。最后通過優(yōu)化來解決一些小問題。
Redis基礎(chǔ) - 基本類型及常用命令
Redis基礎(chǔ) - Java客戶端
基于session實現(xiàn)短信登陸的簡單流程
發(fā)送驗證碼
前端把手機號傳給服務(wù)端,后端經(jīng)過校驗后,生成驗證碼并存入到session中,并通過第三方平臺給用戶手機發(fā)短信驗證碼。
登陸/注冊
前端把登陸用的手機號和剛才接收的驗證碼傳給服務(wù)端,后端經(jīng)過校驗后,若沒毛病就用手機號去查用戶表,沒有用戶的話給他注冊,若有用戶,就算是登陸成功。注冊/登陸成功后,把用戶的部分信息(除去敏感信息)放到session中。
1)校驗手機號:是否規(guī)范,是否是剛才收到驗證碼的那個手機號。
2)校驗驗證碼:放入session的驗證碼與前臺傳入的驗證碼比較。
查看用戶登陸狀態(tài)
前臺調(diào)用服務(wù)端提供的相關(guān)API,因為請求會帶上cookie,cookie里面有sessionid,后端通過sessionID取出相關(guān)用戶信息。
用攔截器實現(xiàn)
一般來說,這個邏輯寫在攔截器。但controller的有些部分可能會也要用到結(jié)果,所以有必要把攔截器的結(jié)果傳給controller里,但要注意線程的安全,所以用ThreadLocal解決,即攔截器里搞到用戶信息之后,可以把他保存在ThreadLocal,因為ThreadLocal是線程域?qū)ο?#xff0c;每一個進入Tomcat的請求都是一個獨立的線程,所以ThreadLocal會在線程內(nèi)開辟一個內(nèi)存空間,去保存對應(yīng)的用戶,這樣的話每個線程相互不干擾。所以到了controller后從ThreadLocal里取出用戶即可。
代碼示例:
LoginInterceptor.java
UserHolder.java
public class UserHolder {pviate static final ThreadLocal<User> tl = new ThreadLocal<>();public static void saveUser(User user) {t1.set(user);}public static User getUser() {returun tl.get();}public static void removeUser() {tl.remove();} }MvcConfig.java
@Configuration public class MvcConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/login");} }網(wǎng)友之言
網(wǎng)友1:為啥要使用ThreadLocal? 網(wǎng)友2:方便同一個線程可以重復(fù)使用user。集群的session共享問題以及對應(yīng)的解決方案
像上面的那樣玩,在集群時會出現(xiàn)問題。因為多臺Tomcat并不共享session存儲空間,當請求切換到不同的Tomcat服務(wù)時導(dǎo)致數(shù)據(jù)丟失的問題。
session的替代方案需要滿足的條件
- 數(shù)據(jù)共享,這是最重要的,就是因為不共享才導(dǎo)致了剛才的問題。
- 內(nèi)存存儲,因為session是內(nèi)存存儲,所以讀寫效率非常高。
- key、value結(jié)構(gòu)
解決方案 - 使用Redis
因為Redis是Tomcat以外的存取方案,所以任何一臺Tomcat都能訪問到Redis,所以就能實現(xiàn)數(shù)據(jù)共享。而且Redis是內(nèi)存存儲,性能非常強,讀寫延時基本都是微妙級別。Redis也是key、value結(jié)構(gòu),所以結(jié)構(gòu)也簡單,用Redis替代session,勢在必行。
用Redis實現(xiàn)登陸/注冊的簡單流程
發(fā)送驗證碼
生成驗證碼后,以前是保存在session,現(xiàn)在要保存在Redis,數(shù)據(jù)類型可以用string。
session有一個特點是每一個不同的瀏覽器發(fā)請求時都有一個獨立的session,也就是說Tomcat內(nèi)部維護著很多的session,那么不同瀏覽器攜帶著手機號來的時候,都是自己獨立的session,所以他們都能用"code"作為key比如 session.setAttribute("code",code),因為互相之間不干擾。
但Redis是共享的內(nèi)存空間,所以key不能簡簡單單的用"code",因為來不同的請求的時候,由于服務(wù)端只有一個Redis,所以一個人來的時候"code"=手機驗證碼1,另一人來的時候又"code"=手機驗證碼2,就出現(xiàn)覆蓋的現(xiàn)象。所以,要確保每一個不同的手機號來做驗證時保存的Key都得是不一樣的,所以直接用手機號當做key也行。這樣將來登陸時也方便得多,直接用手機號[key]和驗證碼[value]去Redis找驗證碼并比較驗證碼即可。
登陸/注冊
一系列校驗后,如果用戶存在(或給她注冊后),就把用戶信息保存到Redis。
在這里也要考慮兩個問題,第一考慮數(shù)據(jù)類型的選擇,第二是考慮到key的問題。此時要保存的是用戶的對象,所以如果不考慮內(nèi)存問題,而且數(shù)據(jù)量較少的話,可以用string類型的JSON之類的,不過從優(yōu)化的角度考慮的話,推薦使用hash類型。第二關(guān)于key,要保證唯一,還有方便客戶端將來可以攜帶的那種key,這里推薦用隨機的token為key存儲用戶數(shù)據(jù)。
查看是否已登陸
用session時,因為Tomcat自動把sessionid寫到瀏覽器的cookie里,所以每次請求帶了cookie就帶了sessionid,所以根據(jù)sessionid查找session可以找到用戶,也就是說這里的sessionid就是你的登陸憑證。
但現(xiàn)在用的是Redis,所以登陸憑證不再是sessionid了,這里的登陸憑證就是token了。即,以后用戶請求訪問時,都要帶上token。由于這個token不會被Tomcat自己寫到瀏覽器,所以只能我們手動的把token返回給客戶端,那么以后客戶端請求都會攜帶這個token了,那么后端可以基于token從Redis獲取用戶信息。
比如前端可以用代碼編寫把token放到sessionStorage(瀏覽器的一種存儲方式)。
... // 保存用戶信息到session sessionStorage.setItem("token",token) ...而每次請求時都要攜帶token。
... // request攔截器,將用戶Token作為請求頭放入頭中 let token = sessionStorage.getItem("token"); axios.interceptors.request.use(config => {if(token) config.headers['authorization'] = tokenreturn config;} ) ...這樣以來以后凡是axios發(fā)起的請求(所有的Ajax請求),都會攜帶authorization這個頭。將來在服務(wù)端獲取authorization這個請求頭,拿到token,從而實現(xiàn)對登陸的驗證。
網(wǎng)友之言
用戶信息的key不能以手機號的理由之一是把這玩意保存到前端瀏覽器會有泄露的風險。代碼示例:
UserServiceImpl.java
@Resource private StringRedisTemplate stringRedisTemplate;// 發(fā)送驗證碼的部分 @Override public Result sendCode(String phone) {// 1,校驗手機號// 2,如果不符合,返回錯誤信息// 3,如果符合,生成驗證碼,變量code// 4,保存驗證碼到Redis(為了防止其他業(yè)務(wù)也有可能會用手機號當做key,所以前面最好加個前綴,/*由于這里是登陸時發(fā)送驗證碼,所以可以用login:code來當做前綴。然后第二個是這里需要設(shè)置驗證碼的有效期,如果不加有效期,那以后每個人都登陸發(fā)驗證碼,終有一天,Redis內(nèi)存會被沾滿。比如這里設(shè)置為2分鐘【參數(shù)3,參數(shù)4】。當然,建議設(shè)置為常量。)*/stringRedisTemplate.opsForValue().set("login:code:"+phone,code, 2,TimeUnit.MINUTES);// 5,發(fā)送驗證碼// 6,返回return Result.ok(); } // 短信登陸/注冊 @Override public Result login(參數(shù)列表) {//1,校驗手機號//2,如果不符合,返回錯誤信息//3,從Redis獲取驗證碼并校驗String cacheCode = stringRedisTemplate.opsForValue().get("login:code:"+phone);//4,與請求中的驗證碼比較不一致的話,報錯//5,一致時,根據(jù)手機號查詢用戶表//6,用戶沒有的話,注冊,并用戶信息存到Redis//7,有的話,用戶信息存到Redis//7.1,生成token,作為登陸令牌String token = UUID.randomUUID().toString(true);// true:不帶中線的//7.2,將user對象轉(zhuǎn)為hashMap存儲(可以用BeanUtil工具類[cn.hutool.core.bean 需要在pom.xml引入依賴]把User轉(zhuǎn)換為HashMap)Map<String,Object> map = BeanUtil.beanToMap(userDTO);//7.3,存儲數(shù)據(jù)到Redis(token也加前綴,可定義為常量。也要加上有效期。)stringRedisTemplate.opsForHash().putAll("login:token:"+token,map);//7.4,設(shè)置token的有效期設(shè)置為30分鐘。/*但這里的有效期是,只要登陸開始,過了30分鐘,Redis必然會把這個干掉。所以不管用戶訪問還是不訪問,30分鐘后,一定會被干掉。應(yīng)該像session那樣,只要用戶不斷的訪問,就要不斷的更新token的有效期,那么怎么知道用戶有沒有訪問呢,網(wǎng)友:攔截器。即只要訪問攔截器,就說明有請求,每次訪問時,更新有效期。具體在下面的3)改。*/stringRedisTemplate.expire("login:token:"+token, 30, TimeUnit.MINUTES);//8,返回token到客戶端return Result.ok(token); }查看是否已登陸
LoginInterceptor.java
UserHolder.java
public class UserHolder {pviate static final ThreadLocal<User> tl = new ThreadLocal<>();public static void saveUser(User user) {t1.set(user);}public static User getUser() {returun tl.get();}public static void removeUser() {tl.remove();} }MvcConfig.java
@Configuration public class MvcConfig implements WebMvcConfigurer {// 這個類是用了@Configuration,所以會被spring來構(gòu)建對象,所以這里可以用依賴注入@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 加到LoginInterceptor的構(gòu)造參函數(shù)數(shù)中registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns("/user/login");} }運行之后,因為這里使用的是stringRedisTemplate,這玩意要求key value都是string類型,在這里轉(zhuǎn)map時,由于UserDTO的id字段是long類型,所以出現(xiàn)轉(zhuǎn)string的錯誤java.lang.Long cannot be cast to java.lang.String。
所以轉(zhuǎn)map的時候,要確保UserDTO里的字段值都要以string的形式存儲到map里。第一個笨方法是不用工具類,可以自己new一個map,然后一個一個干入到map里。第二個方法是,還是用BeanUtil這個工具,比如使用有三個參數(shù)的:
網(wǎng)友之言
網(wǎng)友1:我還是自己new吧。 網(wǎng)友2:我選擇笨方法。 網(wǎng)友3:我選擇使用JSON登陸攔截器的優(yōu)化
目前登陸攔截器的執(zhí)行邏輯如下:
- 獲取用戶提交的頭文件中的token
- 根據(jù)這個token查詢Redis中的用戶(不存在則攔截,存在則繼續(xù))
- 把用戶信息保存到ThreadLocal
- 刷新token有效期
- 放行
但有一個問題,并不是所有的請求都會經(jīng)過這個攔截器,她攔截的只是需要登陸校驗的路徑,即不是攔截一切。
所以這就導(dǎo)致比如說一個用戶只訪問不需要登陸的頁面,比如首頁或文章詳情頁之類的,這樣的話由于不經(jīng)過攔截器,所以不會刷新token的有效期,這樣的話,30分鐘以后,用戶的登陸就得被干掉了。
可以在原有的攔截器基礎(chǔ)上,在他的前面再加一個新的攔截器1,這樣的話用戶請求先進入攔截器1,然后再進入攔截器(原來的攔截器,以下稱為攔截器2)。
讓攔截器1攔截一切路徑,那么在攔截器1里做刷新token有效期的動作,即除了2號的不存在則攔截,存在則繼續(xù)之外,其余的部分都放到攔截器1上,即攔截器1并不會進行真正的攔截,只是主要做刷新有效期的工作。這樣的話,一切請求都會觸發(fā)更新token有效期了。當然,沒登陸時,由于沒有token,所以在攔截器1里直接放行即可,接著在攔截器2里做相應(yīng)判斷即可。
新建攔截器1代碼示例:
RefreshTokenInterceptor.java
private StringRedisTemplate stringRedisTemplate;public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate; }@Override public boolean preHandle(request,response,) {// 進入controller之前處理// 1,獲取請求頭中的tokenString token = request.getHeader("authorization");// 剛才前端代碼中token放到了authorizationif (StrUtil.isBlank(token)) {// 空的話直接放行到第二個攔截器return true;}// 2,基于token去獲取Redis中的用戶Map<Object,Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:" + token);// 3,判斷用戶是否存在if(userMap.isEmpty()) {// 空的話直接放行到第二個攔截器return true;}// 5,將查詢到的hash數(shù)據(jù)轉(zhuǎn)為UserDTO對象(參數(shù)1:要轉(zhuǎn)換的map,// 參數(shù)2:轉(zhuǎn)換成什么bean,參數(shù)3:是否忽略異常 false 否)UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6,若存在,保存用戶信息到ThreadLocalUserHolder.saveUser(userDTO);// 7,刷新token的有效期stringRedisTemplate.expire("login:token:"+token, 30, TimeUnit.MINUTES);// 8,放行return true; }@Override public void afterCompletion(request,response,) {// 業(yè)務(wù)執(zhí)行完畢后,銷毀用戶信息,避免內(nèi)存泄露UserHolder.removeUser(); }攔截器2代碼示例:
LoginInterceptor.java
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(request,response,) {// 判斷是否需要攔截(即判斷ThreadLocal中是否有用戶)if (UserHolder.getUser() == null) {// 沒有,需要攔截,設(shè)置狀態(tài)碼response.setStatus(401);return false;}// 有用戶,則放行return true;}@Overridepublic void afterCompletion(request,response,) {// 業(yè)務(wù)執(zhí)行完畢后,銷毀用戶信息,避免內(nèi)存泄露UserHolder.removeUser();} }然后再MvcConfig配置剛才新加的攔截器1,代碼示例:
MvcConfig.java
@Configuration public class MvcConfig implements WebMvcConfigurer {// 這個類是用了@Configuration,所以會被spring來構(gòu)建對象,所以這里可以用依賴注入@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 加到LoginInterceptor的構(gòu)造參函數(shù)數(shù)中// registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns("/user/login");// 改為如下,把stringRedisTemplate干掉registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/login").order(1);// 新家的攔截器1(她需要stringRedisTemplate),默認是addPathPatterns("/**")registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);} }默認情況下,攔截器的order都是0,所以根據(jù)配置的順序而執(zhí)行。為了嚴謹,也可以給每個攔截器設(shè)置order,值越小越先執(zhí)行。
總結(jié)
以上是生活随笔為你收集整理的Redis 基础 - 短信验证码登录的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 模仿电影院座位预定效果
- 下一篇: mysql导入亿级数据_如何将上亿条大容