項目實戰-黑馬點評
項目架構
短信登錄
發送短信驗證碼
實現思路就是按照上圖左一部分,
實現類如下
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {/*** 驗證手機號發送驗證碼** @param phone* @param session* @return*/@Overridepublic Result sendCode(String phone, HttpSession session) {//1.校驗手機號if (RegexUtils.isPhoneInvalid(phone)) {//2.如果不符合,可以返回錯誤信息return Result.fail("手機號格式錯誤");}//3.生成驗證碼String code = RandomUtil.randomNumbers(6);//4.保存驗證碼到sessionsession.setAttribute("code", code);//5.發送驗證碼log.debug("發送驗證碼成功,驗證碼為:{}",code);//返回okreturn Result.ok();}
}
這里封裝了RegexUtils
工具類,調用了反向驗證手機號方法,我們可以學習一下這個工具類的實現
public class RegexUtils {/*** 是否是無效手機格式* @param phone 要校驗的手機號* @return true:符合,false:不符合*/public static boolean isPhoneInvalid(String phone){return mismatch(phone, RegexPatterns.PHONE_REGEX);}/*** 是否是無效郵箱格式* @param email 要校驗的郵箱* @return true:符合,false:不符合*/public static boolean isEmailInvalid(String email){return mismatch(email, RegexPatterns.EMAIL_REGEX);}/*** 是否是無效驗證碼格式* @param code 要校驗的驗證碼* @return true:符合,false:不符合*/public static boolean isCodeInvalid(String code){return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX);}// 校驗是否不符合正則格式private static boolean mismatch(String str, String regex){if (StrUtil.isBlank(str)) {return true;}return !str.matches(regex);}
}
其實就是先檢查手機號是否為空,如果不為空再把當前手機號字符串按照正則表達式匹配。
短信驗證碼登錄
校驗手機號,校驗驗證碼,如果不一致直接返回錯誤信息。
如果一致,需要查詢用戶,如果根據當前手機號,用戶不存在,那么創建新用戶,其實就是insert
。
這里沒有Mapper
,MyBatis-Plus
是MyBatis
的增強工具,內置了大量的方法,無需XML就能完成CRUD
/*** 實現登錄功能* @param loginForm* @param session* @return*/
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {String phone = loginForm.getPhone();//1.校驗手機號if(RegexUtils.isPhoneInvalid(phone)){//手機號不符合,返回錯誤信息return Result.fail("手機號格式錯誤");}//2.校驗驗證碼Object cacheCode = session.getAttribute("code");String code = loginForm.getCode();if(cacheCode == null || !cacheCode.equals(code)){//3.不一致,報錯return Result.fail("驗證碼錯誤");}//4.一致,根據手機號查詢用戶 select * from tb_user where phone = ?User user = query().eq("phone", phone).one();//5.判斷用戶是否存在if(user == null){//6.不存在,創建新用戶并保存user = createUserWithPhone(phone);}//7.保存用戶信息到sessionsession.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));return Result.ok();
}private User createUserWithPhone(String phone){
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
save(user);
return user;
}
登錄校驗
在訪問后端的多個接口的時候,不可能每次訪問都得登陸,保證只要登陸一次即可。于是可以使用攔截器統一攔截,獲取session信息,如果存在那么放行,并把信息保存到ThreadLocal
,保證可以隨時調用。如果不存在,那么攔截。
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.獲取sessionHttpSession session = request.getSession();//2.獲取session的用戶Object user = session.getAttribute("user");//3.判斷用戶是否存在if(user == null){//4.不存在,攔截,返回401狀態碼response.setStatus(401);return false;}//5.存在,保存用戶信息到ThreadLocalUserHolder.saveUser((UserDTO) user);//6.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}
集群的session共享問題
由于多臺Tomcat并不共享session的共享空間,請求切換到不同的Tomcat時就會導致數據丟失。
最開始考慮的是,只要把數據拷貝一份到每個Tomcat即可,但會導致空間浪費問題,因為保存的都是相同數據。
替代方案應滿足:
- 數據共享
- 內存存儲
- key、value結構
顯然可以借助Redis
基于Redis實現共享session登錄
發送驗證碼
改動的地方就是本來儲存在session中,現在把驗證碼保存到Redis中,使用的key是業務+手機號,從而保證唯一性。
@Resource
private StringRedisTemplate stringRedisTemplate;
/*** 驗證手機號發送驗證碼** @param phone* @param session* @return*/
@Override
public Result sendCode(String phone, HttpSession session) {//1.校驗手機號if (RegexUtils.isPhoneInvalid(phone)) {//2.如果不符合,可以返回錯誤信息return Result.fail("手機號格式錯誤");}//3.生成驗證碼String code = RandomUtil.randomNumbers(6);//4.保存驗證碼到session set key value ex 120stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code,RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);//5.發送驗證碼log.debug("發送驗證碼成功,驗證碼為:{}",code);//返回okreturn Result.ok();
}
實現登錄功能
更新部分在于,取數據從redis中獲取,生成的隨機token作為令牌和儲存用戶信息的key。
/*** 實現登錄功能* @param loginForm* @param session* @return*/
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {String phone = loginForm.getPhone();//1.校驗手機號if (RegexUtils.isPhoneInvalid(phone)) {//手機號不符合,返回錯誤信息return Result.fail("手機號格式錯誤");}//2.校驗驗證碼String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode();if (cacheCode == null || !cacheCode.equals(code)) {//3.不一致,報錯return Result.fail("驗證碼錯誤");}//4.一致,根據手機號查詢用戶 select * from tb_user where phone = ?User user = query().eq("phone", phone).one();//5.判斷用戶是否存在if (user == null) {//6.不存在,創建新用戶并保存user = createUserWithPhone(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().ignoreNullValue().setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));//7.3 儲存String userKey = LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(userKey, userMap);//7.4 設置token有效期stringRedisTemplate.expire(userKey, LOGIN_USER_TTL, TimeUnit.MINUTES);//8.返回tokenreturn Result.ok(token);
}private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
save(user);
return user;
}
登錄攔截器的優化
我們當前的攔截器存在一個問題,就是攔截了需要登錄的請求時,為了避免永久數據對redis的壓力,我們在執行登陸后,一般會給token設置有效期,意思是長時間不點擊訪問頁面,那么token就會過期,不再允許訪問。但是現在只有一個攔截器,攔截的是需要登錄的路徑,而且只有在用戶登錄時會更新token有效期,這樣就會導致即使用戶在不停操作,但是不需要登錄操作的部分功能也不會更新token有效期,同時需要登陸操作的部分也不會更新token有效期。
怎么解決呢?
我們可以定義兩個攔截器,一個用來攔截所有路徑,但是不做“攔截”處理,主要負責獲取token、查詢Redis用戶、保存到ThreadLocal、更新token有效期、放行。另外一個攔截需要登陸的路徑,查詢ThreadLocal用戶,不存在就攔截,存在則放行。