目錄
IDEA集成git
傳統session存在的問題?
redis方案
業務流程
選用的數據結構
整體訪問流程
發送短信驗證碼
?獲取校驗驗證碼
配置登錄攔截器
攔截器注冊配置類
攔截器
用戶狀態刷新問題
刷新問題解決方案
IDEA集成git
遠程倉庫采用碼云,創建好倉庫,復制倉庫的url
?在idea中點擊,出現git選項,點擊ok
?之后右擊項目,點擊remotes
?填寫url即可集成git
傳統session存在的問題?
傳統的登錄認證會采用session進行登錄認證,將登錄的驗證碼,用戶信息都存放到session中,我們通過session來進行操作數據,這有什么問題呢
每個tomcat服務器中都有一份屬于自己的session,假設用戶第一次訪問第一臺tomcat,并且把自己的信息存放到第一臺服務器的session中,但是第二次這個用戶訪問到了第二臺tomcat,那么在第二臺服務器上,肯定沒有第一臺服務器存放的session,即session在各個服務器之間是不共通的,所以此時整個登錄攔截功能就會出現問題,而redis數據本身就是共享的,就可以避免session共享的問題了
redis方案
業務流程
- 將驗證碼存儲到redis中
- 將用戶數據存儲到redis中
選用的數據結構
存儲驗證碼時采用String類型,key采用業務代碼+手機號
存儲user數據時采用hash,key采用業務代碼+隨機的tonke
hash可以將對象中的每個字段獨立存儲,可以針對單個字段做CRUD,并且內存占用更少,實際上也可以采用String類型,但hash類型消耗內存較少,故選用String類型
整體訪問流程
?當注冊完成后,用戶去登錄會去校驗用戶提交的手機號和驗證碼,是否一致,如果一致,則根據手機號查詢用戶信息,不存在則新建,最后將用戶數據保存到redis,并且生成token作為redis的key,當我們校驗用戶是否登錄時,會去攜帶著token進行訪問,從redis中取出token對應的value,判斷是否存在這個數據,如果沒有則攔截,如果存在則將其保存到threadLocal中,簡化后續業務獲取用戶信息的代碼,后續業務需要登錄用戶的信息,只要在threadLocal中取即可,無需從redis中取從而增加復雜度,最后放行。
redis業務代碼常量(后續繼續補充)
public class RedisConstants {//發送驗證碼業務標識public static final String LOGIN_CODE_KEY = "login:code:";public static final Long LOGIN_CODE_TTL = 2L;//用戶登錄業務標識public static final String LOGIN_USER_KEY = "login:token:";public static final Long LOGIN_USER_TTL = 30L;}
發送短信驗證碼
采用日志打印形式,將驗證碼打印在控制臺
public Result sendCode(String phone) {//校驗手機號if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手機號碼格式不正確");}//手機號格式正確生成驗證碼String code = RandomUtil.randomNumbers(6);//保存到redisstringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);//打印日志log.debug("手機驗證碼為:" + code);return Result.ok();}
?獲取校驗驗證碼
我們需要對用戶敏感信息進行篩選,只給前端返回必要的用戶信息數據,需要封裝dto對象,同時在uuid生成token,作為redis取數據的key,最后要設置過期時間,防止reids內存爆炸,設置為30分鐘,因為session的過期時間也是30分鐘
public Result login(LoginFormDTO loginForm) {String phone = loginForm.getPhone();String code = loginForm.getCode();//校驗手機號if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手機號碼格式不正確");}//從redis取驗證碼,進行比對String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);if (cacheCode == null || !cacheCode.equals(code)) {return Result.fail("驗證碼錯誤");}//根據電話號碼從數據庫查詢用戶是否存在LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getPhone, phone);User user = userMapper.selectOne(queryWrapper);//不存在則創建if (user == null) {user = createUserWithPhone(phone);}String token = UUID.randomUUID().toString(true);//只返回不敏感的信息,使用dto進行封裝UserDTO userDTO = new UserDTO();BeanUtils.copyProperties(user, userDTO);//將dto對象轉化為map結構Map<String, String> userMap = new HashMap<>();userMap.put("id", userDTO.getId().toString());userMap.put("icon", userDTO.getIcon());userMap.put("nickName", userDTO.getNickName());
//業務代碼+token形成redis中的keyString tokenKey = LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);return Result.ok(token);}
配置登錄攔截器
攔截器注冊配置類
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 登錄攔截器registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login");}
}
攔截器
攔截器需要的stringRedisTemplate對象不能通過@Autowired直接注入
因為LoginInterceptor 對象是我們手動創建的,不受spring管控,不在spring容器中,故不能注入spring容器中的bean,只能通過在配置類中通過構造方法注入stringRedisTemplate對象
public class LoginInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public LoginInterceptor(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)) {response.setStatus(401);return false;}String tokenKey = LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);//3.判斷用戶是否存在if (userMap.isEmpty()) {//4.不存在,攔截,返回401狀態碼response.setStatus(401);return false;}UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);//5.存在,保存用戶信息到ThreadlocalUserHolder.saveUser(userDTO);//刷新登錄狀態stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);//6.放行return true;}
}
用戶狀態刷新問題
那就是登錄認證和刷新用戶狀態綁定在一個攔截器中,而需要登錄認證的路徑并不是全部路徑,如果用戶不訪問需要登錄認證的路徑,那就刷新不了用戶狀態,即30分鐘之后登錄就會退出,這明顯不符合我們的常識
刷新問題解決方案
我們可以再加一個攔截器,形成一個攔截器鏈,第一個攔截器對所有路徑進行攔截,而它的功能就是刷新登錄狀態,登錄認證則由第二個攔截器完成
?
?刷新狀態攔截器
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();}
登錄認證攔截器
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.判斷是否需要攔截(ThreadLocal中是否有用戶)if (UserHolder.getUser() == null) {// 沒有,需要攔截,設置狀態碼response.setStatus(401);// 攔截return false;}// 有用戶,則放行return true;}
}
為了保證攔截器的執行先后順序,配置類需設置order的大小,設定攔截器執行的先后的順序,如若不設置,就按照注冊順序的先后來執行
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@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);}
}