牛客网项目第二章学习笔记
一、发送邮件
知识点:
- 邮箱设置
- 启用客户端SMTP服务
- Spring Email
- 导入 jar 包
- 邮箱参数配置
- 使用 JavaMailSender 发送邮件
- 模板引擎
- 使用 Thymeleaf 发送 HTML 邮件
1.启用客户端SMTP服务
课程中选择的新浪,我选择的是网易邮箱。将服务开启。
2.Spring Email
导入相关依赖包。mavenrepository官网,用来查找相关包。在pom.xml添加配置。添加如下代码即可。(找包的意义是找到官方给出的配置代码,如果记得模板,也可以自己改,而不用上官网找包)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> <version>2.1.5.RELEASE</version> </dependency>
3.邮箱参数设置
# MailProperties spring.mail.host=smtp.163.com spring.mail.port=465 spring.mail.username=填邮箱名(用来给新注册用户发邮件的邮箱,根据个人情况修改) spring.mail.password=密码(发邮件的邮箱的密码,有同学是授权码而不是登录密码) spring.mail.protocol=smtps spring.mail.properties.mail.smtp.ssl.enable=true
到底是登录密码还是授权码得看具体使用邮箱的政策。比如新浪的就是需要授权码,而网易仍然是登录密码
4.使用JavaMailSender发送邮件
新建一个发邮件的工具类
@Component public class MailClient { private static final Logger logger = LoggerFactory.getLogger(MailClient.class); @Autowired private JavaMailSender mailSender; //直接使用配置文件中的用户名,所以会去配置文件中查找对应参数 @Value("${spring.mail.username}") private String from; public void sendMail(String to,String subject,String content){ try { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message); helper.setFrom(from); helper.setTo(to); helper.setSubject(subject); //不加true说明支持字符文本,加true说明支持html文本 helper.setText(content,true); mailSender.send(helper.getMimeMessage()); } catch (MessagingException e) { logger.error("发送邮件失败"+e.getMessage()); } } }
在发邮箱时,使用一个模板,注册用户都用这个模板发。该模板放在resources下的templates中比较合适
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>邮件示例</title> </head> <body> <p>欢迎你,<span style="color: red" th:text="${username}"/></p> </body> </html>
接下来就是写测试代码了。在专门的Test包下进行测试代码的书写。
@Test public void testHtmlMail() { Context context = new Context(); context.setVariable("username", "sunday");//设置收件人的用户名,注意上面的模板 //找到模板路径 String content = templateEngine.process("/mail/demo", context); //System.out.println(content); mailClient.sendMail("****@qq.com", "HTML", content); }
测试成功!
二、开发注册功能
课程内容:
- 访问注册页面
- 点击顶部区域内的链接,打开注册页面。
- 提交注册数据
- 通过表单提交数据。
- 服务端验证账号是否已存在、邮箱是否已注册。
- 服务端发送激活邮件。
- 激活注册账号
- 点击邮件中的链接,访问服务端的激活服务
1.从dao层开始开发
首先是改静态模板,利用Thymeleaf将index.html和register.html中的静态改为动态,这里改动的是首页和注册两个页面。
2.提交注册数据
首先导入一个常用的包 commons lang。主要是字符串判空等功能。在pom.xml添加依赖。
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.9</version> </dependency>
写一个生成随机字符串的类。因为开发当中总会涉及到随机,比如我有一群头像,注册用户的头像就是随机选取的。那么封装一个随机生成的类,便于复用。
public class CommunityUtil { // 生成随机字符串 public static String generateUUID() { return UUID.randomUUID().toString().replaceAll("-", ""); } // MD5加密(下面的是举例,不是真实情况)(字符串再加一个随机字符串再加密可提高安全性) // hello -> abc123def456 // hello + 3e4a8 -> abc123def456abc public static String md5(String key) { if (StringUtils.isBlank(key)) { return null;//传入空串不处理 } return DigestUtils.md5DigestAsHex(key.getBytes()); } }
3.service层
注册方法的核心代码,dao层之前实现了(user和userMapper),所以直接进入service层的开发。里面会调用dao层去查询数据,如是否用户名是否已存在。
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; } // 验证账号 userMapper是dao层接口 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));//取5个随机生成的字符 //加入到密码中,再一起加密 user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt())); //'0-普通用户; 1-超级管理员; 2-版主;' user.setType(0); //'0-未激活; 1-已激活;' user.setStatus(0); user.setActivationCode(CommunityUtil.generateUUID()); //牛客头像地址0-1000(给你随机选取一个头像,你可以后期自己上传) user.setHeaderUrl(String.format ("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000))); user.setCreateTime(new Date()); //插入后user内会回填id 具体看user-mapper.xml。即添加到数据库中 userMapper.insertUser(user); // 激活邮件(上节课是在test中做测试,现在是在service层实现发邮件功能) Context context = new Context(); context.setVariable("email", user.getEmail()); // http://localhost:8080/community/activation/101/code //满足上面写法的url才允许激活。101是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(), "激活账号", content); return map; }
上面的激活邮件,是有一个激活模板的。activation.html,将对应的静态改为动态。
4.controller层开发
a.对应于注册功能的controller层需要添加的代码。
@Autowired private UserService userService; @RequestMapping(value = "/register",method = RequestMethod.POST) public String register(Model model, User user){ Map<String, Object> map = userService.register(user);//调用service层 if(map==null||map.isEmpty()){ model.addAttribute("msg","注册成功,我们将向您发送一封邮件,请查收并激活账号"); model.addAttribute("target","/index"); return "site/operate-result"; }else{ //注册失败返回注册页面 model.addAttribute("usernameMsg",map.get("usernameMsg")); model.addAttribute("passwordMsg",map.get("passwordMsg")); model.addAttribute("emailMsg",map.get("emailMsg")); return "/site/register"; } }
b.注册成功后,页面的处理
这里设计了一个中间页面site/operate-result.html过渡,然后可以跳转到首页进行登录。
c.注册失败时,页面的处理
重新回到register.html页面
5.激活注册账号
a.创建一个激活的接口,里面设置几个常量。并让UserService实现此接口
public interface CommunityContant { //激活成功 int ACTIVATION_SUCCESS = 0; //重复激活 int ACTIVATION_REPEAT = 1; //激活失败 int ACTIVATION_FAILURE = 2; }
b.serveice层实现接口后,传递给dao层(user)查询。
public int activion(int userId,String code){ User user = userMapper.selectById(userId); if(user==null){ return ACTIVATION_FAILURE; }else if(user.getStatus()==1){ return ACTIVATION_REPEAT; }else if(!code.equals(user.getActivationCode())){ return ACTIVATION_FAILURE; }else{ //设置激活状态 userMapper.updateStatus(userId,1); return ACTIVATION_SUCCESS; } }
c.controller层增加方法。查看是否激活成功,然后向前端返回结果
@RequestMapping(path = "/activation/{userId}/{code}", method = RequestMethod.GET) public String activation (Model model, @PathVariable("userId") int userId, @PathVariable("code") String code) { int result = userService.activation(userId, code); if (result == ACTIVATION_SUCCESS) { model.addAttribute("msg", "激活成功,您的账号已经可以正常使用了!"); model.addAttribute("target", "/login"); } else if (result == ACTIVATION_REPEAT) { model.addAttribute("msg", "无效操作,该账号已经激活过了!"); model.addAttribute("target", "/index"); } else { model.addAttribute("msg", "激活失败,您提供的激活码不正确!"); model.addAttribute("target", "/index"); } return "/site/operate-result"; }
d.处理相关的前端页面。在activation页面进行激活,site/operate-result.html这个过渡页面告诉激活是否成功(模板页面,填充及激活成功或注册成功)。然后跳转至登录页面login。激活失败跳转到index主页。
三、 会话管理
课程内容:简介关于http回话的相关内容,为后续实现登录功能铺垫
- HTTP的基本性质
- HTTP是简单的
- HTTP是可扩展的
- HTTP是无状态的,有会话的
- Cookie
- 是服务器发送到浏览器,并保存在浏览器端的一小块数据。
- 浏览器下次访问该服务器时,会自动携带块该数据,将其发送给服务器。
- Session
- 是JavaEE的标准,用于在服务端记录客户端信息。
- 数据存放在服务端更加安全,但是也会增加服务端的内存压力。
- 服务器分布式部署的时候存放session并没有十分完美的解决方案,所以一般我们都把数据存放进数据库中(redis)解决此问题。
四、生成验证码
课程内容:
- Kaptcha
- 导入 jar 包
- 编写 Kaptcha 配置类
- 生成随机字符、生成图片
1.pom.xml导入相关依赖
<dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency>
2.进行相关配置。
可以配置一个xml文件(spring-context-kaptcha.xml)。或者写一个配置类。这里是选择写配置类。
@Configuration//配置类的注解 public class KaptchaConfig { @Bean public Producer kaptchaProducer(){ Properties properties = new Properties(); properties.setProperty("kaptcha.image.width","100"); properties.setProperty("kaptcha.image.height","40"); properties.setProperty("kaptcha.textproducer.font.size","32"); properties.setProperty("kaptcha,textproducer.font.color","0,0,0"); properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"); properties.setProperty("kaptcha.textproducer.char.length","4"); properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise"); DefaultKaptcha kaptcha = new DefaultKaptcha(); Config config = new Config(properties); kaptcha.setConfig(config); return kaptcha; } }
3.Controller层增加新方法
这个功能不涉及到数据库,所以不用修改dao和service层,在controller层修改即可。注意这个验证码要放在服务端的,以便用来验证用户输入的验证码,为了安全,因此使用session。
@Autowired private Producer kaptchaProducer; @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); //将图片输出给浏览器 response.setContentType("image/png"); try { ServletOutputStream outputStream = response.getOutputStream(); ImageIO.write(image,"png",outputStream); } catch (IOException e) { logger.error("响应验证码获取失败:"+e.getMessage());//记录日志 } }
4.修改前端页面(login.html)
验证码是动态了,因此要做出相关修改。(使用js和Thymeleaf)
五、开发登陆、退出功能
课程内容:
- 访问登录页面
- 点击顶部区域内的链接,打开登录页面。
- 登录
- 验证账号、密码、验证码。
- 成功时,生成登录凭证,发放给客户端。
- 失败时,跳转回登录页。
- 退出
- 将登录凭证修改为失效状态。
- 跳转至网站首页。
1.登录凭证暂时放数据库中,后面将进行重构。
数据库建表,expired是过期时间。最重要的是凭证ticket
CREATE TABLE `login_ticket` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `ticket` varchar(45) NOT NULL, `status` int(11) DEFAULT '0' COMMENT '0-有效; 1-无效;', `expired` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `index_ticket` (`ticket`(20)) ) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;
下面是表的样子。
2.dao层开发
先写一个基础类LoginTicket,再写一个配合使用的LoginTicketMapper的接口。二者共同完成dao层。(Mapper接口的注解和Mapper.xml中都是可以写sql语句的)基础类不实现业务。
//在Mapper接口的注解中写sql语句 @@Repository public interface LoginTicketMapper { @Insert({ "insert into login_ticket(user_id,ticket,status,expired) ", "values(#{userId},#{ticket},#{status},#{expired})" }) @Options(useGeneratedKeys = true, keyProperty = "id") int insertLoginTicket(LoginTicket loginTicket); @Select({ "select id,user_id,ticket,status,expired ", "from login_ticket where ticket=#{ticket}" }) LoginTicket selectByTicket(String ticket); @Update({ "<script>", "update login_ticket set status=#{status} where ticket=#{ticket} ", "<if test=\"ticket!=null\"> ", "and 1=1 ", "</if>", "</script>" }) int updateStatus(@Param("ticket")String ticket, @Param("status")int status); }
3.service层开发登录
登录的过程可以参考注册的相关流程。(这里和之前都省略了service层类名,只记录核心的方法,可以放在一个service类也可以)
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); map.put("ticket", loginTicket.getTicket()); return map; }
4.controller层开发