第2章Spring Boot实践,开发社区登录模块【仿牛客网社区论坛项目】

  • 前言
  • 推荐
  • 项目总结
  • 第2章Spring Boot实践,开发社区登录模块
  • 1.发送邮件
  • 配置
  • MailClient
  • 测试
  • 2.开发注册功能
  • 访问注册页面
  • 提交注册数据
  • 激活注册账号
  • 3.会话管理
  • 体验cookie
  • 体验session
  • 4.生成验证码
  • 配置
  • KaptchaConfig
  • 前端
  • 5.开发登录、退出功能
  • LoginTicket
  • LoginTicketMapper
  • 测试Dao
  • UserService
  • LoginController
  • 前端
  • 退出功能
  • 忘记密码
  • 6.显示登录信息
  • 体验拦截器
  • 配置拦截器
  • 测试
  • 登录拦截器
  • 前端
  • 7.账号设置
  • UserController
  • 前端
  • 配置
  • UserService
  • UserController
  • 前端
  • 测试
  • 修改密码
  • 8.检查登录状态
  • LoginRequired
  • LoginRequiredInterceptor
  • WebMvcConfig
  • 最后


前言

2023-4-30 20:42:51

以下内容源自【Java面试项目】
仅供学习交流使用

推荐

仿牛客网项目【面试】

项目总结

第2章Spring Boot实践,开发社区登录模块

1.发送邮件

配置

pom.xml

<!-- spring 邮箱       -->
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-mail -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
            <version>2.7.4</version>
        </dependency>

配置

# MailProperties
spring.mail.host=smtp.qq.com
spring.mail.port=465
spring.mail.username=邮箱地址
spring.mail.password=密钥
spring.mail.protocol=smtps
spring.mail.properties.mail.smtp.ssl.enable=true
MailClient

新增:/util/MailClient

package com.jsss.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;

/**
 * 邮箱工具类
 */
@Component
public class MailClient {
    private static final Logger logger = LoggerFactory.getLogger(MailClient.class);

    @Autowired(required = true)
    private JavaMailSender mailSender;

    @Value("${spring.mail.username}")
    private String from;


    public void sendMail(String to, String subject, String content) {
        try {
            MimeMessage message = mailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message);
            helper.setFrom(from);
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(content, true);
            mailSender.send(helper.getMimeMessage());
        } catch (MessagingException e) {
            logger.error("发送邮件失败" + e.getMessage());
        }

    }
}

新增:/templates/mail/demo.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>邮件实例</title>
</head>
<body>
    <p>欢迎你,<span style="color: red" th:text="${username}"></span>!</p>
</body>
</html>
测试

新增:test:MailTests

package com.jsss.community;

import com.jsss.community.util.MailClient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;


@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class MailTests {

    @Autowired
    private MailClient mailClient;

    @Autowired
    private TemplateEngine templateEngine;

    String to="3063494684@qq.com";
    @Test
    public void testTextMail(){
        mailClient.sendMail(to,"Test","Welcome");
    }

    @Test
    public void testHtmlMail(){
        Context context=new Context();
        context.setVariable("username","sunday");
        String content = templateEngine.process("/mail/demo", context);
        System.out.println(content);
        mailClient.sendMail(to,"HTML",content);
    }
}

测试结果:

第2章Spring Boot实践,开发社区登录模块【仿牛客网社区论坛项目】_html

第2章Spring Boot实践,开发社区登录模块【仿牛客网社区论坛项目】_spring_02

2.开发注册功能

访问注册页面

新增:controller/LoginController

package com.jsss.community.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;


@Controller
public class LoginController {

    @GetMapping (path = "/register")
    public String getRegisterPage(){
        return "site/register";
    }
    
}

修改:index.html
修改头部尾部,用来复用

修改:register.html

修改html标签
修改静态路径
复用index头部

测试:点击顶部区域内的链接,打开注册页面。

提交注册数据

配置:

pom.xml

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>

配置:

# community
community.path.domain=http://localhost:8080
community.path.upload=d:/work/data/upload

新增:/util/CommunityUtil

package com.jsss.community.util;

import org.springframework.util.DigestUtils;

import java.util.UUID;

import org.apache.commons.lang3.StringUtils;

public class CommunityUtil {
    // 生成随机字符串
    public static String generateUUID() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }

    // MD5加密
    // hello -> abc123def456
    // hello + 3e4a8 -> abc123def456abc
    public static String md5(String key) {
        if (StringUtils.isBlank(key)) {
            return null;
        }
        return DigestUtils.md5DigestAsHex(key.getBytes());
    }

}

修改:/templates/mail/activation.html

<!doctype html>
<html lang="en" xmlns:th="http://www.thymleaf.com">
<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}}">http://www.nowcoder.com/activation/abcdefg123456.html</a>,
			激活您的牛客账号!
		</p>
	</div>
</body>
</html>

新增:UserService.register()

package com.jsss.community.service;


import com.jsss.community.dao.UserMapper;
import com.jsss.community.entity.User;
import com.jsss.community.util.CommunityUtil;
import com.jsss.community.util.MailClient;
import org.apache.commons.lang3.StringUtils;
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;

@Service
public class UserService{
    @Autowired
    UserMapper userMapper;

    @Autowired
    private MailClient mailClient;

    @Autowired
    private TemplateEngine templateEngine;

    @Value("${community.path.domain}")
    private String domain;

    @Value("${server.servlet.context-path}")
    private String contextPath;

    public User findUserById(int id){
        return userMapper.selectById(id);
    }

    public Map<String,Object> register(User user){
        Map<String,Object> map=new HashMap<>();
        // 空值处理
        if (user == null) {
            throw new IllegalArgumentException("参数不能为空!");
        }
        if (StringUtils.isBlank(user.getUsername())) {
            map.put("usernameMsg", "账号不能为空!");
            return map;
        }
        if (StringUtils.isBlank(user.getPassword())) {
            map.put("passwordMsg", "密码不能为空!");
            return map;
        }
        if (StringUtils.isBlank(user.getEmail())) {
            map.put("emailMsg", "邮箱不能为空!");
            return map;
        }

        // 验证账号
        User u = userMapper.selectByName(user.getUsername());
        if (u != null) {
            map.put("usernameMsg", "该账号已存在!");
            return map;
        }

        // 验证邮箱
        u = userMapper.selectByEmail(user.getEmail());
        if (u != null) {
            map.put("emailMsg", "该邮箱已被注册!");
            return map;
        }

        // 注册用户
        user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
        user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt()));
        user.setType(0);
        user.setStatus(0);
        user.setActivationCode(CommunityUtil.generateUUID());
        user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000)));
        user.setCreateTime(new Date());
        userMapper.insertUser(user);

        // 激活邮件
        Context context = new Context();
        context.setVariable("email", user.getEmail());
        // http://localhost: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.register()

package com.jsss.community.controller;


@Controller
public class LoginController {

    @Autowired
    UserService userService;

    @GetMapping (path = "/register")
    public String getRegisterPage(){
        return "site/register";
    }

    @PostMapping("/register")
    public String register(Model model, User user){
        Map<String, Object> map = userService.register(user);
        if (map==null|| map.isEmpty()){
            model.addAttribute("msg","注册成功,我们已经向你的邮箱发送了一封激活邮件,请尽快激活");
            model.addAttribute("target","index");

            return "site/operate-result";
        }else {
            model.addAttribute("usernameMsg",map.get("usernameMsg"));
            model.addAttribute("passwordMsg",map.get("passwordMsg"));
            model.addAttribute("emailMsg",map.get("emailMsg"));

            return "site/register";
        }
    }
}

修改:operate-result.html

修改:register.html
修改:表单的提交

测试

  • 通过表单提交数据。
  • 服务端验证账号是否已存在、邮箱是否已注册。
  • 服务端发送激活邮件。
激活注册账号

新增:/util/CommunityConstant

package com.jsss.community.util;

public interface CommunityConstant {

    /**
     * 激活成功
     */
    int ACTIVATION_SUCCESS = 0;

    /**
     * 重复激活
     */
    int ACTIVATION_REPEAT = 1;

    /**
     * 激活失败
     */
    int ACTIVATION_FAILURE = 2;

}

新增:UserService.activation()

public int activation(int userId,String code){
        User user = userMapper.selectById(userId);
        if (user.getStatus() == 1) {
            return ACTIVATION_REPEAT;
        } else if (user.getActivationCode().equals(code)) {
            userMapper.updateStatus(userId, 1);
            return ACTIVATION_SUCCESS;
        } else {
            return ACTIVATION_FAILURE;
        }
    }

新增:LoginController.activation()

@GetMapping (path = "/login")
    public String getLoginPage(){
        return "site/login";
    }
    
    // http://localhost:8080/community/activation/101/code
    @RequestMapping(path = "/activation/{userId}/{code}",method = RequestMethod.GET)
    public String activation(Model model, @PathVariable("userId") int userId,@PathVariable("code") String code){
        int result = userService.activation(userId, code);
        if (result== CommunityConstant.ACTIVATION_SUCCESS){
            model.addAttribute("msg","激活成功,您的账号已经可以正常使用了!");
            model.addAttribute("target","/login");
        }else if (result==CommunityConstant.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
头部:登录路径

测试

  • 点击邮件中的链接,访问服务端的激活服务。

修改:login.html
修改验证码静态图像路径

3.会话管理

体验cookie

新增:AlphaController.setCookie()

新增:AlphaController.getCookie()

//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";

    }

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

测试:

访问:http://localhost:8080/community/alpha/cookie/set

第2章Spring Boot实践,开发社区登录模块【仿牛客网社区论坛项目】_spring_03


访问:http://localhost:8080/community/alpha/cookie/get

第2章Spring Boot实践,开发社区登录模块【仿牛客网社区论坛项目】_面试_04

体验session

新增:AlphaController.setSession()

新增:AlphaController.getSession()

//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";
    }

    @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";
    }

测试:

访问:http://localhost:8080/community/alpha/session/set

第2章Spring Boot实践,开发社区登录模块【仿牛客网社区论坛项目】_html_05


访问:http://localhost:8080/community/alpha/session/get

第2章Spring Boot实践,开发社区登录模块【仿牛客网社区论坛项目】_java_06

4.生成验证码

配置

pom.xml

<!-- 验证码       -->
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
        </dependency>
KaptchaConfig

新建:/config/KaptchaConfig.java

package com.jsss.community.config;

import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

@Configuration
public class KaptchaConfig {

    @Bean
    public Producer kaptchaProducer() {
        Properties properties = new Properties();
        properties.setProperty("kaptcha.image.width", "100");
        properties.setProperty("kaptcha.image.height", "40");
        properties.setProperty("kaptcha.textproducer.font.size", "32");
        properties.setProperty("kaptcha.textproducer.font.color", "0,0,0");
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYAZ");
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");

        DefaultKaptcha kaptcha = new DefaultKaptcha();
        Config config = new Config(properties);
        kaptcha.setConfig(config);
        return kaptcha;
    }
}

新增:LoginController.getKaptcha()

private static final Logger logger= LoggerFactory.getLogger(LoginController.class);

    @Autowired
    private Producer kaptchaProducer;
    
    @RequestMapping(path = "/kaptcha",method = RequestMethod.GET)
    public void getKaptcha(HttpServletResponse response, HttpSession session){
        //生成验证码
        String text = kaptchaProducer.createText();
        BufferedImage image = kaptchaProducer.createImage(text);

        //将验证码存入session
        session.setAttribute("kaptcha",text);

        //将图片输出给浏览器
        response.setContentType("image/png");
        try {
            ServletOutputStream os = response.getOutputStream();
            ImageIO.write(image,"png",os);
        } catch (IOException e) {
            logger.error("响应验证码失败:"+e.getMessage());
        }

    }

测试:

访问:http://localhost:8080/community/kaptcha

第2章Spring Boot实践,开发社区登录模块【仿牛客网社区论坛项目】_spring_07

前端

修改:login.html:验证码

新增:global.js:CONTEXT_PATH

var CONTEXT_PATH="/community"

5.开发登录、退出功能

LoginTicket

新增:/entity/LoginTicket.java

package com.jsss.community.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
@AllArgsConstructor
@NoArgsConstructor
@Data
public class LoginTicket {
    private int id;
    private int userId;
    private String ticket;
    private int status;
    private Date expired;
}
LoginTicketMapper

新增:/dao/LoginTicketMapper.java

package com.jsss.community.dao;


import com.jsss.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);

    //演示动态sql
    @Update({
            "<script>",
            "update login_ticket set status=#{status} where ticket=#{ticket} ",
            "<if test=\"ticket!=null\"> ",
            "and 1=1 ",
            "</if>",
            "</script>"
    })
    int updateStatus(@Param("ticket") String ticket,@Param("status") int status);

}
测试Dao

新增:MapperTest.testInsertLoginTicket()

新增:MapperTest.testSelectLoginTicket()

@Autowired
    private LoginTicketMapper 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

新增:UserService.login()

@Autowired
    private LoginTicketMapper loginTicketMapper;

    public Map<String,Object> login(String username,String password,int expiredSeconds){
        Map<String,Object> map=new HashMap<>();
        // 空值处理
        if (StringUtils.isBlank(username)) {
            map.put("usernameMsg", "账号不能为空!");
            return map;
        }
        if (StringUtils.isBlank(password)) {
            map.put("passwordMsg", "密码不能为空!");
            return map;
        }

        // 验证账号
        User user = userMapper.selectByName(username);
        if (user == null) {
            map.put("usernameMsg", "该账号不存在!");
            return map;
        }

        // 验证状态
        if (user.getStatus() == 0) {
            map.put("usernameMsg", "该账号未激活!");
            return map;
        }

        // 验证密码
        password = CommunityUtil.md5(password + user.getSalt());
        if (!user.getPassword().equals(password)) {
            map.put("passwordMsg", "密码不正确!");
            return map;
        }

        // 生成登录凭证
        LoginTicket loginTicket = new LoginTicket();
        loginTicket.setUserId(user.getId());
        loginTicket.setTicket(CommunityUtil.generateUUID());
        loginTicket.setStatus(0);
        loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
        loginTicketMapper.insertLoginTicket(loginTicket);

        map.put("ticket", loginTicket.getTicket());
        return map;
    }
LoginController

新增:CommunityConstant

  • DEFAULT_EXPIRED_SECONDS
  • REMEMBER_EXPIRED_SECONDS
/**
     * 默认状态的登录凭证的超时时间
     */
    int DEFAULT_EXPIRED_SECONDS = 3600 * 12;

    /**
     * 记住状态的登录凭证超时时间
     */
    int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;

新增:LoginController.login()

@Controller
public class LoginController implements CommunityConstant{
    @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.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
            model.addAttribute("codeMsg", "验证码不正确!");
            return "site/login";
        }

        // 检查账号,密码
        int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
        Map<String, Object> map = userService.login(username, password, expiredSeconds);
        if (map.containsKey("ticket")) {
            Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
            cookie.setPath(contextPath);
            cookie.setMaxAge(expiredSeconds);
            response.addCookie(cookie);
            return "redirect:/index";
        } else {
            model.addAttribute("usernameMsg", map.get("usernameMsg"));
            model.addAttribute("passwordMsg", map.get("passwordMsg"));
            return "site/login";
        }
    }
前端

修改:login.html:表单提交

退出功能

新增:UserService.logout()

public void logout(String ticket) {
        loginTicketMapper.updateStatus(ticket, 1);
    }

新增:UserController.logout()

@RequestMapping(path = "/logout", method = RequestMethod.GET)
    public String logout(@CookieValue("ticket") String ticket) {
        userService.logout(ticket);
        return "redirect:/login";
    }

修改:index.html:头部

忘记密码

开发忘记密码的功能:

  • 点击登录页面上的“忘记密码”链接,打开忘记密码页面。
  • 在表单中输入注册的邮箱,点击获取验证码按钮,服务器为该邮箱发送一份验证码。
  • 在表单中填写收到的验证码及新密码,点击重置密码,服务器对密码进行修改。

修改后端

配置

<!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.58</version>
        </dependency>

新增:CommunityUtil.getJSONString()

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);
    }

新增:UserService.resetPassword()

// 重置密码
    public Map<String, Object> resetPassword(String email, String password) {
        Map<String, Object> map = new HashMap<>();

        // 空值处理
        if (StringUtils.isBlank(email)) {
            map.put("emailMsg", "邮箱不能为空!");
            return map;
        }
        if (StringUtils.isBlank(password)) {
            map.put("passwordMsg", "密码不能为空!");
            return map;
        }

        // 验证邮箱
        User user = userMapper.selectByEmail(email);
        if (user == null) {
            map.put("emailMsg", "该邮箱尚未注册!");
            return map;
        }

        // 重置密码
        password = CommunityUtil.md5(password + user.getSalt());
        userMapper.updatePassword(user.getId(), password);

        map.put("user", user);
        return map;
    }

新增:LoginController.resetPassword()

// 忘记密码页面
    @RequestMapping(path = "/forget", method = RequestMethod.GET)
    public String getForgetPage() {
        return "/site/forget";
    }

    // 获取验证码
    @RequestMapping(path = "/forget/code", method = RequestMethod.GET)
    @ResponseBody
    public String getForgetCode(String email, HttpSession session) {
        if (StringUtils.isBlank(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);
    }

    // 重置密码
    @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.isBlank(verifyCode) || StringUtils.isBlank(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";
        }
    }

修改前端

login.html:忘记密码路径

添加:forget.js

修改:mail/forget.html

修改:site/forget.html

测试:忘记密码功能

6.显示登录信息

体验拦截器

新增:controller/interceptor/AlphaInterceptor.java

package com.jsss.community.controller.interceptor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class AlphaInterceptor implements HandlerInterceptor {
    private static final Logger logger = LoggerFactory.getLogger(AlphaInterceptor.class);

    // 在Controller之前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        logger.debug("preHandle: " + handler.toString());
        return true;
    }

    // 在Controller之后执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        logger.debug("postHandle: " + handler.toString());
    }

    // 在TemplateEngine之后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        logger.debug("afterCompletion: " + handler.toString());
    }
}
配置拦截器

新增:config/WebMvcConfig.java

package com.jsss.community.config;

import com.jsss.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");
        
    }

}
测试

访问:http://localhost:8080/community/login

2023-05-03 15:36:59,431 DEBUG [http-nio-8080-exec-8] c.j.c.c.i.AlphaInterceptor [AlphaInterceptor.java:19] preHandle: com.jsss.community.controller.LoginController#getLoginPage()
2023-05-03 15:36:59,433 DEBUG [http-nio-8080-exec-8] c.j.c.c.i.AlphaInterceptor [AlphaInterceptor.java:26] postHandle: com.jsss.community.controller.LoginController#getLoginPage()
2023-05-03 15:36:59,463 DEBUG [http-nio-8080-exec-8] c.j.c.c.i.AlphaInterceptor [AlphaInterceptor.java:32] afterCompletion: com.jsss.community.controller.LoginController#getLoginPage()
登录拦截器

新增:util/CookieUtil.java

package com.jsss.community.util;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

public class CookieUtil {

    public static String getValue(HttpServletRequest request, String name) {
        if (request == null || name == null) {
            throw new IllegalArgumentException("参数为空!");
        }

        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(name)) {
                    return cookie.getValue();
                }
            }
        }

        return null;
    }

}

新增:UserService.findLoginTicket()

public LoginTicket findLoginTicket(String ticket) {
        return loginTicketMapper.selectByTicket(ticket);
    }

视频38:58左右

HostUser:
在本次请求中持有用户
响应请求是多线程的
解决存储的并发性
使用ThreadLocal解决
每一个线程,有一份数据

新增:util/HostHolder.java

package com.jsss.community.util;

import com.jsss.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();
    }

}

新增:interceptor/LoginTicketInterceptor.java

package com.jsss.community.controller.interceptor;

import com.jsss.community.entity.LoginTicket;
import com.jsss.community.entity.User;
import com.jsss.community.service.UserService;
import com.jsss.community.util.CookieUtil;
import com.jsss.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;

    //Controller执行之前,拿到登录用户
    @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;
    }

    //Controller执行之后:把loginUser返回mav
    @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:登录拦截器

package com.jsss.community.config;

@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:头部

7.账号设置

UserController

新建:controller/UserController.java

package com.jsss.community.controller;

import com.jsss.community.util.CommunityConstant;
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 implements CommunityConstant {

    @RequestMapping(path = "/setting",method = RequestMethod.GET)
    public String getSettingPage(){
        return "/site/setting";
    }
    

}
前端

修改:setting.html

  • html标签
  • 静态路径

修改:index.html

  • 头部:账号设置路径

测试

  • 点击:账号设置
配置
community.path.upload=d:/work/data/upload
UserService

新增:UserService.updateHeader()

public int updateHeader(int userId,String headUrl){
        return userMapper.updateHeader(userId,headUrl);
    }
UserController

新增:UserController.uploadHeader()

新增:UserController.getHeader()

package com.jsss.community.controller;


@Controller
@RequestMapping("/user")
public class UserController implements CommunityConstant {

    private static Logger logger= LoggerFactory.getLogger(UserController.class);

    @Value("${community.path.upload}")
    private String uploadPath;

    @Value("${community.path.domain}")
    private String domain;

    @Value("${server.servlet.context-path}")
    private String contextPath;

    @Autowired
    private UserService userService;

    @Autowired
    private HostHolder hostHolder;

    @RequestMapping(path = "/setting",method = RequestMethod.GET)
    public String getSettingPage(){
        return "/site/setting";
    }
    
    @RequestMapping(path = "/upload", method = RequestMethod.POST)
    public String uploadHeader(MultipartFile headerImage, Model model) {
        if (headerImage == null) {
            model.addAttribute("error", "您还没有选择图片!");

            return "site/setting";
        }

        String fileName = headerImage.getOriginalFilename();
        String suffix = fileName.substring(fileName.lastIndexOf("."));
        if (StringUtils.isBlank(suffix)) {
            model.addAttribute("error", "文件的格式不正确!");

            return "site/setting";
        }

        // 生成随机文件名
        fileName = CommunityUtil.generateUUID() + suffix;
        // 确定文件存放的路径
        File dest = new File(uploadPath + "/" + fileName);
        try {
            // 存储文件
            headerImage.transferTo(dest);
        } catch (IOException e) {
            logger.error("上传文件失败: " + e.getMessage());
            throw new RuntimeException("上传文件失败,服务器发生异常!", e);
        }

        // 更新当前用户的头像的路径(web访问路径)
        // http://localhost:8080/community/user/header/xxx.png
        User user = hostHolder.getUser();
        String headerUrl = domain + contextPath + "/user/header/" + fileName;
        userService.updateHeader(user.getId(), headerUrl);

        return "redirect:/index";
    }
    
    @RequestMapping(path = "/header/{fileName}", method = RequestMethod.GET)
    public void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response) {
        // 服务器存放路径
        fileName = uploadPath + "/" + fileName;
        // 文件后缀
        String suffix = fileName.substring(fileName.lastIndexOf("."));
        // 响应图片
        response.setContentType("image/" + suffix);
        try (
                FileInputStream fis = new FileInputStream(fileName);
                OutputStream os = response.getOutputStream();
        ) {
            byte[] buffer = new byte[1024];
            int b = 0;
            while ((b = fis.read(buffer)) != -1) {
                os.write(buffer, 0, b);
            }
        } catch (IOException e) {
            logger.error("读取头像失败: " + e.getMessage());
        }
    }
}
前端

修改:setting.html:修改头像

测试

访问:账户设置

测试:上传头像

图片下载到:D:\work\data\upload

修改密码

新增:UserService.updatePassword()

// 修改密码
    public Map<String, Object> updatePassword(int userId, String oldPassword, String newPassword) {
        Map<String, Object> map = new HashMap<>();

        // 空值处理
        if (StringUtils.isBlank(oldPassword)) {
            map.put("oldPasswordMsg", "原密码不能为空!");
            return map;
        }
        if (StringUtils.isBlank(newPassword)) {
            map.put("newPasswordMsg", "新密码不能为空!");
            return map;
        }

        // 验证原始密码
        User user = userMapper.selectById(userId);
        oldPassword = CommunityUtil.md5(oldPassword + user.getSalt());
        if (!user.getPassword().equals(oldPassword)) {
            map.put("oldPasswordMsg", "原密码输入有误!");
            return map;
        }

        // 更新密码
        newPassword = CommunityUtil.md5(newPassword + user.getSalt());
        userMapper.updatePassword(userId, newPassword);

        return map;
    }

新增:UserController.updatePassword()

@RequestMapping(path = "/update_password", method = RequestMethod.POST)
    public String updatePassword(@CookieValue("ticket") String ticket, String oldPassword, String password, Model model) {
        User user=hostHolder.getUser();
        int userId=user.getId();
        oldPassword=CommunityUtil.md5(oldPassword+user.getSalt());
        if (user.getPassword().equals(oldPassword)&&!StringUtils.isBlank(password)){
            password=CommunityUtil.md5(password+user.getSalt());
            if (userService.updatePassword(userId,password)>0){
                model.addAttribute("msg","修改密码成功");
                model.addAttribute("target","/login");

                //取消登录状态
                userService.logout(ticket);

                return "site/operate-result";
            }
        }
        model.addAttribute("msg","修改密码失败");
        model.addAttribute("target","/user/setting");

        return "site/operate-result";

    }

修改:前端

<!-- 修改密码 -->
				<h6 class="text-left text-info border-bottom pb-2 mt-5">修改密码</h6>
				<form class="mt-5" th:action="@{/user/updatePassword}" method="post">
					<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':''}|"
								   name="oldPassword" th:value="${param.oldPassword}" id="old-password" 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" th:class="|form-control ${newPasswordMsg!=null?'is-invalid':''}|"
								   name="newPassword" th:value="${param.newPassword}" id="new-password" placeholder="请输入新的密码!" required>
							<div class="invalid-feedback" th:text="${newPasswordMsg}">
								密码长度不能小于8位!
							</div>							
						</div>
					</div>
					<div class="form-group row mt-4">
						<label for="confirm-password" class="col-sm-2 col-form-label text-right">确认密码:</label>
						<div class="col-sm-10">
							<input type="password" class="form-control" th:value="${param.newPassword}" id="confirm-password" placeholder="再次输入新密码!" required>
							<div class="invalid-feedback">
								两次输入的密码不一致!
							</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>

8.检查登录状态

LoginRequired

新增:/annotation/LoginRequired.java

package com.jsss.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

给需要登录的功能添加注解

  • 账号设置功能
LoginRequiredInterceptor

新增:interceptor/LoginRequiredInterceptor.java

package com.jsss.community.controller.interceptor;

import com.jsss.community.annotation.LoginRequired;
import com.jsss.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
    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

修改:WebMvcConfig

添加登录拦截

@Autowired
    private LoginRequiredInterceptor loginRequiredInterceptor;


        registry.addInterceptor(loginRequiredInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");

测试

访问:http://localhost:8080/community/user/setting

会跳到登录页面

最后

2023-7-30 16:26:24

这篇博客能写好的原因是:站在巨人的肩膀上

这篇博客要写好的目的是:做别人的肩膀

开源:为爱发电

学习:为我而行