前言:
主要完成了基于Session實現登錄,解決集群的Session共享問題,從而實現了基于Redis來實現共享Session登錄
1.基于Session實現登錄
1.1.發送短信驗證碼
步驟:
前端提交手機號
==》校驗手機號
==》不符合返回錯誤信息,符合生成驗證碼
==》保存驗證碼到Session(應該保存手機號與驗證碼)
==》發送驗證碼
==》返回數據
?
?解釋:我們這個是以手機號為唯一標識,前端提交用戶輸入的手機號,后端校驗手機號的格式,符合就生成一個隨機6位的數字驗證碼,并保存到Session中(為了后續登錄與注冊時校驗用戶驗證碼是否填寫正確)(當然這里應該保存手機號與驗證碼,不然會有一個錯誤)
1.2.短信驗證碼實現登錄與注冊
步驟:
前端提交手機號和驗證碼
==》校驗手機號和驗證碼
==》不通過返回錯誤信息,通過根據手機號查詢用戶信息
==》判斷用戶是否存在
==》不存在,創建新用戶,并且保存到數據庫中
==》最終存在與不存在都將保存用戶到Session中(方便后續校驗登錄狀態)
==》結束
?
@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {//獲取緩存數據Object catchPhone = session.getAttribute("phone");Object catchCode = session.getAttribute("code");//獲取登錄數據String code = loginForm.getCode();String phone = loginForm.getPhone();//校驗if(!code.equals(catchCode) || !phone.equals(catchPhone)){return Result.fail("手機號或驗證碼錯誤");}
// if (RegexUtils.isPhoneInvalid(phone)) {
// return Result.fail("手機號格式錯誤");
// }
// String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
// if(!code.equals(cacheCode)){
// return Result.fail("驗證碼錯誤");
// }User user = query().eq("phone", phone).one();//判斷用戶是否存在if(user == null){//不存在User userNew = new User();userNew.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));userNew.setPhone(phone);user = userNew;//保存數據庫save(user);}//添加到SessionUserDTO userDTO = new UserDTO();BeanUtil.copyProperties(user,userDTO);
// String token = UUID.randomUUID().toString(true);
// String tokenKey = LOGIN_USER_KEY + token;
// Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
// CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString()));
// stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);session.setAttribute("user",userDTO);
// stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);return Result.ok();}
解釋: 前端提交用戶輸入的手機號與驗證碼,后端從Session中取出存入的手機號與驗證碼與用戶提交的相對比,相同那么我們就通過手機號來從數據庫中查詢用戶信息,信息為空,那么我們需要注冊一個用戶并將其存入數據庫中,最終存在與不存在的(我們自己注冊了一個用戶)都會有一個用戶信息,將用戶信息存入Session中(這里還有個細節)(方便后續校驗登錄狀態,判斷用戶是否登錄)
?注意:
老師有一個錯誤,在發送短信驗證碼的功能實現時,老師只保存了驗證碼到Session中,那么等到校驗驗證碼來實現登錄與注冊時,如果我將手機號修改了會怎么樣,只要我手機號符合格式一樣可以登錄與注冊,所以我們需要保持前后手機號的一致性,那么存入Session的數據應該是驗證碼與手機號,然后登錄與注冊時同時校驗手機號與驗證碼是否一致
1.3.校驗登陸狀態
步驟:
前端請求攜帶Cokie
==》后端攔截器從Session中獲取用戶信息
==》判斷用戶是否存在
==》不存在不放行,存在放行
?
public class RefreshTokenInterceptor implements HandlerInterceptor {private final StringRedisTemplate stringRedisTemplate;public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//獲取請求頭tokenString token = request.getHeader("authorization");//判斷是否為空if (StrUtil.isBlank(token)) {return true;}
// HttpSession session = request.getSession();//設置keyString key = LOGIN_USER_KEY + token;//獲取Redis中的數據Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);//判斷是否為空if(userMap.isEmpty()){return true;}//獲取userUserDTO user = BeanUtil.fillBeanWithMap(userMap,new UserDTO(),false);//存入線程中UserHolder.saveUser( user);stringRedisTemplate.expire(key,LOGIN_USER_TTL, TimeUnit.MINUTES);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}
?解釋:因為每次我們都需要寫一堆邏輯來判斷用戶是否存在,那么我們可以使用AOP思想,在具體一點就是使用攔截器,但是后續服務器是不是需要用戶信息,還是從Session中取出用戶信息嗎?不,頻繁的訪問Session會增加其訪問開銷并且造成線程不安全,所以我們依舊采用將用戶信息存入TreadLocal中(線程安全)
注意:使用攔截器時,前端需要我們返回一些基礎數據給它渲染用戶信息,而我們之前存入Session的數據是整個用戶的數據(包含密碼,手機號),這些信息不適合暴露出去,所以對應之前存入Session時的數據需要做一些修改,只需要存一些基礎用戶信息即可(且存入Session的信息過多也是不好的)
因此我們需要隱藏用戶的敏感信息(存入一些需展示的信息即可)
2.集群的Session共享問題
問題介紹:Session的數據一般存儲到服務端或Redis中(手動存),而客戶端只保存了一個SessionID(通過Cokie傳遞)(而且每個客戶端的SessionID不同),那么當需要訪問多個服務端時,Session數據并不共享,就會出現問題
解決:
方案一:服務器之間進行Session的拷貝(內存浪費,有延遲)
方案二:使用Redis存(Redis是存入內存的,訪問速度快,多個服務器可以同時訪問不會造成內存浪費)
3.基于Redis實現共享Session登錄
?
@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result sendCode(String phone, HttpSession session) {//1.校驗手機號if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手機號格式錯誤");}//2.生成驗證碼String code = RandomUtil.randomNumbers(6);//3.保存驗證碼stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
// session.setAttribute("code",code);
// session.setAttribute("phone",phone);//4.返回驗證碼log.debug("當前驗證碼:{}",code);//5.返回return Result.ok();}@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {
// //獲取緩存數據
// Object catchPhone = session.getAttribute("phone");
// Object catchCode = session.getAttribute("code");//獲取登錄數據String code = loginForm.getCode();String phone = loginForm.getPhone();//校驗
// if(!code.equals(catchCode) || !phone.equals(catchPhone)){
// return Result.fail("手機號或驗證碼錯誤");
// }if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手機號格式錯誤");}String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);if(!code.equals(cacheCode)){return Result.fail("驗證碼錯誤");}User user = query().eq("phone", phone).one();//判斷用戶是否存在if(user == null){//不存在User userNew = new User();userNew.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));userNew.setPhone(phone);user = userNew;//保存數據庫save(user);}//添加到SessionUserDTO userDTO = new UserDTO();BeanUtil.copyProperties(user,userDTO);String token = UUID.randomUUID().toString(true);String tokenKey = LOGIN_USER_KEY + token;Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString()));stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
// session.setAttribute("user",userDTO);stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);return Result.ok(token);}
public class RefreshTokenInterceptor implements HandlerInterceptor {private final StringRedisTemplate stringRedisTemplate;public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//獲取請求頭tokenString token = request.getHeader("authorization");//判斷是否為空if (StrUtil.isBlank(token)) {return true;}
// HttpSession session = request.getSession();//設置keyString key = LOGIN_USER_KEY + token;//獲取Redis中的數據Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);//判斷是否為空if(userMap.isEmpty()){return true;}//獲取userUserDTO user = BeanUtil.fillBeanWithMap(userMap,new UserDTO(),false);//存入線程中UserHolder.saveUser( user);stringRedisTemplate.expire(key,LOGIN_USER_TTL, TimeUnit.MINUTES);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 {UserDTO user = UserHolder.getUser();if(user == null){response.setStatus(401);return false;}return true;}}
解釋:其實最終我們只需要修改存入Session的部分,改為存入Redis即可
注意:
1.由于先前Session是用戶訪問一次(就是進行一次操作)就會更新登錄憑證的過期時間(防止登錄失效),那么我們也需要實現該功能,一般想到是在攔截器中放行之前更新時間,但是由于之前實現的攔截器有特定的攔截路徑,那么沒有被攔截的路徑我們也需要進行更新,所以我們可以在加一個攔截器專門來更新時間(第一個攔截器更新時間,并進行存用戶,第二個攔截器進行判斷用戶是否存在(它有特定的攔截路徑),不存在不放行)
2.由于使用的是StringRedisTemplate,它要求key與value都必須為String類型,所以我們需要將數據轉換成String類型再存入Redis中
3.注意攔截器的順序,一般先添加的攔截器先執行(你也可以設置優先級order)
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Autowiredprivate 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);registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0).addPathPatterns("/**");}
}