牛客网项目第二章学习笔记
一、发送邮件
知识点:
- 邮箱设置
- 启用客户端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层开发

