ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

12.使用 Redis 优化登陆模块

12.使用 Redis 优化登陆模块

目录

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 中,在调用此方法效率就提升了。

缓存一般分为:

  1. 查询 User 的时候,尝试从缓存中取值,如果取到直接使用,如果取不到,则进行初始化缓存数据,相当于重构 findUserId 方法
  2. 改变用户数据(头像、密码等):更新缓存数据或者直接删除缓存(一般使用删除,下次请求访问用户重新查询)
  • 从缓存中取值为 User:传入 UserId,拼接 key;尝试用 Redis 中取值
  • 取不到,则进行初始化缓存数据:数据来源于 MySQL,在 MySQL 查询数据,拼接 key,往 Redis 中存储据
  • 数据变更时清除缓存数据:拼接 key,删除缓存
  • 在 findUserId 方法中调用:首先在缓存中取值,如果为空初始化缓存,最后返回 User
  • 在修改 User 的地方进行缓存清除
  • 在 activation 方法中修改状态为 1,然后清除缓存
  • 在 修改头像路径 updateHeader 方法中:首先更新头像,再去清理缓存
返回列表