论坛项目进展02
第2章 Spring Boot实践,开发社区登录模块
2.1发送邮件
#对springboot-email的配置
spring.mail.host=smtp.sina.com
spring.mail.port=465
spring.mail.username=nowcoder@sina.com
spring.mail.password=nowcoder123
spring.mail.protocol=smtps
spring.mail.properties.mail.smtp.ssl.enable=true
2.2开发注册功能
在userService里写了register函数用来注册用户,同时写了activation函数用来激活用户。
public int activation(int userId, String code) {
User user = userMapper.selectById(userId);
if (user.getStatus() == 1) {//status为1说明已激活过了
return ACTIVATION_REPEAT;
} else if (user.getActivationCode().equals(code)) {//code等于激活码,激活成功,status设置为1
userMapper.updateStatus(userId, 1);
clearCache(userId);
return ACTIVATION_SUCCESS;
} else {//激活失败
return ACTIVATION_FAILURE;
}
}
2.3会话管理
能用cookie尽量用cookie,session用的越来越少,因为现在大部分是分布式部署,如果在服务器1上存了一个session后。下次浏览器被分配到了服务器2去处理,还要再创建一个session,造成空间浪费。有以下几种解决方法: 1.设置负载均衡的分配策略,一种方式是叫粘性session,当一个浏览器被分配给服务器1去处理,下次访问还分配到服务器1去处理,即一个固定的ip永远分配给同一个服务器去处理。该方法的缺点就是很难保证服务器的负载是均衡的。2.同步session:当某个服务器创建并保存一个session后,会把这个session同步给其他服务器保存,缺点是空间浪费,且服务器之间会产生耦合,且性能下降。2.共享session:单独设置一台服务器专门用来存储session,缺点是这台服务器只有一个,如果宕机则会有很大影响。
所以现在主流的做法是能存到cookie就存cookie里,有敏感数据的不能存的就存到数据库里。数据库可以做一个集群。
缺点就是mysql中的数据在硬盘中,访问硬盘比较慢。所以可以把数据存到redis里。
2.4生成验证码
在config包下创建KaptchaConfig用于配置Kaptcha
在loginController里写getKaptcha函数
然后在前端修改,就可以得到验证码,并且点击刷新验证码也可以刷新
2.5开发登录,退出功能
生成的登录凭证最终要发送一个key给客户端,让它记住,下次提交给服务端时能够识别。因为用户数据包含一些敏感数据,包括用户id,用户名或密码,这些数据不能存到客户端,所以只能存不敏感的ticket。ticket可以存到服务器的session中,也可以存到数据库。这里我们就存到数据库里。将来还会对其进行重构,将其存到redis里。
mysql中的login_ticket表:ticket:凭证,一个随机字符串;status:0-有效1-无效 expired:凭证的过期时间
客户端在cookie中存了ticket凭证后,再次访问服务器后,服务端用cookie中的ticket查询mysql的login_ticket中的数据,可以判断出来这是哪个用户在登录和访问,以及它的status以及过期时间。
在entity报下建一个LoginTicket类,在dao包下建一个LoginTicketMapper
在UserService里写login函数
public Map<String, Object> login(String username, String password, long 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);//将登录凭证插入mysql表中
String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());
redisTemplate.opsForValue().set(redisKey, loginTicket);
map.put("ticket", loginTicket.getTicket());
return map;//将map返回给客户端,这样客户端就能存ticket
}
在UserService里写logout函数,
public void logout(String ticket) {
// loginTicketMapper.updateStatus(ticket, 1);//把status改为1表示无效
String redisKey = RedisKeyUtil.getTicketKey(ticket);
LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
loginTicket.setStatus(1);
redisTemplate.opsForValue().set(redisKey, loginTicket);
}
2.6显示登录信息
每次请求都要走一次这个过程,所以这套逻辑应该用拦截器实现,而不是写多次重复的代码。所以在controller包下建一个interceptor包,再在其中新建一个拦截器,降低耦合
在utils包下建了一个CookieUtil,用来获取某个cookie中的值
public class CookieUtil {
public static String getValue(HttpServletRequest request, String name) {
if (request == null || name == null) {
throw new IllegalArgumentException("参数为空!");
}
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(name)) {
return cookie.getValue();
}
}
}
return null;
}
}
在在utils包下建了一个HostHolder,起到一个容器的作用,持有用户信息,用于代替session对象.而且是线程隔离的,因为服务器是多线程环境的,所以每个客户端访问服务端,服务端都会用单独一个线程去处理请求并持有客户端用户的信息,所以每个线程的用户信息不一样,所以必须使用线程隔离的ThreadLocal对象来存用户信息,而不能用普通的容器来存。
写好拦截器后要配置拦截器,在config包下建WebMvcConfig:
最后在index.html进行修改
2.7账号设置
新建一个UserController,