02 开发社区登录模块

本贴最后更新于 521 天前,其中的信息可能已经事过境迁

发送邮件

流程

  • 邮箱设置

    • 启用客户端 SMTP 服务
  • Spring Email

    • 导入 jar 包
    • 邮箱参数配置
    • 使用 JavaMailSender 发送邮件
  • 模板引擎

    • 使用 Thymeleaf 发送 HTML 邮件

启用客户端 SMTP 服务

QQ 邮箱在这里开启:

image

导入邮箱包

Spring Boot Starter Mail

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

image

qq 的 smtp 服务

代码实现

测试纯文本

在项目包下 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。

image

image

提交注册数据

配置好注册需要的工具类

复制 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";
    }

image

image

会话管理

介绍

  • HTTP 的基本性质

    • HTTP 是简单的
    • HTTP 是可扩展的
    • HTTP 是无状态的,有会话的
  • Cookie

    • 是服务器发送到浏览器,并保存在浏览器端的一小块数据。
    • 浏览器下次访问该服务器时,会自动携带块该数据,将其发送给服务器。
  • Session

    • 是 JavaEE 的标准,用于在服务端记录客户端信息。
    • 数据存放在服务端更加安全,但是也会增加服务端的内存压力

HTTP 无状态,但并非无会话

HTTP 是无状态的:在同一个连接中,两个执行成功的请求之间是没有关系的。这就带来了一个问题,用户没有办法在同一个网站中进行连贯的交互,比如在电商网站中使用购物车功能。尽管 HTTP 根本上来说是无状态的,但借助 HTTP Cookie 就可使用有状态的会话。利用标头的扩展性,HTTP Cookie 被加进了协议工作流程,每个请求之间就能够创建会话,让每个请求都能共享相同的上下文信息或相同的状态。

HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据。浏览器会存储 cookie 并在下次向同一服务器再发起请求时携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器——如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。

image

写在 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";
    }

image

    //获取cookie
    @RequestMapping(path="/cookie/get",method = RequestMethod.GET)
    @ResponseBody
    public String getCookie(@CookieValue("code") String code){
        System.out.println(code);
        return "get cookie";
    }

image

实例——session 的使用

session 的运行模式

image

设置 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";
    }

image

获取 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。

image

所以我们必须设置服务器的负载均衡策略:

  • Session 粘滞(Sticky Sessions):同一个 IP 分布给同一个服务器,很难保证服务器之间是负载均衡的。
  • Session 复制:利用 Tomcat 等 Web 容器同步复制 Session,一个服务器会同步给其他服务器,这会对服务器产生性能影响,还会产生耦合,对部署有影响。
  • 共享 Session:将 Session 存到指定服务器当中,当其他服务器需要 session 的时候就访问这台服务器。但是由于这台服务器是单体服务器,万一挂机造成的影响就很大。

因此考虑到 session 的不便,我们最好存到 cookie 或者数据库里面,从数据库(Redis)读取数据比读内存速度要慢很多,还是有一定的瓶颈。

image

生成验证码

流程


  • 导入 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 演示:

image

写个测试类测试一下 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");
    }
}

用户数据处理

实现逻辑:

image

创建 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,取对象的:

image

创建 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

image

账号设置

  • 开发步骤

    • 访问账号设置页面
    • 上传头像
    • 获取头像
  • 上传文件

    • 请求:必须是 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 错误代码不会执行。

image

处理后就正常了:

image

修改页面

修改 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​​加上拦截器,就能在没有登录页面的时候防止用户访问到设置页面了。

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3190 引用 • 8214 回帖 • 1 关注
2 操作
bleaach 在 2023-07-20 20:36:01 更新了该帖
bleaach 在 2023-07-19 16:47:02 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...