1. 发送邮件
- 邮箱设置
- Spring Email
- 导入 jar 包
- 邮箱参数配置
- 使用 JavaMailSender 发送邮件
- 模板引擎
【实战】
我用的QQ邮箱,“设置”—“账户”—开启 “POP3/SMTP服务”
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
application.properties
spring.mail.host=smtp.qq.com
spring.mail.port=465
spring.mail.username=156xxxxxx4@qq.com
spring.mail.password=xxxxxxxxxx
spring.mail.protocol=smtps
spring.mail.properties.mail.smtp.ssl.enable=true
新建 util 包,新建 MailClient 类
package com.nowcoder.community.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
@Component
public class MailClient {
private static final Logger logger = LoggerFactory.getLogger(MailClient.class);
@Resource
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);
helper.setText(content, true);
mailSender.send(helper.getMimeMessage());
} catch (MessagingException e) {
logger.error("发送邮件失败" + e.getMessage());
}
}
}
新建测试类
(运行之后报错了,提示找不到 Alpha… 的一些类,因为我们之前删掉了 Alpha… 的所有类,但是没在 test 目录下删掉引用了 Alpha… 的类,所以可以删掉引用了 Alpha… 的测试类,也可以只删掉其中的 Alpha… 代码)
package com.nowcoder.community;
import com.nowcoder.community.util.MailClient;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest
public class MailTests {
@Resource
private MailClient mailClient;
@Test
public void testTextMail() {
mailClient.sendMail("15xxxx74@qq.com", "Test", "Welcome");
}
}
测试成功!
再测试一下发送 html 邮件
resources/templates/mail 下新建 demo.html,作为邮件模板
<!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}"></span>!</p>
</body>
</html>
写个测试方法
@Resource
private TemplateEngine templateEngine;
@Test
public void testHtmlMail() {
Context context = new Context();
context.setVariable("username", "法外狂徒");
String content = templateEngine.process("/mail/demo", context);
System.out.println(content);
mailClient.sendMail("15xxxxx74@qq.com", "TestHtml", content);
}
运行,成功接收到邮件!由控制台可以看到,content 就是 html 文件的文字版
2. 注册功能
- 访问注册页面
- 提交注册数据
- 通过表单提交数据。
- 服务端验证账号是否已存在、邮箱是否已注册。
- 服务端发送激活邮件。
- 激活注册账号
2.1 访问注册页面
新建 controller
package com.nowcoder.community.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LoginController {
@GetMapping("/register")
public String getRegisterPage() {
return "/site/register";
}
}
修改 site/register.html
2行
<html lang="en" xmlns:th="http://www.thymeleaf.org">
8-9行 绝对路径不经不用改,相对路径需要交给 thymeleaf 接管
<link rel="stylesheet" th:href="@{/css/global.css}" />
<link rel="stylesheet" th:href="@{/css/login.css}" />
最后面两个 script 标签
<script th:src="@{/js/global.js}"></script>
<script th:src="@{/js/register.js}"></script>
我们需要从 首页 点击 跳转到注册页面,所以还需要修改 index.html 27行
<a class="nav-link" th:href="@{/index}">首页</a>
33行
<a class="nav-link" th:href="@{/register}">注册</a>
14行,给 header 标签起个名字,方便复用
<header class="bg-dark sticky-top" th:fragment="header">
修改 site/register.html 15行,使用 index.html 的 header 标签替换自己的 header
<header class="bg-dark sticky-top" th:replace="index::header">
启动程序,访问 http://localhost:8080/community/index,点击 “首页” 和 “注册” 查看效果!
2.2 提交注册数据
pom.xml 导入校验工具
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
application.properties 配个域名
community.path.domain=http://localhost:8080
写个工具类
package com.nowcoder.community.util;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.DigestUtils;
import java.util.UUID;
public class CommunityUtil {
public static String generateUUID () {
return UUID.randomUUID().toString().replaceAll("-", "");
}
public static String md5(String key) {
if (StringUtils.isBlank(key)) return null;
return DigestUtils.md5DigestAsHex(key.getBytes());
}
}
完善 UserService 类
package com.nowcoder.community.service;
import com.nowcoder.community.dao.UserMapper;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.util.CommunityUtil;
import com.nowcoder.community.util.MailClient;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import javax.annotation.Resource;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
@Service
public class UserService {
@Resource
private UserMapper userMapper;
@Resource
private MailClient mailClient;
@Resource
private TemplateEngine templateEngine;
@Value("${community.path.domain}")
private String domain;
@Value("${server.servlet.context-path}")
private String contextPath;
public User findUserById(int id) {
return userMapper.selectById(id);
}
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));
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());
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;
}
}
完善激活码邮件模板 templates / mail /activation.html
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link rel="icon" href="https://static.nowcoder.com/images/logo_87_87.png"/>
<title>牛客网-激活账号</title>
</head>
<body>
<div>
<p>
<b th:text="${email}">xxx@xxx.com</b>, 您好!
</p>
<p>
您正在注册牛客网, 这是一封激活邮件, 请点击
<a th:href="${url}">此链接</a>,
激活您的牛客账号!
</p>
</div>
</body>
</html>
完善 LoginController 类
package com.nowcoder.community.controller;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import javax.annotation.Resource;
import java.util.Map;
@Controller
public class LoginController {
@Resource
private UserService userService;
@GetMapping("/register")
public String getRegisterPage() {
return "/site/register";
}
@PostMapping("/register")
public String register(Model model, User user) {
Map<String, Object> map = userService.register(user);
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";
}
}
}
修改 templates / site / operate-result.html 第2行
<html lang="en" xmlns:th="http://www.thymeleaf.org">
8行
<link rel="stylesheet" th:href="@{/css/global.css}" />
14行
<header class="bg-dark sticky-top" th:replace="index::header">
61行
<div class="main">
<div class="container mt-5">
<div class="jumbotron">
<p class="lead" th:text="${msg}">您的账号已经激活成功,可以正常使用了!</p>
<hr class="my-4">
<p>
系统会在 <span id="seconds" class="text-danger">8</span> 秒后自动跳转,
您也可以点此 <a id="target" th:href="@{${target}}" class="text-primary">链接</a>, 手动跳转!
</p>
</div>
</div>
</div>
修改 templates/site/register.html
<div class="main">
<div class="container pl-5 pr-5 pt-3 pb-3 mt-3 mb-3">
<h3 class="text-center text-info border-bottom pb-3">注 册</h3>
<form class="mt-5" method="post" th:action="@{/register}">
<div class="form-group row">
<label for="username" class="col-sm-2 col-form-label text-right">账号:</label>
<div class="col-sm-10">
<input type="text"
th:class="|form-control ${usernameMsg != null ? 'is-invalid' : ''}|"
th:value="${user != null ? user.username : ''}"
id="username" name="username" placeholder="请输入您的账号!" required>
<div class="invalid-feedback" th:text="${usernameMsg}">
该账号已存在!
</div>
</div>
</div>
<div class="form-group row mt-4">
<label for="password" class="col-sm-2 col-form-label text-right">密码:</label>
<div class="col-sm-10">
<input type="password"
th:class="|form-control ${passwordMsg != null ? 'is-invalid' : ''}|"
th:value="${user != null ? user.password : ''}"
id="password" name="password" placeholder="请输入您的密码!" required>
<div class="invalid-feedback" th:text="${passwordMsg}">
密码长度不能小于8位!
</div>
</div>
</div>
<div class="form-group row mt-4">
<label for="confirm-password" class="col-sm-2 col-form-label text-right">确认密码:</label>
<div class="col-sm-10">
<input type="password" class="form-control"
th:value="${user != null ? user.password : ''}"
id="confirm-password" placeholder="请再次输入密码!" required>
<div class="invalid-feedback">
两次输入的密码不一致!
</div>
</div>
</div>
<div class="form-group row">
<label for="email" class="col-sm-2 col-form-label text-right">邮箱:</label>
<div class="col-sm-10">
<input type="email"
th:class="|form-control ${emailMsg != null ? 'is-invalid' : ''}|"
th:value="${user != null ? user.email : ''}"
id="email" name="email" placeholder="请输入您的邮箱!" required>
<div class="invalid-feedback" th:text="${emailMsg}">
该邮箱已注册!
</div>
</div>
</div>
<div class="form-group row mt-4">
<div class="col-sm-2"></div>
<div class="col-sm-10 text-center">
<button type="submit" class="btn btn-info text-white form-control">立即注册</button>
</div>
</div>
</form>
</div>
</div>
启动程序,先注册一个数据库中存在的用户试试
注册一个有效的:
2.3 激活注册账号
写个接口类,存放常量
package com.nowcoder.community.util;
public interface CommunityConstant {
int ACTIVATION_SUCCESS = 0;
int ACTIVATION_REPEAT = 1;
int ACTIVATION_FAILURE = 2;
}
UserService 类
实现该接口
public class UserService implements CommunityConstant {...}
增加业务方法
public int activation(int userId, String code) {
User user = userMapper.selectById(userId);
if (user.getStatus() == 1) {
return ACTIVATION_REPEAT;
} else if (user.getActivationCode().equals(code)) {
userMapper.updateStatus(userId, 1);
return ACTIVATION_SUCCESS;
} else {
return ACTIVATION_FAILURE;
}
}
LoginController 类 实现接口
public class LoginController implements CommunityConstant {...}
增加方法
@GetMapping("/login")
public String getLoginPage() {
return "/site/login";
}
@GetMapping("/activation/{userId}/{code}")
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";
}
修改 templates /site/login.html 2行
<html lang="en" xmlns:th="http://www.thymeleaf.org">
8-9行
<link rel="stylesheet" th:href="@{/css/global.css}" />
<link rel="stylesheet" th:href="@{/css/login.css}" />
15行
<header class="bg-dark sticky-top" th:replace="index::header">
94行
<img th:src="@{/img/captcha.png}" style="width:100px;height:40px;" class="mr-2"/>
最后一个 script 标签
<script th:src="@{/js/global.js}"></script>
修改 index.html
<a class="nav-link" th:href="@{/login}">登录</a>
启动程序,注册一个账号,点击激活邮件里的链接!
【踩坑】
从首页点击“登录”之后:
500错,基本都是代码有错误,运行期间出了 error,找了半天发现: login.html 里拼错了单词!!大无语,以后这种字符串建议复制。
3. 会话管理
- HTTP的基本性质
- HTTP是简单的
- HTTP是可扩展的
- HTTP是无状态的,有会话的
- Cookie
- 是服务器发送到浏览器,并保存在浏览器端的一小块数据。
- 浏览器下次访问该服务器时,会自动携带块该数据,将其发送给服务器。
- Session
- 是JavaEE的标准,用于在服务端记录客户端信息。
- 数据存放在服务端更加安全,但是也会增加服务端的内存压力。
4. 生成验证码
使用 kaptcha
pom.xml
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
写个配置类
package com.nowcoder.community.Config;
import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
@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", "black");
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;
}
}
LoginController 类
引入属性
public static final Logger logger = LoggerFactory.getLogger(LoginController.class);
@Resource
private Producer kaptchaProducer;
新增方法
@GetMapping("/kaptcha")
public void getKaptcha(HttpServletResponse response, HttpSession session) {
String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);
session.setAttribute("kaptcha", text);
response.setContentType("image/png");
try {
OutputStream os = response.getOutputStream();
ImageIO.write(image, "png", os);
} catch (IOException e) {
logger.error("响应验证码失败:" + e.getMessage());
}
}
启动程序,访问 http://localhost:8080/community/kaptcha,成功显示验证码!
将图片应用到登录页面,templates / site / login.html 94行
<img th:src="@{/kaptcha}" id="kaptcha" style="width:100px;height:40px;" class="mr-2"/>
实现验证码刷新功能 95行
<a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a>
在最后面实现 refresh_kaptcha()
<script>
function refresh_kaptcha() {
var path = CONTEXT_PATH + "/kaptcha?p=" + Math.random();
$("#kaptcha").attr("src", path);
}
</script>
static / js / global.js 的第一行
var CONTEXT_PATH = "/community";
如果不传个参数 p 的话,访问路径没变,访问的又是个图片,一些浏览器可能会自行判定不用发起访问
重新编译程序,访问 http://localhost:8080/community/login ,点击 “刷新验证码”,成功!
5. 登录、退出
5.1 实体类
根据登录凭证表 login_ticket,写一个对应的实体类
package com.nowcoder.community.entity;
import java.util.Date;
public class LoginTicket {
private int id;
private int userId;
private String ticket;
private int status;
private Date expired;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getTicket() {
return ticket;
}
public void setTicket(String ticket) {
this.ticket = ticket;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public Date getExpired() {
return expired;
}
public void setExpired(Date expired) {
this.expired = expired;
}
@Override
public String toString() {
return "LoginTicket{" +
"id=" + id +
", userId=" + userId +
", ticket='" + ticket + '\'' +
", status=" + status +
", expired=" + expired +
'}';
}
}
5.2 dao
package com.nowcoder.community.dao;
import com.nowcoder.community.entity.LoginTicket;
import org.apache.ibatis.annotations.*;
@Mapper
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({
"update login_ticket set status = #{status} where ticket = #{ticket}"
})
int updateStatus(@Param("ticket") String ticket, @Param("status") int status);
}
在 MapperTests 类中先测试一次
@Resource
private LoginTicketMapper loginTicketMapper;
@Test
public void testInsertLoginTicket() {
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(101);
loginTicket.setExpired(new Date(System.currentTimeMillis() + 1000 * 60 * 10));
loginTicket.setTicket("aaa");
loginTicket.setStatus(0);
loginTicketMapper.insertLoginTicket(loginTicket);
}
插入成功!
@Test
public void testSelectLoginTicket() {
LoginTicket loginTicket = loginTicketMapper.selectByTicket("aaa");
System.out.println(loginTicket);
loginTicketMapper.updateStatus("aaa", 1);
loginTicket = loginTicketMapper.selectByTicket("aaa");
System.out.println(loginTicket);
}
查询、更新成功!
5.3 service
UserService
注入 mapper
@Resource
private LoginTicketMapper loginTicketMapper;
增加方法
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 (!password.equals(user.getPassword())) {
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;
}
5.4 controller
在 CommunityConstant 类中定义常量
int DEFAULT_EXPIRED_SECONDS = 3600 * 12;
int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;
LoginController 类
注入属性
@Value("${server.servlet.context-path}")
private String contextPath;
新增方法
形参中的对象类型,会被自动加到 model 中,可以在视图中直接取 java自带的数据类型不会被加到 model 中,如果在视图中想用,可以在 request 对象中取
@PostMapping("/login")
public String login(String username, String password, String code,
boolean rememberme, Model model, HttpSession session,
HttpServletResponse response) {
String kaptcha = (String) session.getAttribute("kaptcha");
if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
model.addAttribute("codeMsg", "验证码不正确!");
return "/site/login";
}
int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
Map<String, Object> map = userService.login(username, password, expiredSeconds);
if (map.containsKey("ticket")) {
Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
cookie.setPath(contextPath);
cookie.setMaxAge(expiredSeconds);
response.addCookie(cookie);
return "redirect:/index";
} else {
model.addAttribute("usernameMsg", map.get("usernameMsg"));
model.addAttribute("passwordMsg", map.get("passwordMsg"));
return "/site/login";
}
}
修改 site / login.html,对于每个框,增加 name 属性,name的值要和 controller 中的 login () 形参名保持一致
${param.username} 等价于 request.getParamter(“username”)
<!-- 内容 -->
<div class="main">
<div class="container pl-5 pr-5 pt-3 pb-3 mt-3 mb-3">
<h3 class="text-center text-info border-bottom pb-3">登 录</h3>
<form class="mt-5" method="post" th:action="@{/login}">
<div class="form-group row">
<label for="username" class="col-sm-2 col-form-label text-right">账号:</label>
<div class="col-sm-10">
<input type="text" th:class="|form-control ${usernameMsg != null ? 'is-invalid' : ''}|"
th:value="${param.username}"
id="username" name="username" placeholder="请输入您的账号!" required>
<div class="invalid-feedback" th:text="${usernameMsg}">
该账号不存在!
</div>
</div>
</div>
<div class="form-group row mt-4">
<label for="password" class="col-sm-2 col-form-label text-right">密码:</label>
<div class="col-sm-10">
<input type="password" th:class="|form-control ${passwordMsg != null ? 'is-invalid' : ''}|"
th:value="${param.password}"
id="password" name="password" placeholder="请输入您的密码!" required>
<div class="invalid-feedback" th:text="${passwordMsg}">
密码长度不能小于8位!
</div>
</div>
</div>
<div class="form-group row mt-4">
<label for="verifycode" class="col-sm-2 col-form-label text-right">验证码:</label>
<div class="col-sm-6">
<input type="text" th:class="|form-control ${codeMsg != null ? 'is-invalid' : ''}|"
id="verifycode" name="code" placeholder="请输入验证码!">
<div class="invalid-feedback" th:text="${codeMsg}">
验证码不正确!
</div>
</div>
<div class="col-sm-4">
<img th:src="@{/kaptcha}" id="kaptcha" style="width:100px;height:40px;" class="mr-2"/>
<a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a>
</div>
</div>
<div class="form-group row mt-4">
<div class="col-sm-2"></div>
<div class="col-sm-10">
<input type="checkbox" id="remember-me" name="rememberme"
th:checked="${param.rememberme}">
<label class="form-check-label" for="remember-me">记住我</label>
<a href="forget.html" class="text-danger float-right">忘记密码?</a>
</div>
</div>
<div class="form-group row mt-4">
<div class="col-sm-2"></div>
<div class="col-sm-10 text-center">
<button type="submit" class="btn btn-info text-white form-control">立即登录</button>
</div>
</div>
</form>
</div>
</div>
启动程序,测试,各项正常!
退出功能
UserService
public void logout(String ticket) {
loginTicketMapper.updateStatus(ticket, 1);
}
LoginController
@GetMapping("/logout")
public String logout(@CookieValue("ticket") String ticket) {
userService.logout(ticket);
return "redirect:/login";
}
index.html 45行
<a class="dropdown-item text-center" th:href="@{/logout}">退出登录</a>
启动,测试,点击“退出登录”,成功返回登录页,数据库中的登录凭证状态值改为了1
6. 显示登录信息
因为需要经常用到 cookie,所以我们封装一个工具类
package com.nowcoder.community.util;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
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;
}
}
UserService 新增方法
public LoginTicket findLoginTicket(String ticket) {
return loginTicketMapper.selectByTicket(ticket);
}
再写个工具类
package com.nowcoder.community.util;
import com.nowcoder.community.entity.User;
import org.springframework.stereotype.Component;
@Component
public class HostHolder {
private ThreadLocal<User> users = new ThreadLocal<>();
public void setUser(User user) {
users.set(user);
}
public User getUser() {
return users.get();
}
public void clear() {
users.remove();
}
}
写个拦截器 LoginTicketInterceptor
package com.nowcoder.community.controller.interceptor;
import com.nowcoder.community.entity.LoginTicket;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.UserService;
import com.nowcoder.community.util.CookieUtil;
import com.nowcoder.community.util.HostHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {
@Resource
private UserService userService;
@Resource
private HostHolder hostHolder;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String ticket = CookieUtil.getValue(request, "ticket");
if (ticket != null) {
LoginTicket loginTicket = userService.findLoginTicket(ticket);
if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
User user = userService.findUserById(loginTicket.getUserId());
hostHolder.setUser(user);
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
User user = hostHolder.getUser();
if (user != null && modelAndView != null) {
modelAndView.addObject("loginUser", user);
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
hostHolder.clear();
}
}
配到配置类中
package com.nowcoder.community.Config;
import com.nowcoder.community.controller.interceptor.LoginTicketInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private LoginTicketInterceptor loginTicketInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginTicketInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
}
修改 index.html,让没登陆时,该显示的显示,不该显示的不显示,登陆之后显示该显示的
<ul class="navbar-nav mr-auto">
<li class="nav-item ml-3 btn-group-vertical">
<a class="nav-link" th:href="@{/index}">首页</a>
</li>
<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser != null}">
<a class="nav-link position-relative" href="site/letter.html">消息<span class="badge badge-danger">12</span></a>
</li>
<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser == null}">
<a class="nav-link" th:href="@{/register}">注册</a>
</li>
<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser == null}">
<a class="nav-link" th:href="@{/login}">登录</a>
</li>
<li class="nav-item ml-3 btn-group-vertical dropdown" th:if="${loginUser != null}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img th:src="${loginUser.headerUrl}" class="rounded-circle" style="width:30px;"/>
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item text-center" href="site/profile.html">个人主页</a>
<a class="dropdown-item text-center" href="site/setting.html">账号设置</a>
<a class="dropdown-item text-center" th:href="@{/logout}">退出登录</a>
<div class="dropdown-divider"></div>
<span class="dropdown-item text-center text-secondary" th:utext="${loginUser.username}">nowcoder</span>
</div>
</li>
</ul>
启动,测试: 未登陆时,不显示 “消息”
登录 hahaha,123456,显示 “消息” 和头像!
7. 账号设置
写个 controller
package com.nowcoder.community.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/user")
public class UserController {
@GetMapping("/setting")
public String getSettingPage() {
return "site/setting";
}
}
修改 setting.html
2行
<html lang="en" xmlns:th="http://www.thymeleaf.org">
8-9行
<link rel="stylesheet" th:href="@{/css/global.css}" />
<link rel="stylesheet" th:href="@{/css/login.css}" />
15行
<header class="bg-dark sticky-top" th:replace="index::header">
191行
<script th:src="@{/js/global.js}"></script>
修改 index.html
<a class="dropdown-item text-center" th:href="@{user/setting}">账号设置</a>
测试: 启动程序,在首页登录之后,点击“账号设置”,成功!
配置头像存储路径 application.properties
community.path.upload=d:/work/data/upload
UserService
public int updateHeader(int userId, String headerUrl) {
return userMapper.updateHeader(userId, headerUrl);
}
UserController
package com.nowcoder.community.controller;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.UserService;
import com.nowcoder.community.util.CommunityUtil;
import com.nowcoder.community.util.HostHolder;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
@Controller
@RequestMapping("/user")
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@Value("${community.path.upload}")
private String uploadPath;
@Value("${community.path.domain}")
private String domain;
@Value("${server.servlet.context-path}")
private String contextPath;
@Resource
private UserService userService;
@Resource
private HostHolder hostHolder;
@PostMapping("/upload")
public String uploadHeader(MultipartFile headerImage, Model model) {
if (headerImage == null) {
model.addAttribute("error", "您还没有选择图片");
return "site/setting";
}
String fileName = headerImage.getOriginalFilename();
String suffix = fileName.substring(fileName.lastIndexOf("."));
if (StringUtils.isBlank(suffix)) {
model.addAttribute("error", "文件格式不正确");
return "site/setting";
}
fileName = CommunityUtil.generateUUID() + suffix;
File dest = new File(uploadPath + "/" + fileName);
try {
headerImage.transferTo(dest);
} catch (IOException e) {
logger.error("上传文件失败:" + e.getMessage());
throw new RuntimeException("上传文件失败,服务器发生异常!", e);
}
User user = hostHolder.getUser();
String headerUrl = domain + contextPath + "/user/header/" + fileName;
userService.updateHeader(user.getId(), headerUrl);
return "redirect:/index";
}
@GetMapping("/header/{fileName}")
public void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response) {
fileName = uploadPath + "/" + fileName;
String suffix = fileName.substring(fileName.lastIndexOf("."));
response.setContentType("image/" + suffix);
try(FileInputStream fis = new FileInputStream(fileName)) {
OutputStream os = response.getOutputStream();
byte[] buffer = new byte[1024];
int b = 0;
while ((b = fis.read(buffer)) != -1) {
os.write(buffer, 0, b);
}
} catch (IOException e) {
logger.error("读取头像失败:" + e.getMessage());
}
}
@GetMapping("/setting")
public String getSettingPage() {
return "site/setting";
}
}
setting.html 67行
<form class="mt-5" method="post" enctype="multipart/form-data" th:action="@{/user/upload}">
71行
<div class="custom-file">
<input type="file" th:class="|custom-file-input ${error != null ? 'is-invalid' : ''}|"
id="head-image" name="headerImage" lang="es" required="">
<label class="custom-file-label" for="head-image" data-browse="文件">选择一张图片</label>
<div class="invalid-feedback" th:text="${error}">
该账号不存在!
</div>
</div>
在 D 盘创建相应的路径 D:\work\data\upload
启动程序,测试
8. 检查登录状态
自定义一个注解类
package com.nowcoder.community.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
UserController 类中 getSettingPage() 和 uploadHeader() 的方法上加上 @LoginRequired 注解,表明这俩方法需要登录才可以访问
写个拦截器,拦截带有该注解的方法
package com.nowcoder.community.controller.interceptor;
import com.nowcoder.community.annotation.LoginRequired;
import com.nowcoder.community.util.HostHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {
@Resource
private HostHolder hostHolder;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
if (loginRequired != null && hostHolder.getUser() == null) {
response.sendRedirect(request.getContextPath() + "/login");
return false;
}
}
return true;
}
}
将拦截器 LoginRequiredInterceptor 配置到配置类 WebMvcConfig 中
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private LoginTicketInterceptor loginTicketInterceptor;
@Resource
private LoginRequiredInterceptor loginRequiredInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginTicketInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
registry.addInterceptor(loginRequiredInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
}
启动,测试,在未登录状态下访问 http://localhost:8080/community/user/setting,直接拦截到登录页!
【课后作业】
自行实现修改密码功能
|