一、登录注册模块
1、注册功能
1.1、注册流程图
1.2、注册代码
/**
* 用户注册
* @param user
* @return Map<String, Object> 返回错误提示消息,如果返回的 map 为空,则说明注册成功
*/
public Map<String, Object> register(User user) {
Map<String, Object> map = new HashMap<>();
if (user == null) {
throw new IllegalArgumentException("参数不能为空");
}
if (StringUtils.isBlank(user.getUsername())) {
map.put("usernameMsg", "账号不能为空");
return map;
}
if (StringUtils.isBlank(user.getPassword())) {
map.put("passwordMsg", "密码不能为空");
return map;
}
if (StringUtils.isBlank(user.getEmail())) {
map.put("emailMsg", "邮箱不能为空");
return map;
}
// 验证账号是否已存在
User u = userMapper.selectByName(user.getUsername());
if (u != null) {
map.put("usernameMsg", "该账号已存在");
return map;
}
// 验证邮箱是否已存在
u = userMapper.selectByEmail(user.getEmail());
if (u != null) {
map.put("emailMsg", "该邮箱已被注册");
return map;
}
// 注册用户
user.setSalt(CommunityUtil.generateUUID().substring(0, 5)); // salt
user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt())); // 加盐加密
user.setType(0); // 默认普通用户
user.setStatus(0); // 默认未激活
user.setActivationCode(CommunityUtil.generateUUID()); // 激活码
// 随机头像(用户登录后可以自行修改)
user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000)));
user.setCreateTime(new Date()); // 注册时间
userMapper.insertUser(user);
// 给注册用户发送激活邮件
Context context = new Context();
context.setVariable("email", user.getEmail());
// http://localhost:8080/echo/activation/用户id/激活码
String url = domain + contextPath + "/activation/" + user.getId() + "/" + user.getActivationCode();
context.setVariable("url", url);
String content = templateEngine.process("/mail/activation", context);
mailClient.sendMail(user.getEmail(),"激活 Echo 账号", content);
return map;
}
2、登录模块
2.1、登录页面
2.2、登录验证码问题
首先,登录的时候会随机生成验证码,如何把这个验证码和当前用户对应起来,实现验证码的校验呢?
显然,由于这个时候用户还没有登录,我们是没有办法通过用户的 id 来唯一的对应它的验证码的。所以这个时候我们考虑生成一个随机的 id 来暂时的代替这个用户,将其id和对应的验证码暂时存入 Redis 中(60s)。并且在 Cookie中暂时存一份为这个用户生成的随机 id(60s)。
其中生成验证码和进行校验分别是两个URL请求地址。
这样,当用户点击登录按钮后,就会去 Redis中获取这个随机 id和验证码,去Cookie查询对应的验证码,判断用户输入的验证码是否一致。
2.3、登录认证并持有用户状态问题
用户输入用户名和密码并且校验完验证码之后,就登录成功了,那我们如何在一次请求中去保存这个用户的状态?如何回显用户的信息呢?
做法可以设计一个类如下图:
解释一下,每个用户登录成功后,我们都会为其生成一个随机的唯一的登录凭证实体类对象 LoginTicket
(包含用户 id、登录凭证字符串 ticket、是否有效、过期时间),我们把这个登录凭证实体类对象永久的存储在 Redis 中(key 就是登录凭证字符串 ticket)。而所谓登录凭证的无效,就是指用户登出后,这个凭证就会被设置为无效状态;凭证的默认过期时间是 30分。
存储完 LoginTicket
后,我们就可以根据它来获取用户的状态了。我们定义了一个拦截器 LoginTicketInterceptor
,每次请求之前都会从 Redis获取到 ticket,然后根据 ticket 去 Redis 中查看这个用户的登录凭证 LoginTicket
是否过期和是否有效,只有登录凭证有效且没有过期才会执行请求,不然就会跳转到登录界面。
如果该用户的登录凭证有效且没有过期,那我们就可以在本次请求中持有这个用户的信息了。如何持有呢?这里我们考虑使用 ThreadLocal
保存用户信息,ThreadLocal
在每个线程中都创建了一个用户信息副本,也就是说每个线程都可以访问自己内部的用户信息副本变量,从而实现了线程隔离,来看下 HostHolder
类:
所以将登录成功后要保存的信息为:
将生成的凭证保存到Redis上,并且设置过期时间,置state为1,其中key为凭证,value为LoginTicket
类。
然后每次请求首先经过拦截器,通过Cookie获取ticket凭证。凭借ticket凭证从Redis获取LoginTicket
类的信息。如果存在就将通过LoginTicket
类的用户id查询用户信息其保存到ThreadLocal中,否则拦截。
2.4、退出功能
从Redis删除凭证信息,执行ThreadLocal的remove()方法清空用户信息,删除Cookie保存的凭证信息。
2.5、性能优化
因为每次请求需要在拦截器中通过通过Cookie获取凭证,然后去Redis获取LoginTicket
类。如果通过验证则会每次去数据库查询用户信息,这样导致每一次请求访问都会去数据库查询造成巨大的访问压力。
为了避免这种情况,所以拦截器首先去Redis查询用户信息,如果有则直接保存到ThreadLocal,否则再去数据库查询用户信息,再保存到Redis中。
2.5、流程
- 用户登录 —> 生成登录凭证作为key存入 Redis,value是凭证类信息,Cookie 中存一份 ticket凭证
- 每次执行请求之前,拦截器都会通过 Cookie 去 Redis 中查询该用户的登陆凭证是否过期和是否有效。点击记住我可以延长登录凭证的过期时间,用户退出则其登录凭证变为无效状态
- 根据这个登录凭证对应的用户 id,去数据库中查询这个用户信息
- 使用 ThreadLocal 在本次请求中一直持有这个用户信息
- 优化点:每次请求前都需要去数据库查询这个用户信息,访问频率比较高,所以我们考虑把登录成功的用户信息在 Redis 中保存一会,拦截器每次查询前先去 Redis 中查询
流程图如下图: