😀如果对你有帮助的话😊
🌺为博主点个赞吧 👍
👍点赞是对博主最大的鼓励😋
💓爱心发射~💓
目录
一、发送邮件
1、启用客户端SMTP服务
bofryuzursekbiab——密码
2、导入jar包
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-mail -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>2.7.0</version>
</dependency>
3、邮箱参数配置
- 访问邮箱域名
- 邮箱端口
- 账号
- 密码
- 协议
- 详细配置
# MailProperties
spring.mail.host=smtp.sina.com
spring.mail.port=465
spring.mail.username=@.com
spring.mail.password=nowcoder123
spring.mail.protocol=smtps
spring.mail.properties.mail.smtp.ssl.enable=true
MailClient
package com.nowcoder.community.util;
@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);
helper.setText(content, true);
mailSender.send(helper.getMimeMessage());
} catch (MessagingException e) {
logger.error("发送邮件失败:" + e.getMessage());
}
}
}
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>
MailTests
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class MailTests {
@Autowired
private MailClient mailClient;
@Autowired
private TemplateEngine templateEngine;
@Test
public void testTextMail() {
mailClient.sendMail("1724206051@qq.com", "TEST", "Welcome.");
}
@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("1724206051@qq.com", "HTML", content);
}
}
总结
JavaMailSender
是Spring Email
的核心组件,负责发送邮件MimeMessage
用于封装邮件的相关信息MimeMessageHelper
用于辅助构建MimeMessage
对象TemplateEngine
是模板引擎,负责格式化HTML
格式的邮件
Spring Boot
对发送邮件提供了支持,可以通过MailProperties
对邮件进行配置
- 可以配置邮件服务器的域名和端口
- 可以配置发件人的账号及密码
- 可以配置发送邮件的协议类型
哪些会被Spring Boot
自动装配到Spring
容器中
JavaMailSender
TemplateEngine
二、开发注册功能
1、访问注册页面
点击顶部区域内的链接,打开注册页面。
修改——thymeleaf
首页—超链接——index.html
每个html
头部复用——index.html
LoginController
返回注册页面
@Controller
public class LoginController {
@Autowired
private UserService userService;
@RequestMapping(path = "/register", method = RequestMethod.GET)
public String getRegisterPage() {
return "/site/register";
}
}
2、提交注册数据
通过表单提交数据。
添加依赖和配置
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
# community
community.path.domain=http://localhost:8080
service层
- 服务端发送激活邮件。
工具类——CommunityUtil
生成随机字符串
给文件生成随机名字
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());
}
}
注入——UserService
- 注入邮件客户端
- 注入模板引擎
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private MailClient mailClient; // 邮件客户端
@Autowired
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<>(); // map实例化
// 空值处理
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());
// http://localhost:8081/community/activation/101/code
// 域名——项目名——功能访问名 + 用户 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
<!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
@RequestMapping(path = "/register", method = RequestMethod.POST)
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";
}
}
激活成功模板——operate-result.html
账号、密码、邮箱错误——返回register.html
默认值的显示
3、激活注册账号
点击邮件中的链接,访问服务端的激活服务。
在service层加一个业务,几种情况:
- 激活成功
- 多次点击激活链接
- 重复激活给提示
- 激活码伪造
三种结果:
- 成功
- 重复激活
- 失败
常量接口——CommunityConstant
public interface CommunityConstant {
/**
* 激活成功
*/
int ACTIVATION_SUCCESS = 0;
/**
* 重复激活
*/
int ACTIVATION_REPEAT = 1;
/**
* 激活失败
*/
int ACTIVATION_FAILURE = 2;
}
UserService
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
@RequestMapping(path = "/login", method = RequestMethod.GET)
public String getLoginPage() {
return "/site/login";
}
//处理请求
// http://localhost:8080/community/activation/101/code
@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";
}
将login
添加到模板
验证码更改
首页更改——index
激活,跳到登录页面
三、会话管理
HTTP 是无状态,有会话的
HTTP 是无状态的:在同一个连接中,两个执行成功的请求之间是没有关系的。这就带来了一个问题,用户没有办法在同一个网站中进行连续的交互,比如在一个电商网站里,用户把某个商品加入到购物车,切换一个页面后再次添加了商品,这两次添加商品的请求之间没有关联,浏览器无法知道用户最终选择了哪些商品。而使用 HTTP 的头部扩展,HTTP Cookies 就可以解决这个问题。把 Cookies 添加到头部中,创建一个会话让每次请求都能共享相同的上下文信息,达成相同的状态。
1、HTTP Cookie
HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。
-
是服务器发送到浏览器,并保存在浏览器端的一小块数据。
-
浏览器下次访问该服务器时,会自动携带块该数据,将其发送给服务器。
-
识别浏览器、记住浏览器
-
下次再发到浏览器,会携带上次数据
好处:弥补HTTP无状态时的情况,让业务得以延续
缺点:
- 存在客户端,不安全
- 增加数据量,影响性能
- 浏览器访问服务器,服务器会产生一个
Cookie
对象 - 服务器-返回——
Cookie
,其中携带数据(默认在响应的头里),浏览器保存以下数据 - 浏览器——服务器,
Cookie
在请求的头里,服务器记住用户
set cookie
// cookie示例
@RequestMapping(path = "/cookie/set", method = RequestMethod.GET)
@ResponseBody
public String setCookie(HttpServletResponse response) {
// 创建cookie
Cookie cookie = new Cookie("code", CommunityUtil.generateUUID());
// 设置cookie生效的范围
cookie.setPath("/community/alpha");
// 设置cookie的生存时间
cookie.setMaxAge(60 * 10);
// 发送cookie
response.addCookie(cookie);
return "set cookie";
}
get cookie
@RequestMapping(path = "/cookie/get", method = RequestMethod.GET)
@ResponseBody
public String getCookie(@CookieValue("code") String code) {
System.out.println(code);
return "get cookie";
}
2、Session
- 是
JavaEE
的标准,用于在服务端记录客户端信息。 - 数据存放在服务端更加安全,但是也会增加服务端的内存压力。
服务器靠什么区分Session
- 服务器——浏览器发送
cookie
,cookie
中携带Session
标识
set Session
// session示例
@RequestMapping(path = "/session/set", method = RequestMethod.GET)
@ResponseBody
public String setSession(HttpSession session) {
session.setAttribute("id", 1);
session.setAttribute("name", "Test");
return "set session";
}
get session
// session示例
@RequestMapping(path = "/session/get", method = RequestMethod.GET)
@ResponseBody
public String getSession(HttpSession session) {
System.out.println(session.getAttribute("id"));
System.out.println(session.getAttribute("name"));
return "get session";
}
为什么在分布式部署下,Session用的少了?实际应用中怎么解决
- 分布式部署——同时部署多台服务器,同时向浏览器提供支持
分布式部署下,有什么问题:
解决方法
1、粘性Session
每个浏览器始终分配给一台服务器去处理,固定ip
给同一个服务器
缺点:难以保证负载均衡,性能不好
2、同步Session
服务器之间同步Session
缺点: 对服务器性能产生影响,服务器之间耦合
3、共享Session
单独有一台服务器来处理Session
缺点:这台服务器挂了都无法工作
4、能存cookie就存cookie,敏感数据可以存到数据库里
优点:
- 很好的共享数据、同步数据
缺点: - 传统的关系型数据库把数据存到硬盘,访问数据到硬盘,性能慢
- 并发量大出现瓶颈
5、 可以存到非关系型数据库 Redis
目前没部署Redis,怎么办?
- 适合存到MySQL,就存
- 不适合存到session
四、生成验证码——Kaptcha
1、Kaptcha
- 导入
jar
包 - 编写
Kaptcha
配置类 - 生成随机字符、生成图片
导入依赖
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
2、KaptchaConfig——定义验证码图片
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", "0,0,0"); // 颜色- 黑色
properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYAZ"); // 随机字符范围
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、LoginController——生成验证码
@Autowired
private Producer kaptchaProducer;
private static final Logger logger = LoggerFactory.getLogger(LoginController.class);
@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 {
OutputStream os = response.getOutputStream();
ImageIO.write(image, "png", os);
} catch (IOException e) {
logger.error("响应验证码失败:" + e.getMessage());
}
}
4、刷新验证码
结果
5、总结
关于Kaptcha的描述
Producer
是Kaptcha
的核心接口DefaultKaptcha
是Kaptcha
核心接口的默认实现类Spring Boot
没有为Kaptcha
提供自动配置
关于使用Kaptcha的描述
- 可以通过
Producer
创建随机的验证码文本 - 可以传入文本,让
Producer
创建对应的验证码图片 - 服务端需要将验证码图片输出给浏览器
关于Kaptcha配置的描述
- 可以配置Kaptcha图片的宽度、高度、字号、颜色
- 可以配置Kaptcha验证码的字符范围、字符个数
五、开发登录、退出功能
访问登录页面
- 点击顶部区域内的链接,打开登录页面。
1、数据库——login_ticket
- id——主键
- user_id——用户id
- ticket——凭证(唯一标识,唯一字符串)
- status——0 有效, 1 无效
- expired——过期时间
2、实体类——LoginTicket
getter and setter
toString
public class LoginTicket {
private int id;
private int userId;
private String ticket;
private int status;
private Date expired;
}
3、LoginTicketMapper——写SQL、通过注解
依据ticket
,来查找
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}"
})
// 以 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(String ticket, int status);
}
4、测试
@Test
public void testInsertLoginTicket() {
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(101);
loginTicket.setTicket("abc");
loginTicket.setStatus(0);
loginTicket.setExpired(new Date(System.currentTimeMillis() + 1000 * 60 * 10));
loginTicketMapper.insertLoginTicket(loginTicket);
}
@Test
public void testSelectLoginTicket() {
LoginTicket loginTicket = loginTicketMapper.selectByTicket("abc");
System.out.println(loginTicket);
loginTicketMapper.updateStatus("abc", 1);
loginTicket = loginTicketMapper.selectByTicket("abc");
System.out.println(loginTicket);
}
登录
- 验证账号、密码、验证码。
- 成功时,生成登录凭证,发放给客户端。
- 失败时,跳转回登录页。
1、业务层——UserService
登录失败的原因:
- 账号没输入、不存在、没激活
用户在页面输入的密码是明文
// 用户在页面输入的密码是明文,存的是加密后的,MD5
// expiredSeconds 多长时间后,凭证过期
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;
}
public void logout(String ticket) {
loginTicketMapper.updateStatus(ticket, 1);
}
2、LoginController
- 验证账号、密码、验证码。
- 成功时,生成登录凭证,发放给客户端。
- 失败时,跳转回登录页。
@RequestMapping(path = "/login", method = RequestMethod.POST)
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路径——整个项目
cookie.setMaxAge(expiredSeconds); // cookie 有效时间
response.addCookie(cookie); // 把 cookie 发送给页面上
return "redirect:/index";
} else {
model.addAttribute("usernameMsg", map.get("usernameMsg"));
model.addAttribute("passwordMsg", map.get("passwordMsg"));
return "/site/login";
}
}
3、登录页面
发送请求时,修改表单提交方式、路径、名字
错误信息展示,给默认值
- 账号
- 密码
账号相关的提示是动态的
退出
- 将登录凭证修改为失效状态。
- 跳转至网站首页。
1、状态标识——UserService
- 将
ticket
改为 1,无效
public void logout(String ticket) {
loginTicketMapper.updateStatus(ticket, 1);
}
2、返回退出页面请求——LoginController
- 返回重新登录页面
@RequestMapping(path = "/logout", method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket) {
userService.logout(ticket);
return "redirect:/login";
}
3、配置退出登录页面的链接——index
六、显示登录信息
拦截器
- 示例定义拦截器,实现
Handlerlnterceptor
- 配置拦截器,为它指定拦截、排除的路径
拦截器应用
- 在请求开始时查询登录用户。
- 在本次请求中持有用户数据
- 在模板视图上显示用户数据
- 在请求结束时清理用户数据
拦截器可以拦截请求,在拦截请求的开始和结束,插入一些代码
1、拦截器
- 示例定义拦截器,实现
Handlerlnterceptor
- 配置拦截器,为它指定拦截、排除的路径
拦截器测试——AlphaInterceptor
package com.nowcoder.community.controller.interceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class AlphaInterceptor implements HandlerInterceptor {
// 日志——debug级别
private static final Logger logger = LoggerFactory.getLogger(AlphaInterceptor.class);
// 在Controller之前执行, 请求之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.debug("preHandle: " + handler.toString()); //debug级别
return true;
}
// 在Controller之后执行, 模板之前执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
logger.debug("postHandle: " + handler.toString());
}
// 在 TemplateEngine 之后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
logger.debug("afterCompletion: " + handler.toString());
}
}
配置类——WebMvcConfig
- 实现接口——
WebMvcConfigurer
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
//拦截器注入
@Autowired
private AlphaInterceptor alphaInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
/*
拦截一切请求,不拦截的 加后边
*/
// /**/*.css _ static 目录下
registry.addInterceptor(alphaInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
.addPathPatterns("/register", "/login"); // 拦截注册 和登录
}
2、拦截器应用
- 在请求开始时查询登录用户。
- 在本次请求中持有用户数据
- 在模板视图上显示用户数据
- 在请求结束时清理用户数据
拦截器——LoginTicketInterceptor
每次请求的过程
request获取Cookie——CookieUtil
- 复用request获取Cookie
- 返回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) {
// cookie 的 name 是不是传入的
if (cookie.getName().equals(name)) {
return cookie.getValue();
}
}
}
return null;
}
}
查询登录凭证——UserService
//查询登录凭证
public LoginTicket findLoginTicket(String ticket) {
return loginTicketMapper.selectByTicket(ticket);
}
找map——HostHolder
- 持有用户信息,用于代替session对象.
/**
* 持有用户信息,用于代替session对象.
*/
@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
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Autowired
private HostHolder hostHolder;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从 cookie 中获取凭证 cookie——ticket
String ticket = CookieUtil.getValue(request, "ticket");
if (ticket != null) {
// 查询凭证
LoginTicket loginTicket = userService.findLoginTicket(ticket);
// 检查凭证是否有效
// 凭证不为空、状态为 0、超时时间晚于登陆时间
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) {
// 将 user 添加到 model
modelAndView.addObject("loginUser", user);
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清理数据
hostHolder.clear();
}
}
配置——WebMvcConfig
@Autowired
private LoginTicketInterceptor loginTicketInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
/*
拦截一切请求,不拦截的 加后边
*/
registry.addInterceptor(loginTicketInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
首页——index
登录 才能看到 消息
没登录 才显示 注册
没登录 才显示 登录
调整登录账号显示
3、运行结果:
4、总结
关于Spring MVC拦截器:
- 拦截器需实现
HandlerInterceptor
接口,而WebMvcConfigurer
接口是MVC
配置类要实现的接口 preHandle
方法在Controller
之前执行,若返回false
,则终止执行后续的请求。postHandle
方法在Controller
之后、模板之前执行。afterCompletion
方法在模板之后执行。
关于配置Spring MVC拦截器
- 配置类需实现
WebMvcConfigurer
接口 - 通过
addInterceptors
方法对拦截器进行配置 - 可以配置忽略拦截的路径,也可以配置希望拦截的路径
关于ThreadLocal的描述
ThreadLocal
采用线程隔离的方式存放数据,可以避免多线程之间出现数据访问冲突。ThreadLocal
提供set
方法,能够以当前线程为key
存放数据。ThreadLocal
提供get
方法,能够以当前线程为key
获取数据。
七、账号设置——上传头像、修改密码
上传文件
- 请求:必须是
POST
请求 - 表单:
enctype="multipart/form-data'
Spring MVC
:通过MultipartFile处理上传文件
开发步骤
- 访问账号设置页面
- 上传头像
- 获取头像
完成这个页面
1、可以访问这个页面
返回访问页面——UserController
@RequestMapping(path = "/setting", method = RequestMethod.GET)
public String getSettingPage() {
return "/site/setting";
}
显示页面——setting.html
修改路径等
index中修改——链接
2、上传头像
- 开放时,是
Windows
- 上线时,是
Linux
配置文件
添加 存储 上传文件的路径
community.path.upload=j:/work/data/upload
UserService
更新修改图像的路径,返回更新行数
public int updateHeader(int userId, String headerUrl) {
return userMapper.updateHeader(userId, headerUrl);
}
UserController
- 上传表单提交为
post
请求 - 项目域名
- 项目名
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; //项目名
@Autowired
private UserService userService;
@Autowired
private HostHolder hostHolder; //取 当前用户是谁
@RequestMapping(path = "/upload", method = RequestMethod.POST)
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);
}
// 更新当前用户的头像的路径(web访问路径)
// http://localhost:8080/community/user/header/xxx.png
User user = hostHolder.getUser();
String headerUrl = domain + contextPath + "/user/header/" + fileName;
userService.updateHeader(user.getId(), headerUrl);
return "redirect:/index";
}
@RequestMapping(path = "/header/{fileName}", method = RequestMethod.GET)
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());
}
}
账号设置——setting.html
3、修改密码
UserService
public User findUserByName(String username) {
return userMapper.selectByName(username);
}
// 重置密码
public Map<String, Object> resetPassword(String email, String password) {
Map<String, Object> map = new HashMap<>();
// 空值处理
if (StringUtils.isBlank(email)) {
map.put("emailMsg", "邮箱不能为空!");
return map;
}
if (StringUtils.isBlank(password)) {
map.put("passwordMsg", "密码不能为空!");
return map;
}
// 验证邮箱
User user = userMapper.selectByEmail(email);
if (user == null) {
map.put("emailMsg", "该邮箱尚未注册!");
return map;
}
// 重置密码
password = CommunityUtil.md5(password + user.getSalt());
userMapper.updatePassword(user.getId(), password);
map.put("user", user);
return map;
}
// 修改密码
public Map<String, Object> updatePassword(int userId, String oldPassword, String newPassword) {
Map<String, Object> map = new HashMap<>();
// 空值处理
if (StringUtils.isBlank(oldPassword)) {
map.put("oldPasswordMsg", "原密码不能为空!");
return map;
}
if (StringUtils.isBlank(newPassword)) {
map.put("newPasswordMsg", "新密码不能为空!");
return map;
}
// 验证原始密码
User user = userMapper.selectById(userId);
oldPassword = CommunityUtil.md5(oldPassword + user.getSalt());
if (!user.getPassword().equals(oldPassword)) {
map.put("oldPasswordMsg", "原密码输入有误!");
return map;
}
// 更新密码
newPassword = CommunityUtil.md5(newPassword + user.getSalt());
userMapper.updatePassword(userId, newPassword);
return map;
}
UserController
// 修改密码
@RequestMapping(path = "/updatePassword", method = RequestMethod.POST)
public String updatePassword(String oldPassword, String newPassword, Model model) {
User user = hostHolder.getUser();
Map<String, Object> map = userService.updatePassword(user.getId(), oldPassword, newPassword);
if (map == null || map.isEmpty()) {
return "redirect:/logout";
} else {
model.addAttribute("oldPasswordMsg", map.get("oldPasswordMsg"));
model.addAttribute("newPasswordMsg", map.get("newPasswordMsg"));
return "/site/setting";
}
}
setting.html
更改
总结
上传文件的必要条件
- 必须在POST请求中上传文件
- 表单的enctype属性必须设置为“multipart/form-data”
关于上传路径与访问路径的描述
- 上传路径可以是本地路径也可以是web路径,
- 访问路径必须是符合HTTP协议的Web路径。
关于MultipartFile
类型的描述
- 一个
MultipartFile
只能封装一个文件 - 通过
MultipartFile
的getOriginalFilename
方法,可以获得原始文件名 - 通过
MultipartFile
的transferTo
方法,可以将文件存入指定位置
八、检查登录状态
- 没有登陆也能访问登录后的页面
- 安全隐患
- 拦截器——不在配置文件中拦截,用注解在方法上拦截
使用拦截器
- 在方法前标注自定义注解
- 拦截所有请求,只处理带有该注解的方法
自定义注解
常用的元注解:
@Target
、——声明自定义注解作用在哪个位置,例如方法上、类上@Retention
、——声明自定义注解的有效时间(编译时、运行时)@Document
、——声明自定义注解生成文档的时候要不要把注解带上去@Inherited
——用于继承,父类有注解,子类是否要继承
如何读取注解:
- 反射
Method.getDeclaredAnnotations()
Method.getAnnotation(class<T>annotationclass)
1、写注解——LoginRequired
新建一个包,写注解
@Target(ElementType.METHOD) // 方法上
@Retention(RetentionPolicy.RUNTIME) //程序运行时有效
public @interface LoginRequired {
// 里边不用写内容,标注解就行
}
2、加上注解——UserController
在需要的方法上,加上注解
3、拦截器——LoginRequiredInterceptor
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {
@Autowired
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;
}
}
4、拦截器配置——WebMvcConfig
拦截器配置——指定生成的路径
好处,拦截谁,就给谁加注解
// 拦截指定方法
@Autowired
private LoginRequiredInterceptor loginRequiredInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
/*
拦截一切请求,不拦截的 加后边
*/
// /**/*.css _ static 目录下
registry.addInterceptor(loginRequiredInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
总结
关于元注解
- @Target用于描述该注解可以作用的目标类型
- @Retention用于描述该注解被保留的时间
- @Document用于描述该注解是否可以生成到文档里
- 比如LoginRequired加上了这个@Inherited,那注解LoginRequired的类的子类也会自动注解上LoginRequired
关于解析注解
- 在程序中,可以通过反射的方式解析注解
- 通过Method对象可以获取某方法上标注的所有注解
- 通过Method对象可以获取某方法上指定类型的注解
- Method对象上还有很多其他的方法,可以获取该方法上标注的注解
在程序中,可以通过哪些方式正确实现重定向
- 在Controller的方法里,通过返回以”redirect”开头的字符串实现重定向
- 在Controller的方法里,通过response对象的sendRedirect方法实现重定向
- 在拦截器中,通过response对象的sendRedirect方法实现重定向