发送邮件
流程
-
邮箱设置
- 启用客户端 SMTP 服务
-
Spring Email
- 导入 jar 包
- 邮箱参数配置
- 使用 JavaMailSender 发送邮件
-
模板引擎
- 使用 Thymeleaf 发送 HTML 邮件
启用客户端 SMTP 服务
QQ 邮箱在这里开启:
导入邮箱包
maven 坐标:
<!-- 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.1.15.RELEASE</version>
</dependency>
试了我好久,老师的 2.1.5 的版本用不了,最新的也不能兼容,随便试了一个 15 的反而可以了。搞 jdk12 和 idea2021 版本又花了我一两个小时。
邮件参数配置
在 application.properties
:
# MailProperties
spring.mail.host=smtp.qq.com
spring.mail.por=465
spring.mail.username=1563893963@qq.com
spring.mail.password=
spring.mail.protocol=smtps
spring.mail.properties.mail.ssl.enable=true
代码实现
测试纯文本
在项目包下 util
下创建 MailClient
类:
package com.nowcoder.community.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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.mail.MessagingException;
import javax.mail.internet.MimeMessage;
/**
* @author 008
* @create 2023-07-17 10:04
*/
@Component
public class MailClient {
private static final Logger logger= LoggerFactory.getLogger(MailClient.class);
@Autowired
private JavaMailSender mailSender;
@Value("${spring.mail.username}")
private String from;
/**
* 发送邮件
* @param to
* @param subject
* @param content
*/
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);//true表示支持html的文本
mailSender.send(helper.getMimeMessage());
} catch (MessagingException e) {
logger.error("发送邮件失败"+e.getMessage());
}
}
}
ctrl+alt+t
将代码用 try-catch
包裹起来。
写一个 MailTests
测试类测试纯文本的邮件:
package com.nowcoder.community;
import com.nowcoder.community.util.MailClient;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class MailTests {
@Autowired
private MailClient mailClient;
@Test
public void testTextMail(){
mailClient.sendMail("931967477@qq.com","Test","Welcome.");
}
}
使用 Thymeleaf 发送 HTML 邮件
在 templates
下新建 mail
文件夹,新建 demo.html
,注意到这里老师又多了两个文件,也顺并拷到这里来。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>邮件实例</title>
</head>
<body>
<p>欢迎你,<span style="color: red;" th:text="${username}"></span> </p>
</body>
</html>
写测试:
//测试发送html的邮件。设置传给模板变量的值
@Test
public void testHtmlMail(){
Context context=new Context();
context.setVariable("username","sunday");
//将content的变量值传给引擎处理,生成我们想要的内容
String content=templateEngine.process("/mail/demo",context);
System.out.println(content);
//发送邮件
mailClient.sendMail("931967477@qq.com","Html",content);
}
开发注册功能
流程
-
访问注册页面
- 点击顶部区域内的链接,打开注册页面。
-
提交注册数据
- 通过表单提交数据。
- 服务端验证账号是否已存在、邮箱是否已注册。
- 服务端发送激活邮件。
-
激活注册账号
- 点击邮件中的链接,访问服务端的激活服务邮箱设置
访问注册页面
处理请求,跳转页面
创建 LoginController
类:
package com.nowcoder.community.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class LoginController {
@RequestMapping(path="/register",method= RequestMethod.GET)
public String getRegisterPage(){
return "/site/register";
}
}
处理页面信息
-
处理
register.html
首尾的相对路径,使用模板引擎,和修改 css<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.3.1/css/bootstrap.min.css" crossorigin="anonymous">
,其他的像之前一样处理。 -
更改
index.html
的首页和注册内容:<a class="nav-link" th:href="@{/index}">首页
<a class="nav-link" th:href="@{/register}">注册
-
给
index
的头部 header 代码取别名,方便后续复用:<header class="bg-dark sticky-top" th:fragment="header">
-
在
register
复用上面的代码:<header class="bg-dark sticky-top" th:replace="index::header">
。index::header
表示 index 目录下的 headder。
提交注册数据
配置好注册需要的工具类
复制 Apache Commons Lang
的 maven 坐标:
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
Lang 为 java.lang API 提供了许多帮助程序实用程序,特别是字符串操作方法,基本数值方法,对象反射,并发,创建和序列化以及系统属性。此外,它还包含对 java.util.Date 的基本增强,以及一系列专用于构建方法的实用程序,例如 hashCode,toString 和 equals。
在 properties 下配置:
# Community
community.path.domain=http://localhost:8080
在 util
下创建 CommunityUtil
类:
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("-","");
}
//md5加密,只能加密,不能解密--->密码+随机字符串,黑客就永远破解不出来了
public static String md5(String key){
if(StringUtils.isBlank(key)){
return null;
}
return DigestUtils.md5DigestAsHex(key.getBytes());
}
}
修改发送激活码的邮件模板
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>
注册用户业务实现
package com.nowcoder.community.service;
import com.mysql.cj.util.StringUtils;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
/**
* @author 008
* @create 2023-07-15 22:19
*/
@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);
}
/**
* 注册用户,并发送邮件激活码
* 返回多个错误信息才用的Map<String,Object>
*/
public Map<String,Object> register(User user) {
Map<String, Object> map = new HashMap<>();
//空值处理
if (user == null) {
throw new IllegalArgumentException("参数不能为空");
}
if (StringUtils.isNullOrEmpty(user.getUsername())) {
map.put("usernameMsg", "账号不能为空!");
return map;
}
if (StringUtils.isNullOrEmpty(user.getPassword())) {
map.put("passwordMsg", "密码不能为空!");
return map;
}
if (StringUtils.isNullOrEmpty(user.getUsername())) {
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);//mybatis会自动生成id
//激活邮件
Context context=new Context();
context.setVariable("email",user.getEmail());
//http://localhost:8080/community/activation/101/code
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;
}
}
在 LoginController 下处理交互
package com.nowcoder.community.controller;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.Map;
@Controller
public class LoginController {
@Autowired
private UserService userService;
@RequestMapping(path="/register",method= RequestMethod.GET)
public String getRegisterPage(){
return "/site/register";
}
@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";
}
}
}
修改页面
注册账号成功后的页面 templates/site/operate-result.html
:
- 头部区域的 css 改成相对路径,用模板引擎。
- 更改操作信息内容:
<p class="lead" th:text="${msg}">您的账号已经激活成功,可以正常使用了!<p></p>
- 更改页面跳转:
您也可以点此 <a id="target" th:href="@{${target}}"
- 复用 index 的代码:
<header class="bg-dark sticky-top" th:replace="index::header">
- 底部有一个 js 文件需要需要路径:
<script src="https://cdn.bootcss.com/bootstrap/4.3.1/js/bootstrap.min.js" crossorigin="anonymous">
register 的页面:
-
更改提交方式:
<form class="mt-5" method="post" th:action="@{/register}">
-
要补上 User 实体类当中相对应的 username 属性:
<input type="text" class="form-control" id="username" name="username" placeholder="请输入您的账号!" required>
。依次修改密码、邮箱。 -
注册失败时返回页面,依然要保存数据,继续展示在页面上,此时的 register 页面的 model 有了 user 数据,th 可以直接访问到,按照下面的方式,依次修改密码、邮箱。
-
<input type="password" class="form-control" id="password" name="password" th:value="${user!=null?user.username:''}"
-
-
修改错误信息内容,按照下面的方式,依次修改密码(确认密码不需要)、邮箱。
<div class="invalid-feedback" th:text="${usernameMsg}"> 该账号已存在! </div>
-
修改错误信息内容展示,只有当错误信息存在才展示,所以我们必须要修改样式:
-
<input type="text" th:class="|form-control ${usernameMsg!=null?'is-valid':''}|" id="username" name="username" th:value="${user!=null?user.username:''}" placeholder="请输入您的账号!" required>
-
激活注册账号
激活有三种结果:1)失败。2)成功。3)重复激活。
定义结果常量
在 util
下创建 CommunityConstant
接口:
package com.nowcoder.community.util;
public interface CommunityConstant {
//激活成功
int ACTIVATION_SUCCESS=0;
//重复激活
int ACTIVATION_REPEAT=1;
//激活失败
int ACTIVATION_FAILURE=2;
}
处理激活业务
在 UserService
下,实现刚刚声明的接口,并增加下列方法:
/**
* 返回激活码验证情况
* @param userId
* @param code
* @return
*/
public int activation(int userId,String code){
//我们可以从路径下获得id和激活码
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
处理激活请求
先实现 CommunityConstant
接口,新增以下方法:
/**
* 处理激活请求,跳转到正确页面
* @param model
* @param userId
* @param code
* @return
*/
@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.html
的相对路径、复用头部、使用引擎。 - 修改
index.html
的登录的路径:<a class="nav-link" th:href="@{/login}">登录
在 Controller 下处理请求进行页面跳转:
@RequestMapping(path="/login",method = RequestMethod.GET)
public String getLoginPage(){
return "/site/login";
}
会话管理
介绍
-
HTTP 的基本性质
- HTTP 是简单的
- HTTP 是可扩展的
- HTTP 是无状态的,有会话的
-
Cookie
- 是服务器发送到浏览器,并保存在浏览器端的一小块数据。
- 浏览器下次访问该服务器时,会自动携带块该数据,将其发送给服务器。
-
Session
- 是 JavaEE 的标准,用于在服务端记录客户端信息。
- 数据存放在服务端更加安全,但是也会增加服务端的内存压力
HTTP 无状态,但并非无会话
HTTP 是无状态的:在同一个连接中,两个执行成功的请求之间是没有关系的。这就带来了一个问题,用户没有办法在同一个网站中进行连贯的交互,比如在电商网站中使用购物车功能。尽管 HTTP 根本上来说是无状态的,但借助 HTTP Cookie 就可使用有状态的会话。利用标头的扩展性,HTTP Cookie 被加进了协议工作流程,每个请求之间就能够创建会话,让每个请求都能共享相同的上下文信息或相同的状态。
HTTP Cookie
HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据。浏览器会存储 cookie 并在下次向同一服务器再发起请求时携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器——如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。
实例——cookie 的使用
cookie 的运行模式
设置 cookie
写在 AlphaController
下:
//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";
}
获取 cookie
//获取cookie
@RequestMapping(path="/cookie/get",method = RequestMethod.GET)
@ResponseBody
public String getCookie(@CookieValue("code") String code){
System.out.println(code);
return "get cookie";
}
实例——session 的使用
session 的运行模式
设置 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";
}
获取 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 单台服务器适合用,多台服务器不用 session。
在分布式部署服务器当中,服务器 1 先有 session,后续浏览器再发送请求,因为服务器 1 忙碌,访问服务器 3,而服务器 3 没有服务器 1 的 session,就只能创建一个新的 session,得不到服务器 1 的 session。
所以我们必须设置服务器的负载均衡策略:
- Session 粘滞(Sticky Sessions):同一个 IP 分布给同一个服务器,很难保证服务器之间是负载均衡的。
- Session 复制:利用 Tomcat 等 Web 容器同步复制 Session,一个服务器会同步给其他服务器,这会对服务器产生性能影响,还会产生耦合,对部署有影响。
- 共享 Session:将 Session 存到指定服务器当中,当其他服务器需要 session 的时候就访问这台服务器。但是由于这台服务器是单体服务器,万一挂机造成的影响就很大。
因此考虑到 session 的不便,我们最好存到 cookie 或者数据库里面,从数据库(Redis)读取数据比读内存速度要慢很多,还是有一定的瓶颈。
生成验证码
流程
- 导入 jar 包
- 编写 Kaptcha 配置类
- 生成随机字符、生成图片
配置 Kaptcha
maven 坐标:
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
写一个 KaptchaConfig
配置类:
package com.nowcoder.community.config;
import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.util.Config;
import com.google.code.kaptcha.impl.DefaultKaptcha;
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","0123456789ABCDEFGHIJKLMNOPQRSTUVWYZ");
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;
}
}
处理验证码请求
//生成验证码
@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("img/png");
try {
OutputStream os = response.getOutputStream();
ImageIO.write(image,"png",os);
} catch (IOException e) {
log.error("响应验证码失败:"+e.getMessage());
}
}
这里的日志是直接在类前加了一个 @slf4j
输出。
修改登录页面
修改 login.html
的验证码部分:
<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>
- 更改成访问请求 kaptcha,加个 id 方便 js 调用。
- 更改点击刷新验证码时,进入 js 方法。
在 global.js
下新增一行:var CONTEXT_PATH="/community";
用来储存路径值,方便后续在任何地方引用该变量。
在底部写 js 方法:
<script>
function refresh_kaptcha(){
var path=CONTEXT_PATH+"/kaptcha?p="+Math.random();
$("#kaptcha").attr("src",path);
}
</script>
加一个参数 p 是为了欺骗服务器,不然刷新一直用原本的路径,服务器就不会继续生成新的验证码。
开发登录退出功能
-
访问登录页面
- 点击顶部区域内的链接,打开登录页面。
-
登录
- 验证账号、密码、验证码。
- 成功时,生成登录凭证,发放给客户端。
- 失败时,跳转回登录页。
-
退出
- 将登录凭证修改为失效状态。
- 跳转至网站首页。
实现登录验证功能
创建登录凭证相关功能模块
LoginTicket
类:
package com.nowcoder.community.entity;
import java.util.Date;
public class LoginTicket {
private int id;
private int userId;
private String ticket;
private int status;
@Override
public String toString() {
return "LoginTicket{" +
"id=" + id +
", userId=" + userId +
", ticket='" + ticket + '\'' +
", status=" + status +
", expired=" + 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;
}
private Date expired;
}
LoginTicketMapper
接口:
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(String ticket,int status);
}
动态 SQL 演示:
写个测试类测试一下 CRUD 是否正常,记得在 MapperTest
下注入 LoginTicketMapper
:
@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);
}
实现登录业务功能
在 UserService
下:
/**
* 验证账号密码,并生成凭证
* @param username
* @param password
* @param expireSeconds
* @return
*/
public Map<String,Object> login(String username,String password,int expireSeconds){
Map<String,Object> map=new HashMap<>();
//空值处理
if(StringUtils.isNullOrEmpty(username)){
map.put("usernameMsg","账号不能为空");
return map;
}
if(StringUtils.isNullOrEmpty(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() + expireSeconds * 1000));
loginTicketMapper.insertLoginTicket(loginTicket);
map.put("ticket",loginTicket.getTicket());
return map;
}
处理请求
在 CommunityConstant
增加两个常量:
//默认状态的登陆凭证的超时时间
int DEFAULT_EXPIRED_SECONDS=3600*12;
//记住状态的登陆凭证超时时间
int REMEMBER_EXPIRED_SECONDS=3600*24*100;
在 LoginController
注入路径值,并新增下列方法:
@Value("${server.servlet.context-path}")
private String contextPath;
@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.isNullOrEmpty(kaptcha)||StringUtils.isNullOrEmpty(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";
}
}
修改 login 网页
-
修改提交方式:
<form class="mt-5" th:action="@{/login}" method="post">
-
每个 input 输入框加上
name
属性:<input type="text" class="form-control is-invalid" name="username" id="username" placeholder="请输入您的账号!" required>
,包括验证码,记住我name="rememberme"
。 -
更改账号和密码的默认显示,即便是登录错误,依然保留原来的账号密码:
<input type="text" class="form-control is-invalid" th:value="${param.username}" name="username"
。${param.username}
表示从 request 中获取 username。验证码不需要设置。
-
更改记住我的默认选项:
<input type="checkbox" name="rememberme" id="remember-me" th:checked="${param.rememberme}">
-
动态提示错误内容:
- 显示错误内容取值:
<div class="invalid-feedback" th:text="${usernameMsg}">该账号不存在!/div>
- 更改显示样式,保证有错误才会出现:
<input type="text" th:class="|form-control ${usernameMsg!=null?'is-invalid':''}|" th:value="${param.username}" name="username" id="username"
- 显示错误内容取值:
退出功能
UserService
:
/**
* 退出功能
*/
public void logout(String ticket){
loginTicketMapper.updateStatus(ticket,1);
}
LoginController
:
@RequestMapping(path = "/logout",method = RequestMethod.GET)
public String logout(@CookieValue("ticket")String ticket){
userService.logout(ticket);
return "redirect:/login";
}
在 index.html
链接退出网页:<a class="dropdown-item text-center" th:href="@{/logout}">退出登录
忘记密码功能(课后作业)
梳理一下忘记密码流程:
- 1)在登录页面点击忘记密码,要在 controller 下跳转到 forget 页面。
- 2)要输入邮箱信息,由工具类**发送验证码请求。**在 a 标签里面发送 get 请求必须带上邮件信息,不然发送到哪里呢?所以要加个 js 文件,写 a 标签的点击事件,以 json 的形式发送数据,又因为需要验证邮箱是否存在,还得写一个回调函数接受服务器的数据,提示浏览器是否正确发送了。
- 3)最后输入密码,提交数据,controller 处理请求,判断信息是否一致,如果正确我们就跳转到登录页面,否则就在当前页面提示错误信息。
访问忘记密码页面
修改 login
的链接:<a href="@{/forget}" class="text-danger float-right">忘记密码?
处理页面跳转:
@RequestMapping(path="/forget",method = RequestMethod.GET)
public String getForgetPage(){
return "/site/forget";
}
处理重置密码的业务
在 userService
下重置密码:
/**
* 重置密码
*/
public Map<String, Object> resetPassword(String email, String password) {
Map<String, Object> map = new HashMap<>();
// 空值处理
if (org.apache.commons.lang3.StringUtils.isBlank(email)) {
map.put("emailMsg", "邮箱不能为空!");
return map;
}
if (org.apache.commons.lang3.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;
}
CommunityUtil
下,用来将 JSON 对象转换为字符串形式,方便接受前端传来的 json 数据:
public static String getJSONString(int code, String msg, Map<String, Object> map) {
JSONObject json = new JSONObject();
json.put("code", code);
json.put("msg", msg);
if (map != null) {
for (String key : map.keySet()) {
json.put(key, map.get(key));
}
}
return json.toJSONString();
}
public static String getJSONString(int code, String msg) {
return getJSONString(code, msg, null);
}
public static String getJSONString(int code) {
return getJSONString(code, null, null);
}
注意这里要导入一个包,用于 json 到字符串的转换:
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.36</version>
</dependency>
处理发送验证码的请求
LoginController
下:
// 获取验证码
@RequestMapping(path = "/forget/code", method = RequestMethod.GET)
@ResponseBody
public String getForgetCode(String email, HttpSession session) {
if (StringUtils.isNullOrEmpty(email)) {
return CommunityUtil.getJSONString(1, "邮箱不能为空!");
}
// 发送邮件
Context context = new Context();
context.setVariable("email", email);
String code = CommunityUtil.generateUUID().substring(0, 4);
context.setVariable("verifyCode", code);
String content = templateEngine.process("/mail/forget", context);
mailClient.sendMail(email, "找回密码", content);
// 保存验证码
session.setAttribute("verifyCode", code);
return CommunityUtil.getJSONString(0);//0表示发送成功
}
// 重置密码
@RequestMapping(path = "/forget/password", method = RequestMethod.POST)
public String resetPassword(String email, String verifyCode, String password, Model model, HttpSession session) {
String code = (String) session.getAttribute("verifyCode");
if (StringUtils.isNullOrEmpty(verifyCode) || StringUtils.isNullOrEmpty(code) || !code.equalsIgnoreCase(verifyCode)) {
model.addAttribute("codeMsg", "验证码错误!");
return "/site/forget";
}
Map<String, Object> map = userService.resetPassword(email, password);
if (map.containsKey("user")) {
return "redirect:/login";
} else {
model.addAttribute("emailMsg", map.get("emailMsg"));
model.addAttribute("passwordMsg", map.get("passwordMsg"));
return "/site/forget";
}
}
修改前端网页代码
forget.js
放在 static/js
下。
$(function(){
$("#verifyCodeBtn").click(getVerifyCode);
});
function getVerifyCode() {
var email = $("#your-email").val();
if(!email) {
alert("请先填写您的邮箱!");
return false;
}
$.get(
CONTEXT_PATH + "/forget/code",
{"email":email},
function(data) {
data = $.parseJSON(data);
if(data.code == 0) {
alert("验证码已发送至您的邮箱,请登录邮箱查看!");
} else {
alert(data.msg);
}
}
);
}
上面的代码是一个获取验证码的函数
getVerifyCode
。它首先从页面中获取邮箱的值,并进行非空判断。如果邮箱为空,则弹出提示框并返回false
。接下来,它通过 AJAX 发送 GET 请求到指定的 URL(
CONTEXT_PATH + "/forget/code"
),并将邮箱作为参数传递。服务器会根据邮箱发送验证码到对应的邮箱。在请求成功后,它将返回的数据解析为 JSON 对象,并根据
data.code
的值进行判断。如果code
为 0,则弹出提示框显示验证码已发送到邮箱。否则,弹出提示框显示data.msg
的值,即错误信息。需要注意的是,上面的代码中用到了
CONTEXT_PATH
,它可能是一个全局变量或者从其他地方获取的值,用于指定请求的 URL。在实际使用时,可以根据具体情况进行适当修改。
修改 login
页面到 forget
页面的链接。
修改 forget
页面:
<form class="mt-5" th:action="@{/forget/password}" method="post">
<div class="form-group row">
<label for="your-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="${param.email}" name="email" id="your-email" placeholder="请输入您的邮箱!" required>
<div class="invalid-feedback" th:text="${emailMsg}">
该邮箱已被注册!
</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':''}|" th:value="${param.verifyCode}" name="verifyCode" id="verifycode" placeholder="请输入验证码!">
<div class="invalid-feedback" th:text="${codeMsg}">
验证码不正确!
</div>
</div>
<div class="col-sm-4">
<a href="javascript:;" id="verifyCodeBtn" class="btn btn-info form-control">获取验证码</a>
</div>
</div>
<div class="form-group row mt-4">
<label for="your-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}" name="password" id="your-password" placeholder="请输入新的密码!" required>
<div class="invalid-feedback" th:text="${passwordMsg}">
密码长度不能小于8位!
</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>
修改忘记密码邮件模板
<!doctype html>
<html lang="en">
<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>
您正在找回牛客账号的密码, 本次操作的验证码为 <b th:text="${verifyCode}">u5s6dt</b> ,
有效时间5分钟, 请您及时进行操作!
</p>
</div>
</body>
</html>
显示登陆信息
-
拦截器示例
- 定义拦截器,实现 HandlerInterceptor
- 配置拦截器,为它指定拦截、排除的路径
-
拦截器应用
- 在请求开始时查询登录用户
- 在本次请求中持有用户数据
- 在模板视图上显示用户数据
- 在请求结束时清理用户数据
拦截器的使用示例
在 Controller
下创建 Interceptor
包,新建 AlphaInterceptor
类:
package com.nowcoder.community.controller.interceptor;
import lombok.extern.slf4j.Slf4j;
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;
@Slf4j
@Component
public class AlphaInterceptor implements HandlerInterceptor {
//在Controller之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.debug("prehandle:"+handler.toString());
return HandlerInterceptor.super.preHandle(request, response, handler);
}
//在Controller之后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.debug("postHandle"+handler.toString());
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
//在templateEngine之后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.debug("aftertHandle"+handler.toString());
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
对比着看瑞吉外卖的拦截看:https://blog.csdn.net/weixin_46066669/article/details/131387232
在 Config
下写配置类 WebMvcConfig
:
package com.nowcoder.community.config;
import com.nowcoder.community.controller.interceptor.AlphaInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AlphaInterceptor alphaInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(alphaInterceptor)
.excludePathPatterns("/**/*.css","/**/*.js","/**/*.png","/**/*.jpg","/**/*.jpeg")
.addPathPatterns("/register","/login");
}
}
用户数据处理
实现逻辑:
创建 CookieUtil
类,方便从 request 中获取 cookie 的 name 的值:
package com.nowcoder.community.util;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
public class CookieUtil {
//从request中获取cookie的name的值
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
类,新增方法 findLoginTicket
,根据凭证内容找到凭证对象:
/**
* 根据凭证内容找到凭证对象
* @param ticket
* @return
*/
public LoginTicket findLoginTicket(String ticket){
return loginTicketMapper.selectByTicket(ticket);
}
创建 HostHolder
类,便于在请求中持有用户,只要请求没有处理完,线程就一直存在,请求处理完后,才会销毁。
package com.nowcoder.community.util;
import com.nowcoder.community.entity.User;
import org.springframework.stereotype.Component;
/**
* 持有用户信息,用于代替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();
}
}
这个 threadLocal 在瑞吉外卖第三章中也用到过 BaseContext 工具类,是以线程为 key,取对象的:
创建 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.beans.factory.annotation.Autowired;
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;
import java.util.Date;
@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中获取凭证
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();
}
}
加拦截器
在 WebMvcConfig
下新增拦截器:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AlphaInterceptor alphaInterceptor;
@Autowired
private LoginTicketInterceptor loginTicketInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(alphaInterceptor)
.excludePathPatterns("/**/*.css","/**/*.js","/**/*.png","/**/*.jpg","/**/*.jpeg")
.addPathPatterns("/register","/login");
registry.addInterceptor(loginTicketInterceptor)
.excludePathPatterns("/**/*.css","/**/*.js","/**/*.png","/**/*.jpg","/**/*.jpeg");
}
}
修改登录信息显示
index.html
:
-
没有登录就不显示
消息、下拉菜单
:<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser!=null}"> <a class="nav-link position-relative" href="site/letter.html">消息
-
登录了就不显示
注册、登录
:<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser==null}"> <a class="nav-link" th:href="@{/register}">注册</a>
-
动态显示头像:
<img th:src="${loginUser.headerUrl}" class="rounded-circle" style="width:30px;"/>
-
动态显示用户名:
<span class="dropdown-item text-center text-secondary" th:utext="${loginUser.username}">nowcoder
账号设置
-
开发步骤
- 访问账号设置页面
- 上传头像
- 获取头像
-
上传文件
- 请求:必须是 POST 请求
- 表单:enctype=“multipart/form-data”
- Spring MVC:通过 MultipartFile 处理上传文件
访问账号设置页面
处理网页 setting
,使用模板引擎,复用头部,处理相对路径。修改 index
的账号设置链接:<a class="dropdown-item text-center" th:href="@{/user/setting}">账号设置
。
创建 UserController
类,处理请求页面跳转:
package com.nowcoder.community.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping(path = "/setting",method = RequestMethod.GET)
public String getSettingPage(){
return "/site/setting";
}
}
上传头像
这里和瑞吉外卖的第四章对比着看。
在配置文件内设置上传路径:community.path.upload=d:/data/upload
UserService
下新增更新头像的方法:
/**
* 更新头像
* @param userId
* @param headerUrl
* @return
*/
public int updateHeader(int userId,String headerUrl){
return userMapper.updateHeader(userId,headerUrl);
}
UserController
下注入属性:
@Value("${community.path.upload}")
private String upload;
@Value("${community.path.domain}")
private String domain;
@Value("${server.servlet.context-path}")
private String contextPath;
@Autowired
private UserService userService;
@Autowired
private HostHolder hostHolder;
UserController
下新增方法:
//上传头像
@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(".")+1);
if(StringUtils.isNullOrEmpty(suffix)){
model.addAttribute("error","文件的格式不正确");
return "/site/setting";
}
//生成随机文件名
fileName= CommunityUtil.generateUUID()+suffix;
//确定文件存放路径
File dest=new File(upload+"/"+fileName);
try {
headerImage.transferTo(dest);
} catch (IOException e) {
log.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=upload+"/"+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) {
e.printStackTrace();
}
}
这里好像有个小问题,按照老师的写文件格式有问题的代码的逻辑是有问题的,文件上传没有后缀的话,lastIndexOf 会返回-1,再放到 substring 函数里面用,就会报错,那个 error 错误代码不会执行。
处理后就正常了:
修改页面
修改 setting
:
-
更改提交方式:
<form class="mt-5" method="post" enctype="multipart/form-data" th:action="@{/user/upload}">
-
更改头像的路径:
<input type="file" class="custom-file-input" id="head-image" name="headerImage" lang="es" required="">
-
增加错误提示:
<div class="invalid-feedback" th:text="${error}"> 头像有误! </div>
-
必要时显示错误提示样式:
<input type="file" th:class="|custom-file-input ${error!=null?'is-invalid':''}|" id="head-image" name="headerImage" lang="es" required="">
修改密码(课后作业)
在更新密码的时候加 md5,判断是否与原密码一致,判断两次新密码输入是否一致。
UserService
:
public Map<String, Object> updatePassword(int userId,String oldPassword, String newPassword){
Map<String, Object> map = new HashMap<>();
//空值处理
if (StringUtils.isNullOrEmpty(oldPassword)) {
map.put("oldPasswordMsg", "原密码不能为空!");
return map;
}
if (StringUtils.isNullOrEmpty(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+userMapper.selectById(userId).getSalt());
userMapper.updatePassword(userId,newPassword);
return map;
}
Orz,对比老师的代码,这里我忘记做空值处理了。
UserController
:
//更新密码
@RequestMapping(path = "/updatePassword",method = RequestMethod.POST)
public String updatePassword(Model model,String oldPassword,String newPassword,String secPassword){
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
:
<form class="mt-5" method="post" th:action="@{/user/updatePassword}">
<div class="form-group row mt-4" >
<label for="old-password" class="col-sm-2 col-form-label text-right"> 原密码:</label>
<div class="col-sm-10">
<input type="password" th:class="|form-control ${oldPasswordMsg!=null?'is-invalid':''}|" id="old-password" name="oldPassword" placeholder="请输入原始密码!" required>
<div class="invalid-feedback" th:text="${oldPasswordMsg}">
密码长度不能小于8位!
</div>
</div>
</div>
<div class="form-group row mt-4">
<label for="new-password" class="col-sm-2 col-form-label text-right" >新密码:</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="new-password" name="newPassword" placeholder="请输入新的密码!" required>
<div class="invalid-feedback">
密码长度不能小于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" th:class="|form-control ${newPasswordMsg!=null?'is-invalid':''}|" id="confirm-password" name="secPassword" placeholder="再次输入新密码!" required>
<div class="invalid-feedback" th:text="${newPasswordMsg}">
两次输入的密码不一致!
</div>
</div>
</div>
老师的前端代码中的两段新密码没有进行验证,也可以后台写,只要多加一个参数,这里就不写了,因为最好在前端处理,只是我不会(笑),很久没写 js 了,如果不一致,密码存入的是 newPassword,不是 ConfirmPassword/secPassword。
检查登录状态
-
使用拦截器
- 在方法前标注自定义注解
- 拦截所有请求,只处理带有该注解的方法
-
自定义注解
- 常用的元注解:
@Target、@Retention、@Document、@Inherited - 如何读取注解:
Method.getDeclaredAnnotations()
Method.getAnnotation(Class annotationClass)
- 常用的元注解:
创建 annotation
包,创建 LoginRequired
注解:
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
两个方法前加上注解。
新建拦截器 LoginRequiredInterceptor
:
package com.nowcoder.community.controller.interceptor;
import com.nowcoder.community.annotation.LoginRequired;
import com.nowcoder.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
@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;
}
}
在 WebMvcConfig
加上拦截器,就能在没有登录页面的时候防止用户访问到设置页面了。