Echo/docs/220-优化登录模块.md

6.4 KiB
Raw Blame History

优化登录模块


使用 Redis 短暂存储验证码

  • 验证码需要频繁刷新,对性能要求较高
  • 验证码不需永久保存(设置在 Cookie 和 Redis 中的保留时间为 60 s
  • 分布式部署,存在 Session 共享问题

原来我们是将验证码存在 session 中,这里我们将其存入 redis 中

存入 redis 中的数据 key(kaptcha:随机字符串) value(验证码)

用户在输入验证码的时候,还没有进行登录,我们无法通过用户的 id 指明这个验证码是针对谁的。所以这里我们随机生成一个字符串,将其短暂的存入 cookie使用这个字符串来标识这个用户

工具类:生成 Redis 的 Key

/**
 * 生成 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;
    }

}

修改生成验证码的方法:

/**
 * 生成验证码
 * @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

/**
 * 生成 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 中生成/修改/查询登录凭证这三个方法

// 生成登录凭证
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 中添加以下三个方法:

  • 优先从缓存中取值;
  • 缓存中没有该用户信息时,则将其存入缓存;
  • 用户信息变更时清除对应缓存数据;
/**
 * 优先从缓存中取值
 * @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 中其他方法的逻辑,将上面三个方法加入

/**
 * 根据 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;
    }
}