短信登錄分別使用session和redis實現
1、基于Session實現登錄
主要功能:
- 發送驗證碼
- 短信驗證碼登錄、注冊
- 校驗登錄狀態
1.1 實現發送短信驗證碼功能
1.1.1 業務邏輯
用戶在提交手機號后,會校驗手機號是否合法,如果不合法,則要求用戶重新輸入手機號
如果手機號合法,后臺此時生成對應的驗證碼,同時將驗證碼進行保存,然后再通過短信的方式將驗證碼發送給用戶
1.1.2 代碼實現
模擬發送短信驗證碼功能,把短信驗證碼控制臺打印
@Overridepublic Result sendCode(String phone, HttpSession session) {// 1. 校驗手機號if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手機號格式錯誤!");}// 2.生成驗證碼, 長度6位隨機數String code = RandomUtil.randomNumbers(CAPTCHA_LENGTH);// 3.保存驗證碼到sessionsession.setAttribute("code", code);// 4.發送驗證碼(模擬)log.info("發送驗證碼成功,驗證碼:{}", code);// 5.返回成功信息return Result.ok();}
代碼使用了Hutool的工具類,RegexUtils和RandomUtil,其中:
- RegexUtils.isPhoneInvalid() : 用于手機號格式校驗、郵箱校驗、驗證碼校驗。手機號格式,不滿足,返回true。
- RandomUtil.randomNumbers() : 生成隨機數
1.1.3 實現效果
1.2 實現短信驗證登錄、注冊功能
1.2.1 業務邏輯
用戶將驗證碼和手機號進行輸入,后臺從session中拿到當前驗證碼,然后和用戶輸入的驗證碼進行校驗,如果不一致,則無法通過校驗,如果一致,則后臺根據手機號查詢用戶,如果用戶不存在,則為用戶創建賬號信息,保存到數據庫,無論是否存在,都會將用戶信息保存到session中,方便后續獲得當前登錄信息
1.2.2 代碼實現
@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {// 1.獲取手機號、驗證碼String phone = loginForm.getPhone();String code = loginForm.getCode();// 2.校驗驗證碼if (!code.equals(session.getAttribute(CAPTCHA))) {return Result.fail("驗證碼錯誤!");}// 3.根據手機號查詢用戶User user = this.lambdaQuery().eq(User::getPhone, phone).one();// 4.判斷用戶是否存在if (user == null) {// 4.1 不存在,創建新用戶,并保存到數據庫中user = createNewUser(phone);}// 4.2 存在,保存用戶到sessionLoginFormDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);session.setAttribute(USER_NICK_NAME, userDTO);// 5.返回成功信息return Result.ok();}/*** 創建新用戶* @param phone 手機號* @return User*/private User createNewUser(String phone) {User user = new User();user.setPhone(phone);user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(USER_NICK_NAME_APPEND_LENGTH));save(user);return user;}
代碼使用了Hutool的工具類,BeanUtil,其中:
-
BeanUtil.copyProperties(user, UserDTO.class)? : 用于將user中的數據復制給UserDTO實體,其中字段和字段類型要保存一致。這步操作是用來,session存儲盡可能存儲少量數據,防止數據泄露。
1.2.3 實現效果
注意用Apifox實現這個操作,要先調用手機驗證碼接口,然后在調用用戶登錄接口,并且用戶登錄接口的驗證碼參數需要用到調用的手機驗證碼接口生成的驗證碼
1.2.4 tomcat的運行原理和ThreadLocal
1.2.4.1 tomcat的運行原理
當用戶發起請求時,會訪問我們像tomcat注冊的端口,任何程序想要運行,都需要有一個線程對當前端口號進行監聽,tomcat也不例外,當監聽線程知道用戶想要和tomcat連接連接時,那會由監聽線程創建socket連接,socket都是成對出現的,用戶通過socket像互相傳遞數據,當tomcat端的socket接受到數據后,此時監聽線程會從tomcat的線程池中取出一個線程執行用戶請求,在我們的服務部署到tomcat后,線程會找到用戶想要訪問的工程,然后用這個線程轉發到工程中的controller,service,dao中,并且訪問對應的DB,在用戶執行完請求后,再統一返回,再找到tomcat端的socket,再將數據寫回到用戶端的socket,完成請求和響應
通過以上講解,我們可以得知 每個用戶其實對應都是去找tomcat線程池中的一個線程來完成工作的, 使用完成后再進行回收,既然每個請求都是獨立的,所以在每個用戶去訪問我們的工程時,我們可以使用threadlocal來做到線程隔離,每個線程操作自己的一份數據
1.2.4.2 ThreadLocal
如果小伙伴們看過threadLocal的源碼,你會發現在threadLocal中,無論是他的put方法和他的get方法, 都是先從獲得當前用戶的線程,然后從線程中取出線程的成員變量map,只要線程不一樣,map就不一樣,所以可以通過這種方式來做到線程隔離
1.3 實現登錄校驗攔截器(ThreadLocal)
如果不用攔截器,每個controller都會先進行登錄校驗
1.3.1 業務邏輯
把攔截器攔截的用戶信息保存到ThreadLocal,因為ThreadLocal是線程,每一個進入tomcat的請求都是一個線程,ThreadLocal給每一個用戶開辟線程空間創建獨立線程。
1.3.2 代碼實現
/*** ThreadLocal 處理user信息*/
public class UserHolder {/*** 定義ThreadLocal常量* 這里使用UserDTO,是因為ThreadLocal存儲,只需要存儲少量數據,避免數據泄露*/private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();/*** 保存用戶** @param user*/public static void saveUser(UserDTO user) {tl.set(user);}/*** 從ThreadLocal獲取用戶** @return UserDTO*/public static UserDTO getUser() {return tl.get();}/*** 刪除用戶*/public static void removeUser() {tl.remove();}
}/*** 登錄攔截器*/
public class LoginInterceptor implements HandlerInterceptor {/*** 前置攔截* 登錄校驗* @param request 獲取session* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.從request中獲取sessionHttpSession session = request.getSession();// 2.獲取session中的用戶 userObject user = session.getAttribute(USER_NICK_NAME);// 3.判斷用戶是否存在if (user == null) {// 4.不存在,攔截 ,返回狀態碼401response.setStatus(UNAUTHORIZED);return false;}// 5.存在,保存用戶信息到ThreadLocalUserHolder.saveUser((UserDTO) user);// 6.放行return true;}/*** 渲染之后攔截* 用戶登錄完畢,銷毀登錄信息,避免用戶信息泄露* @param request* @param response* @param handler* @param ex* @throws Exception*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用戶,避免數據泄露UserHolder.removeUser();}
}/*** 登錄攔截器生效*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {/*** 添加攔截器* @param registry 攔截器注冊器*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 添加攔截器,并排除不需要攔截的路徑// ** 是通配符,表示任意registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login");}
}
2、Session實現登錄的弊端
session登錄會有session共享問題
每個tomcat中都有一份屬于自己的session,假設用戶第一次訪問第一臺tomcat,并且把自己的信息存放到第一臺服務器的session中,但是第二次這個用戶訪問到了第二臺tomcat,那么在第二臺服務器上,肯定沒有第一臺服務器存放的session,所以此時 整個登錄攔截功能就會出現問題,我們能如何解決這個問題呢?早期的方案是session拷貝,就是說雖然每個tomcat上都有不同的session,但是每當任意一臺服務器的session修改時,都會同步給其他的Tomcat服務器的session,這樣的話,就可以實現session的共享了
但是這種方案具有兩個大問題
1、每臺服務器中都有完整的一份session數據,服務器壓力過大。
2、session拷貝數據時,可能會出現延遲
所以咱們后來采用的方案都是基于redis來完成,我們把session換成redis,redis數據本身就是共享的,就可以避免session共享的問題了
3、基于Redis實現登錄
首先我們要思考一下利用redis來存儲數據,那么到底使用哪種結構呢?由于存入的數據比較簡單,我們可以考慮使用String,或者是使用哈希,如下圖,如果使用String,同學們注意他的value,用多占用一點空間,如果使用哈希,則他的value中只會存儲他數據本身,如果不是特別在意內存,其實使用String就可以啦。
3.1 實現發送短信驗證碼功能
3.1.1 業務邏輯
由原理存儲到session,改為存儲到redis中。
3.1.2 代碼實現
@Overridepublic Result sendCode(String phone, HttpSession session) {// 1. 校驗手機號if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手機號格式錯誤!");}// 2.生成驗證碼, 長度6位隨機數String code = RandomUtil.randomNumbers(CAPTCHA_LENGTH);// 3.保存驗證碼到redis,設置過期時間stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);// 4.發送驗證碼(模擬)log.info("發送驗證碼成功,驗證碼:{}", code);// 5.返回成功信息return Result.ok();}
3.1.3 Apifox調用接口,實現效果
小技巧:把有效期改為無效
3.2 實現短信驗證登錄、注冊功能
3.2.1 業務邏輯
當注冊完成后,用戶去登錄會去校驗用戶提交的手機號和驗證碼,是否一致,如果一致,則根據手機號查詢用戶信息,不存在則新建,最后將用戶數據保存到redis,并且生成token作為redis的key,當我們校驗用戶是否登錄時,會去攜帶著token進行訪問,從redis中取出token對應的value,判斷是否存在這個數據,如果沒有則攔截,如果存在則將其保存到threadLocal中,并且放行。
3.2.2 代碼實現
@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {// 1.獲取手機號、驗證碼String phone = loginForm.getPhone();String code = loginForm.getCode();// 1. 校驗手機號if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回錯誤信息return Result.fail("手機號格式錯誤!");}// 3.從redis中獲取驗證碼,校驗驗證碼String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);if (StrUtil.isBlank(cacheCode) && !code.equals(cacheCode)) {// 不一致,報錯return Result.fail("驗證碼錯誤!");}// 4.根據手機號查詢用戶User user = this.lambdaQuery().eq(User::getPhone, phone).one();// 5.判斷用戶是否存在if (user == null) {// 5.1 不存在,創建新用戶,并保存到數據庫中user = createNewUser(phone);}// 7.保存用戶信息到 redis中// 7.1.隨機生成token,作為登錄令牌String token = UUID.randomUUID().toString(true);// 7.2.將User對象轉為HashMap存儲UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((k, v) -> v.toString()));// 7.3.存儲stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token, userMap);// 7.4.設置token有效期stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.返回tokenreturn Result.ok(token);}/*** 創建新用戶* @param phone 手機號* @return User*/private User createNewUser(String phone) {User user = new User();user.setPhone(phone);user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(USER_NICK_NAME_APPEND_LENGTH));save(user);return user;}
代碼中使用Hutool的BeanUtil工具的beanToMap方法,把對象轉成Map。
- ?Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((k, v) -> v.toString()));
3.2.3 實現效果
用Apifox調用接口,要先調用3.2.1,獲取驗證碼作為參數,然后在調用登錄接口
3.3 解決狀態登錄刷新問題
把token存儲到redis中,設置有效期,為了防止有效期失效,每當用戶訪問,就刷新token,讓token不失效。
3.3.1 業務邏輯(攔截器優化)
設置兩個攔截器,第一個攔截器用于刷新token,保存ThreadLocal。確保一切請求都觸發刷新token的動作。第二個攔截器,查ThreadLocal,不存在就攔截
3.3.2 代碼實現
/*** 第一個攔截器:刷新token、保存ThreadLocal* 攔截所有請求,只有要請求就攔截,然后進行刷新token有效期*/
public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.獲取請求頭中的tokenString 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數據轉為UserDTOUserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6.存在,保存用戶信息到 ThreadLocalUserHolder.saveUser(userDTO);// 7.刷新token有效期stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用戶UserHolder.removeUser();}
}/*** 第二個攔截器,判斷ThreadLocal中是否有用戶信息*/
public class LoginInterceptor implements HandlerInterceptor {/*** 前置攔截* 登錄校驗* @param request 獲取session* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.判斷是否需要攔截(ThreadLocal中是否有用戶)if (UserHolder.getUser() == null) {// 沒有,需要攔截,設置狀態碼response.setStatus(UNAUTHORIZED);// 攔截return false;}// 有用戶,放行return true;}}/*** 登錄攔截器生效*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;/*** 添加攔截器* 給兩個攔截器后面加 order,用于決定誰先執行 值越小越先執行* @param registry 攔截器注冊器*/@Overridepublic 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);}
}
上述代碼使用Hutool的BeanUtil工具的fillBeanWithMap方法,把Map轉實體。
- UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
視頻學習:黑馬點評項目實戰,掌握企業實戰項目真實應用場景,一套精通redis緩存技術_嗶哩嗶哩_bilibili