# 优化登录模块 --- ## 使用 Redis 短暂存储验证码 - 验证码需要频繁刷新,对性能要求较高 - 验证码不需永久保存(设置在 Cookie 和 Redis 中的保留时间为 60 s) - 分布式部署,存在 Session 共享问题 > 原来我们是将验证码存在 session 中,这里我们将其存入 redis 中 > > 存入 redis 中的数据 key(kaptcha:随机字符串) value(验证码) > > 用户在输入验证码的时候,还没有进行登录,我们无法通过用户的 id 指明这个验证码是针对谁的。所以这里我们随机生成一个字符串,将其短暂的存入 cookie,使用这个字符串来标识这个用户 工具类:生成 Redis 的 Key ```java /** * 生成 Redis 的 key */ public class RedisKeyUtil { private static final String SPLIT = ":"; private static final String PREFIX_KAPTCHA = "kaptcha"; // 验证码 /** * 登录验证码(指定这个验证码是针对哪个用户的) * @param owner 用户进入登录页面的时候,由于此时用户还未登录,无法通过 id 标识用户 * 随机生成一个字符串,短暂的存入 cookie,使用这个字符串来标识这个用户 * @return */ public static String getKaptchaKey(String owner) { return PREFIX_KAPTCHA + SPLIT + owner; } } ``` 修改生成验证码的方法: ```java /** * 生成验证码 * @param response * @param session */ @GetMapping("/kaptcha") public void getKaptcha(HttpServletResponse response, HttpSession session) { // 生成验证码 String text = kaptchaProducer.createText(); // 生成随机字符 System.out.println("验证码:" + text); BufferedImage image = kaptchaProducer.createImage(text); // 生成图片 // 将验证码存入 session // session.setAttribute("kaptcha", text); // 验证码的归属者 String kaptchaOwner = CommunityUtil.generateUUID(); Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner); cookie.setMaxAge(60); cookie.setPath(contextPath); response.addCookie(cookie); // 将验证码存入 redis String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner); redisTemplate.opsForValue().set(redisKey, text, 60, TimeUnit.SECONDS); // 将图片输出给浏览器 response.setContentType("image/png"); try { ServletOutputStream os = response.getOutputStream(); ImageIO.write(image, "png", os); } catch (IOException e) { logger.error("响应验证码失败", e.getMessage()); } } ``` ## 使用 Redis 存储登录凭证 - 处理每次请求时,都要查询用户的登录凭证,访问的频率非常高(登录凭证这个数据不需要删除,永久保存在 redis 中) 工具类:生成 Redis 的 Key ```java /** * 生成 Redis 的 key */ public class RedisKeyUtil { private static final String SPLIT = ":"; private static final String PREFIX_TICKET = "ticket"; // 登录凭证 /** * 登陆凭证 * @param ticket * @return */ public static String getTicketKey(String ticket) { return PREFIX_TICKET + SPLIT + ticket; } } ``` **LoginTicket 这张表以及相关操作可以废弃了**,不过我们最好不要直接就删除了。在 `LoginMapper` 类上面加上 `@Deprecated` 注解表示不推荐使用就好了。 修改 UserService 中生成/修改/查询登录凭证这三个方法 ```java // 生成登录凭证 String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket()); redisTemplate.opsForValue().set(redisKey, loginTicket); /** * 用户退出(将凭证状态设为无效) * @param ticket */ public void logout(String ticket) { // loginTicketMapper.updateStatus(ticket, 1); // 修改(先删除再插入)对应用户在 redis 中的凭证 String redisKey = RedisKeyUtil.getTicketKey(ticket); LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey); loginTicket.setStatus(1); redisTemplate.opsForValue().set(redisKey, loginTicket); } /** * 根据 ticket 查询 LoginTicket 信息 * @param ticket * @return */ public LoginTicket findLoginTicket(String ticket) { // return loginTicketMapper.selectByTicket(ticket); String redisKey = RedisKeyUtil.getTicketKey(ticket); return (LoginTicket) redisTemplate.opsForValue().get(redisKey); } ``` ## 使用 Redis 缓存用户信息 - 处理每次请求时,都要根据凭证查询用户信息,访问的频率非常高(用户信息只在 Redis 中保存一段时间) 在 UserService 中添加以下三个方法: - 优先从缓存中取值; - 缓存中没有该用户信息时,则将其存入缓存; - 用户信息变更时清除对应缓存数据; ```java /** * 优先从缓存中取值 * @param userId * @return */ private User getCache(int userId) { String redisKey = RedisKeyUtil.getUserKey(userId); return (User) redisTemplate.opsForValue().get(redisKey); } /** * 缓存中没有该用户信息时,则将其存入缓存 * @param userId * @return */ private User initCache(int userId) { User user = userMapper.selectById(userId); String redisKey = RedisKeyUtil.getUserKey(userId); redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS); return user; } /** * 用户信息变更时清除对应缓存数据 * @param userId */ private void clearCache(int userId) { String redisKey = RedisKeyUtil.getUserKey(userId); redisTemplate.delete(redisKey); } ``` 在修改 UserService 中其他方法的逻辑,将上面三个方法加入 ```java /** * 根据 Id 查询用户 * @param id * @return */ public User findUserById (int id) { // return userMapper.selectById(id); User user = getCache(id); // 优先从缓存中查询数据 if (user == null) { user = initCache(id); } return user; } /** * 激活用户 * @param userId 用户 id * @param code 激活码 * @return */ public int activation(int userId, String code) { User user = userMapper.selectById(userId); if (user.getStatus() == 1) { // 用户已激活 return ACTIVATION_REPEAT; } else if (user.getActivationCode().equals(code)) { // 修改用户状态为已激活 userMapper.updateStatus(userId, 1); clearCache(userId); // 用户信息变更,清除缓存中的旧数据 return ACTIVATION_SUCCESS; } else { return ACTIVATION_FAILURE; } } ```