目錄
1. 使用 Redis 優化登陸模塊
1.1 使用 Redis 存儲驗證碼
1.2 使用 Redis 存儲登錄憑證
1.3 使用 Redis 緩存用戶信息
1. 使用 Redis 優化登陸模塊
- 使用 Redis 存儲驗證碼:驗證碼需要頻繁的訪問與刷新,對性能要求較高;驗證碼不需要永久保存,通常在很短的時間后就會失效;分布式部署時,存在 Session 共享的問題
- 使用 Redis 存儲登陸憑證:處理每次請求時,都要查詢用戶的登陸憑證,訪問的頻率非常高
- 使用 Redis 緩存用戶信息:處理每次請求時,都要根據拼爭查詢用戶信息,訪問的頻率非常高
1.1 使用 Redis 存儲驗證碼
在 RedisKeyUtil 類中添加:
- 定義驗證碼的前綴
- 添加登錄驗證碼方法(驗證碼和用戶是相關的,不同的用戶驗證碼不同):給用戶登錄頁面發送憑證(隨機生成的字符串),存入 Cookie 中,以字符串臨時標識用戶,傳入字符串(用戶臨時的憑證),返回 前綴 + 分隔符 + 憑證
private static final String PREFIX_KAPTCHA = "kaptcha";// 登錄驗證碼public static String getKaptchaKey(String owner) {return PREFIX_KAPTCHA + SPLIT + owner;}
驗證碼在登陸功能中使用(修改 LoginController 類中的生成驗證碼的方法):
- 重構獲取驗證碼方法:之前是把驗證碼存入 Session 中,現在使用 Redis
- 將驗證碼存入 Redis 中:首先構造 key,而 key 需要驗證碼的歸屬,這個憑證需要發送給客戶端,客戶端需要 cookie 保存,創建 Cookie、設置 Cookie 生成時間、有效路徑,最后發送給客戶端
- 拼接 key,將驗證碼存入 Redis 中,注入 RedisTemplate
//驗證碼在登陸功能中使用,重構獲取驗證碼方法:之前是把驗證碼存入 Session 中,現在使用 Redis//生成驗證碼的方法@RequestMapping(path = "/kaptcha", method = RequestMethod.GET)public void getKaptcha(HttpServletResponse response/*, HttpSession session*/) {// 生成驗證碼String text = kaptchaProducer.createText();BufferedImage image = kaptchaProducer.createImage(text);// 將驗證碼存入session//session.setAttribute("kaptcha", text);//將驗證碼存入 Redis 中:首先構造 key,而 key 需要驗證碼的歸屬// 這個憑證需要發送給客戶端,客戶端需要 cookie 保存,創建 Cookie、設置 Cookie 生成時間、有效路徑,最后發送給客戶端String kaptchaOwner = CommunityUtil.generateUUID();Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);cookie.setMaxAge(60);cookie.setPath(contextPath);response.addCookie(cookie);//拼接 key, 將驗證碼存入Redis,注入 RedisTemplateString redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);redisTemplate.opsForValue().set(redisKey, text, 60, TimeUnit.SECONDS);// 將圖片輸出給瀏覽器response.setContentType("image/png");try {OutputStream os = response.getOutputStream();ImageIO.write(image, "png", os);} catch (IOException e) {logger.error("響應驗證碼失敗:" + e.getMessage());}}
首次訪問登陸頁面,當 getKaptcha 方法被調用,生成驗證碼存入 Redis 中,在對登陸具體驗證的時候使用,因此還需要處理登陸的方法(logic 方法)?
- 之前是從 Session 中獲取驗證碼,現在需要從 Redis 中獲取(需要 key,而 key 需要驗證碼的歸屬,從 Cookie 中獲取),因此登陸方法還需要添加注解@CookieValue,從 Cookie 中取值
- 判斷 key 是否存在:如果存在,構造 key,然后從 Redis 中獲取這個值
//首次訪問登陸頁面,當getKaptcha方法被調用,生成驗證碼存入Redis中,在對登陸具體驗證的時候使用,因此還需要處理登陸的方法(logic 方法)@RequestMapping(path = "/login", method = RequestMethod.POST)//表單中傳入 用戶、密碼、驗證碼、記住我(boolean 類型)、Model(返回數據)、HttpSession(頁面傳入驗證碼和之前的驗證碼進行對比)// 、 HttpServletResponse (登錄成功,要把 ticket 發送給客戶端使用 cookie 保存,創建 cookie 使用 HttpServletResponse 對象)//之前是從 Session 中獲取驗證碼,現在需要從 Redis 中獲取(需要 key,而 key 需要驗證碼的歸屬,從 Cookie 中獲取)// 因此登陸方法還需要添加注解@CookieValue,從 Cookie 中取值public String login(String username, String password, String code, boolean remember, Model model, /*HttpSession session, */ HttpServletResponse response, @CookieValue("kaptchaOwner") String kaptchaOwner) {//檢查驗證碼//String kaptcha = (String) session.getAttribute("kaptcha");String kaptcha = null;//判斷 key 是否存在:如果存在,構造 key,然后從 Redis 中獲取這個值if (StringUtils.isNotBlank(kaptchaOwner)) {String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);kaptcha = (String) redisTemplate.opsForValue().get(redisKey);}if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {model.addAttribute("codeMsg", "驗證碼不正確");return "/site/login";}//檢查賬號、密碼:判斷賬號密碼是否正確:沒有勾選記住我,存入庫中的時間比較短;勾選記住我,存入庫中的時間比較長// 定義兩個常量放入 CommunityConstant 接口中:如果勾選記住我,使用記住狀態時間;如果不勾選,則使用默認的int expiredSeconds = remember ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;Map<String, Object> map = userService.login(username, password, expiredSeconds);//成功:取出 ticket 發送 cookie 給客戶端,重定向首頁if (map.containsKey("ticket")) {Cookie cookie = new Cookie("ticket", map.get("ticket").toString());//map 中拿到的是對象需要轉化為字符串cookie.setPath(contextPath);//有效路徑:整個項目但是不要寫死,寫參數即可cookie.setMaxAge(expiredSeconds);//設置 cookie 有效時間response.addCookie(cookie);//把 cookie 發送到頁面return "redirect:/index";} else { //如果登錄失敗,返回登陸頁面//把錯誤的消息返回給頁面model.addAttribute("usernameMsg", map.get("usernameMsg"));model.addAttribute("passwordMsg", map.get("passwordMsg"));return "/site/login";}}
1.2 使用 Redis 存儲登錄憑證
在 RedisKeyUtil 類中添加:
- 定義登錄憑證的前綴
- 添加登錄的憑證方法:獲得登錄憑證的詳細數據,傳入登錄成功的憑證(ticket),返回 前綴 + 分隔符 + 憑證
//定義登錄憑證的前綴private static final String PREFIX_TICKET = "ticket";// 添加登錄的憑證方法:獲得登錄憑證的詳細數據,傳入登錄成功的憑證(ticket),返回 前綴 + 分隔符 + 憑證public static String getTicketKey(String ticket) {return PREFIX_TICKET + SPLIT + ticket;}
接下來使用 Redis 存儲憑證代替之前的 LoginTicketMapper 類,在此類中添加注解 @Deprecated,表示不推薦使用;重構使用到 Bean 的地方:在
UserService 中使用到(在登錄成功以后生成憑證并且保存、退出刪除憑證、查詢憑證),修改 UserService 中登錄功能模塊
- 在登錄憑證時保存憑證到 Redis 中,拼接 key,注入 RedisTemplate
- 退出的時候,將狀態改為1:將 key 傳入 Redis 中,返回為一個對象,將狀態改為1,再把 key 傳回去
- 查詢憑證的時候:需要在 Redis 中查找,首先拼接 key,直接取
@Service
public class UserService implements CommunityConstant {@Autowiredprivate RedisTemplate redisTemplate;/*** 實現登錄功能*///實現登錄功能:成功、失敗、不存在等等情況,返回數據的情況很多,可以使用 map 封裝多種返回結果//登錄需要傳入用戶、密碼、憑證有限時間public Map<String, Object> login(String username, String password, int expiredSeconds) {Map<String, Object> map = new HashMap<>();// 空值處理if (StringUtils.isBlank(username)) {map.put("usernameMsg", "賬號不能為空!");return map;}if (StringUtils.isBlank(password)) {map.put("passwordMsg", "密碼不能為空!");return map;}// 驗證賬號User user = userMapper.selectByName(username);if (user == null) {map.put("usernameMsg", "該賬號不存在!");return map;}// 驗證狀態if (user.getStatus() == 0) {map.put("usernameMsg", "該賬號未激活!");return map;}// 驗證密碼password = CommunityUtil.md5(password + user.getSalt());//加密后的密碼if (!user.getPassword().equals(password)) {map.put("passwordMsg", "密碼不正確!");return map;}// 生成登錄憑證LoginTicket loginTicket = new LoginTicket();//創建實體往庫里存loginTicket.setUserId(user.getId());loginTicket.setTicket(CommunityUtil.generateUUID());//生成不重復隨機字符串loginTicket.setStatus(0);loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));//loginTicketMapper.insertLoginTicket(loginTicket);String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());redisTemplate.opsForValue().set(redisKey, loginTicket);map.put("ticket", loginTicket.getTicket());return map;}//退出業務方法//退出的時候把憑證傳入,根據憑證找到用戶進行退出,最后改變憑證狀態public void logout(String ticket) {//loginTicketMapper.updateStatus(ticket, 1);//退出的時候,將狀態改為1:將 key 傳入 Redis 中,返回為一個對象,將狀態改為1,再把 key 傳回去String redisKey = RedisKeyUtil.getTicketKey(ticket);LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);loginTicket.setStatus(1);redisTemplate.opsForValue().set(redisKey, loginTicket);}//添加 查詢憑證代碼public LoginTicket findLoginTicket(String ticket) {//return loginTicketMapper.selectByTicket(ticket);String redisKey = RedisKeyUtil.getTicketKey(ticket);return (LoginTicket) redisTemplate.opsForValue().get(redisKey);}//更新修改頭像路徑public int updateHeader(int userId, String headerUrl) {return userMapper.updateHeader(userId, headerUrl);}//修改密碼public int updatePassword(int userId,String password){return userMapper.updatePassword(userId,password);}//通過用戶名查詢用戶得到 idpublic User findUserByName(String username) {return userMapper.selectByName(username);}
}
1.3 使用 Redis 緩存用戶信息
在 RedisKeyUtil 類中添加:
- 定義用戶憑證的前綴
- 添加用戶的憑證方法:傳入用戶 id,返回 前綴 + 分隔符 + 憑證
private static final String PREFIX_USER = "user";// 用戶public static String getUserKey(int userId) {return PREFIX_USER + SPLIT + userId;}
緩存數據主要是重構 UserService 中 findUserId 方法:每次請求獲取憑證,根據憑證查詢用戶就需要調用?findUserId 方法,把每個 User 緩存到 Redis 中,在調用此方法效率就提升了。
緩存一般分為:
- 查詢 User 的時候,嘗試從緩存中取值,如果取到直接使用,如果取不到,則進行初始化緩存數據,相當于重構 findUserId 方法
- 改變用戶數據(頭像、密碼等):更新緩存數據或者直接刪除緩存(一般使用刪除,下次請求訪問用戶重新查詢)
- 從緩存中取值為 User:傳入 UserId,拼接 key;嘗試用 Redis 中取值
- 取不到,則進行初始化緩存數據:數據來源于 MySQL,在 MySQL 查詢數據,拼接 key,往 Redis 中存儲據
- 數據變更時清除緩存數據:拼接 key,刪除緩存
- 在 findUserId 方法中調用:首先在緩存中取值,如果為空初始化緩存,最后返回 User
- 在修改 User 的地方進行緩存清除
- 在 activation 方法中修改狀態為 1,然后清除緩存
- 在 修改頭像路徑 updateHeader 方法中:首先更新頭像,再去清理緩存