【牛客讨论区】第二章:社区登录模块

1. 发送邮件

  • 邮箱设置
    • 启用客户端SMTP服务
  • Spring Email
    • 导入 jar 包
    • 邮箱参数配置
    • 使用 JavaMailSender 发送邮件
  • 模板引擎
    • 使用 Thymeleaf 发送 HTML 邮件

【实战】

我用的QQ邮箱,“设置”—“账户”—开启 “POP3/SMTP服务”

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

application.properties

# MailProperties
spring.mail.host=smtp.qq.com
spring.mail.port=465
spring.mail.username=156xxxxxx4@qq.com
# 密码这里填的是授权码
spring.mail.password=xxxxxxxxxx
spring.mail.protocol=smtps
spring.mail.properties.mail.smtp.ssl.enable=true

新建 util 包,新建 MailClient 类

package com.nowcoder.community.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;

@Component
public class MailClient {

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

    @Resource
    private JavaMailSender mailSender;

    @Value("${spring.mail.username}") //我一开始写成 $(),找了半天错!
    private String from;

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

新建测试类

(运行之后报错了,提示找不到 Alpha… 的一些类,因为我们之前删掉了 Alpha… 的所有类,但是没在 test 目录下删掉引用了 Alpha… 的类,所以可以删掉引用了 Alpha… 的测试类,也可以只删掉其中的 Alpha… 代码)

package com.nowcoder.community;

import com.nowcoder.community.util.MailClient;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest
public class MailTests {

    @Resource
    private MailClient mailClient;

    @Test
    public void testTextMail() {
        mailClient.sendMail("15xxxx74@qq.com", "Test", "Welcome");
    }
}

测试成功!

再测试一下发送 html 邮件

resources/templates/mail 下新建 demo.html,作为邮件模板

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

写个测试方法

@Resource
private TemplateEngine templateEngine;

@Test
public void testHtmlMail() {
	//org.thymeleaf.context.Context;
    Context context = new Context();
    context.setVariable("username", "法外狂徒");
    String content = templateEngine.process("/mail/demo", context);
    System.out.println(content);

    mailClient.sendMail("15xxxxx74@qq.com", "TestHtml", content);
}

运行,成功接收到邮件!由控制台可以看到,content 就是 html 文件的文字版

在这里插入图片描述

2. 注册功能

  • 访问注册页面
    • 点击顶部区域内的链接,打开注册页面。
  • 提交注册数据
    • 通过表单提交数据。
    • 服务端验证账号是否已存在、邮箱是否已注册。
    • 服务端发送激活邮件。
  • 激活注册账号
    • 点击邮件中的链接,访问服务端的激活服务。

2.1 访问注册页面

新建 controller

package com.nowcoder.community.controller;

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

@Controller
public class LoginController {

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

修改 site/register.html

2行

<html lang="en" xmlns:th="http://www.thymeleaf.org">

8-9行
绝对路径不经不用改,相对路径需要交给 thymeleaf 接管

<link rel="stylesheet" th:href="@{/css/global.css}" />
<link rel="stylesheet" th:href="@{/css/login.css}" />

最后面两个 script 标签

<script th:src="@{/js/global.js}"></script>
<script th:src="@{/js/register.js}"></script>

我们需要从 首页 点击 跳转到注册页面,所以还需要修改 index.html
27行

<a class="nav-link" th:href="@{/index}">首页</a>

33行

<a class="nav-link" th:href="@{/register}">注册</a>

14行,给 header 标签起个名字,方便复用

<!-- 头部 -->
<header class="bg-dark sticky-top" th:fragment="header">

修改 site/register.html
15行,使用 index.html 的 header 标签替换自己的 header

<!-- 头部 -->
<header class="bg-dark sticky-top" th:replace="index::header">

启动程序,访问 http://localhost:8080/community/index,点击 “首页” 和 “注册” 查看效果!

2.2 提交注册数据

pom.xml 导入校验工具

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.9</version>
</dependency>

application.properties
配个域名

# community
community.path.domain=http://localhost:8080

写个工具类

package com.nowcoder.community.util;

import org.apache.commons.lang3.StringUtils;
import org.springframework.util.DigestUtils;

import java.util.UUID;

public class CommunityUtil {

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

    //MD5加密
    public static String md5(String key) {
        if (StringUtils.isBlank(key)) return null;
        return DigestUtils.md5DigestAsHex(key.getBytes());
    }
}

完善 UserService 类

package com.nowcoder.community.service;

import com.nowcoder.community.dao.UserMapper;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.util.CommunityUtil;
import com.nowcoder.community.util.MailClient;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import javax.annotation.Resource;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

@Service
public class UserService {

    @Resource
    private UserMapper userMapper;

    @Resource
    private MailClient mailClient;

    @Resource
    private TemplateEngine templateEngine;

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

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

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

    public Map<String, Object> register(User user) {
        Map<String, Object> map = new HashMap<>();
        //空值校验
        if (user == null) {
            throw new IllegalArgumentException("参数不能为空!");
        }
        if (StringUtils.isBlank(user.getUsername())) {
            map.put("usernameMsg", "账号不能为空!");
            return map;
        }
        if (StringUtils.isBlank(user.getPassword())) {
            map.put("passwordMsg", "密码不能为空!");
            return map;
        }
        if (StringUtils.isBlank(user.getEmail())) {
            map.put("emailMsg", "邮箱不能为空!");
            return map;
        }
        //验证账号
        User u = userMapper.selectByName(user.getUsername());
        if (u != null) {
            map.put("usernameMsg", "该账号已存在!");
            return map;
        }
        //验证邮箱
        u = userMapper.selectByEmail(user.getEmail());
        if (u != null) {
            map.put("emailMsg", "该邮箱已被注册!");
            return map;
        }
        //到这里才可以正常注册
        user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
        user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt()));
        user.setType(0);//默认普通用户
        user.setStatus(0);
        user.setActivationCode(CommunityUtil.generateUUID());//激活码
        //%d占位符,随件选择牛客网图库的一张图片
        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/userId/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;
    }
}

完善激活码邮件模板
templates / mail /activation.html

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link rel="icon" href="https://static.nowcoder.com/images/logo_87_87.png"/>
    <title>牛客网-激活账号</title>
</head>
<body>
	<div>
		<p>
			<b th:text="${email}">xxx@xxx.com</b>, 您好!
		</p>
		<p>
			您正在注册牛客网, 这是一封激活邮件, 请点击 
			<a th:href="${url}">此链接</a>,
			激活您的牛客账号!
		</p>
	</div>
</body>
</html>

完善 LoginController 类

package com.nowcoder.community.controller;

import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import javax.annotation.Resource;
import java.util.Map;

@Controller
public class LoginController {

    @Resource
    private UserService userService;

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

    @PostMapping("/register")
    public String register(Model model, User user) {//user会被自动添加到 model中
        Map<String, Object> map = userService.register(user);
        if (map == null || map.isEmpty()) { //说明注册成功
            model.addAttribute("msg", "注册成功!请查收激活邮件!");
            model.addAttribute("target", "/index");
            return "/site/operate-result";
        } else {
            model.addAttribute("usernameMsg", map.get("usernameMsg"));
            model.addAttribute("passwordMsg", map.get("passwordMsg"));
            model.addAttribute("emailMsg", map.get("emailMsg"));
            return "/site/register";
        }
    }
}

修改 templates / site / operate-result.html
第2行

<html lang="en" xmlns:th="http://www.thymeleaf.org">

8行

<link rel="stylesheet" th:href="@{/css/global.css}" />

14行

<!-- 头部 -->
<header class="bg-dark sticky-top" th:replace="index::header">

61行

<!-- 内容 -->
<div class="main">
	<div class="container mt-5">
		<div class="jumbotron">
			<p class="lead" th:text="${msg}">您的账号已经激活成功,可以正常使用了!</p>
			<hr class="my-4">
			<p>
				系统会在 <span id="seconds" class="text-danger">8</span> 秒后自动跳转,
				您也可以点此 <a id="target" th:href="@{${target}}" class="text-primary">链接</a>, 手动跳转!
			</p>
		</div>
	</div>
</div>

修改 templates/site/register.html

<!-- 内容 -->
<div class="main">
	<div class="container pl-5 pr-5 pt-3 pb-3 mt-3 mb-3">
		<h3 class="text-center text-info border-bottom pb-3">&nbsp;&nbsp;</h3>
		<form class="mt-5" method="post" th:action="@{/register}">
			<div class="form-group row">
				<label for="username" class="col-sm-2 col-form-label text-right">账号:</label>
				<div class="col-sm-10">
					<input type="text"
						   th:class="|form-control ${usernameMsg != null ? 'is-invalid' : ''}|"
						   th:value="${user != null ? user.username : ''}"
						   id="username" name="username" placeholder="请输入您的账号!" required>
					<div class="invalid-feedback" th:text="${usernameMsg}">
						该账号已存在!
					</div>
				</div>
			</div>
			<div class="form-group row mt-4">
				<label for="password" class="col-sm-2 col-form-label text-right">密码:</label>
				<div class="col-sm-10">
					<input type="password"
						   th:class="|form-control ${passwordMsg != null ? 'is-invalid' : ''}|"
						   th:value="${user != null ? user.password : ''}"
						   id="password" name="password" placeholder="请输入您的密码!" required>
					<div class="invalid-feedback" th:text="${passwordMsg}">
						密码长度不能小于8位!
					</div>							
				</div>
			</div>
			<div class="form-group row mt-4">
				<label for="confirm-password" class="col-sm-2 col-form-label text-right">确认密码:</label>
				<div class="col-sm-10">
					<input type="password" class="form-control"
						   th:value="${user != null ? user.password : ''}"
						   id="confirm-password" placeholder="请再次输入密码!" required>
					<div class="invalid-feedback">
						两次输入的密码不一致!
					</div>
				</div>
			</div>
			<div class="form-group row">
				<label for="email" class="col-sm-2 col-form-label text-right">邮箱:</label>
				<div class="col-sm-10">
					<input type="email"
						   th:class="|form-control ${emailMsg != null ? 'is-invalid' : ''}|"
						   th:value="${user != null ? user.email : ''}"
						   id="email" name="email" placeholder="请输入您的邮箱!" required>
					<div class="invalid-feedback" th:text="${emailMsg}">
						该邮箱已注册!
					</div>
				</div>
			</div>
			<div class="form-group row mt-4">
				<div class="col-sm-2"></div>
				<div class="col-sm-10 text-center">
					<button type="submit" class="btn btn-info text-white form-control">立即注册</button>
				</div>
			</div>
		</form>				
	</div>
</div>

启动程序,先注册一个数据库中存在的用户试试

在这里插入图片描述
在这里插入图片描述

注册一个有效的:

在这里插入图片描述

2.3 激活注册账号

写个接口类,存放常量

package com.nowcoder.community.util;

public interface CommunityConstant {

    //激活成功
    int ACTIVATION_SUCCESS = 0;
    //重复激活
    int ACTIVATION_REPEAT = 1;
    //激活失败
    int ACTIVATION_FAILURE = 2;
}

UserService 类

实现该接口

public class UserService implements CommunityConstant {...}

增加业务方法

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

LoginController 类
实现接口

public class LoginController implements CommunityConstant {...}

增加方法

@GetMapping("/login")
public String getLoginPage() {
    return "/site/login";
}

//http://localhost:8080/community/activation/userId/code
@GetMapping("/activation/{userId}/{code}")
public String activation(Model model, @PathVariable("userId") int userId, @PathVariable("code") String code) {
    int result = userService.activation(userId, code);
    if (result == ACTIVATION_SUCCESS) {
        model.addAttribute("msg", "激活成功!您的账号可以正常使用了!");
        model.addAttribute("target", "/login");
    } else if (result == ACTIVATION_REPEAT) {
        model.addAttribute("msg", "无效操作!该账号已被激活过!");
        model.addAttribute("target", "/index");
    } else {
        model.addAttribute("msg", "激活失败!您提供的激活码不正确!");
        model.addAttribute("target", "/index");
    }
    return "/site/operate-result";
}

修改 templates /site/login.html
2行

<html lang="en" xmlns:th="http://www.thymeleaf.org">

8-9行

<link rel="stylesheet" th:href="@{/css/global.css}" />
<link rel="stylesheet" th:href="@{/css/login.css}" />

15行

<!-- 头部 -->
<header class="bg-dark sticky-top" th:replace="index::header">

94行

<img th:src="@{/img/captcha.png}" style="width:100px;height:40px;" class="mr-2"/>

最后一个 script 标签

<script th:src="@{/js/global.js}"></script>

修改 index.html

<a class="nav-link" th:href="@{/login}">登录</a>

启动程序,注册一个账号,点击激活邮件里的链接!

在这里插入图片描述

【踩坑】

从首页点击“登录”之后:

在这里插入图片描述


500错,基本都是代码有错误,运行期间出了 error,找了半天发现:
login.html 里拼错了单词!!大无语,以后这种字符串建议复制。

在这里插入图片描述

3. 会话管理

  • HTTP的基本性质
    • HTTP是简单的
    • HTTP是可扩展的
    • HTTP是无状态的,有会话的
  • Cookie
    • 是服务器发送到浏览器,并保存在浏览器端的一小块数据。
    • 浏览器下次访问该服务器时,会自动携带块该数据,将其发送给服务器。
  • Session
    • 是JavaEE的标准,用于在服务端记录客户端信息。
    • 数据存放在服务端更加安全,但是也会增加服务端的内存压力。

在这里插入图片描述


在这里插入图片描述


4. 生成验证码

使用 kaptcha

pom.xml

<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>

写个配置类

package com.nowcoder.community.Config;

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

import java.util.Properties;

@Configuration
public class KaptchaConfig {

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

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

LoginController 类

引入属性

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

@Resource
private Producer kaptchaProducer;

新增方法

@GetMapping("/kaptcha")
public void getKaptcha(HttpServletResponse response, HttpSession session) {
    //生成验证码
    String text = kaptchaProducer.createText();
    BufferedImage image = kaptchaProducer.createImage(text);

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

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

启动程序,访问 http://localhost:8080/community/kaptcha,成功显示验证码!

将图片应用到登录页面,templates / site / login.html
94行

<img th:src="@{/kaptcha}" id="kaptcha" style="width:100px;height:40px;" class="mr-2"/>

实现验证码刷新功能
95行

<a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a>

在最后面实现 refresh_kaptcha()

<script>
	function refresh_kaptcha() {
		var path = CONTEXT_PATH + "/kaptcha?p=" + Math.random();
		$("#kaptcha").attr("src", path);
	}
</script>

static / js / global.js 的第一行

var CONTEXT_PATH = "/community";

如果不传个参数 p 的话,访问路径没变,访问的又是个图片,一些浏览器可能会自行判定不用发起访问

重新编译程序,访问 http://localhost:8080/community/login ,点击 “刷新验证码”,成功!

5. 登录、退出

在这里插入图片描述

5.1 实体类

根据登录凭证表 login_ticket,写一个对应的实体类

package com.nowcoder.community.entity;

import java.util.Date;

public class LoginTicket {

    private int id;
    private int userId;
    private String ticket;
    private int status;
    private Date expired;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public String getTicket() {
        return ticket;
    }

    public void setTicket(String ticket) {
        this.ticket = ticket;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public Date getExpired() {
        return expired;
    }

    public void setExpired(Date expired) {
        this.expired = expired;
    }

    @Override
    public String toString() {
        return "LoginTicket{" +
                "id=" + id +
                ", userId=" + userId +
                ", ticket='" + ticket + '\'' +
                ", status=" + status +
                ", expired=" + expired +
                '}';
    }
}

5.2 dao

package com.nowcoder.community.dao;

import com.nowcoder.community.entity.LoginTicket;
import org.apache.ibatis.annotations.*;

@Mapper
public interface LoginTicketMapper {

    @Insert({
            "insert into login_ticket(user_id, ticket, status, expired) " +
            "values(#{userId}, #{ticket}, #{status}, #{expired})"
    })
    @Options(useGeneratedKeys = true, keyProperty = "id") //自动生成主键,并注入给loginTicket的 id
    int insertLoginTicket(LoginTicket loginTicket);

    @Select({
            "select id, user_id, ticket, status, expired " +
             "from login_ticket where ticket = #{ticket}"
    })
    LoginTicket selectByTicket(String ticket);

    @Update({
            "update login_ticket set status = #{status} where ticket = #{ticket}"
    })
    int updateStatus(@Param("ticket") String ticket, @Param("status") int status);
}

在 MapperTests 类中先测试一次

@Resource
private LoginTicketMapper loginTicketMapper;

@Test
public void testInsertLoginTicket() {
    LoginTicket loginTicket = new LoginTicket();
    loginTicket.setUserId(101);
    loginTicket.setExpired(new Date(System.currentTimeMillis() + 1000 * 60 * 10));
    loginTicket.setTicket("aaa");
    loginTicket.setStatus(0);

    loginTicketMapper.insertLoginTicket(loginTicket);
}

插入成功!

@Test
public void testSelectLoginTicket() {
    LoginTicket loginTicket = loginTicketMapper.selectByTicket("aaa");
    System.out.println(loginTicket);

    loginTicketMapper.updateStatus("aaa", 1);
    loginTicket = loginTicketMapper.selectByTicket("aaa");
    System.out.println(loginTicket);
}

查询、更新成功!

5.3 service

UserService

注入 mapper

@Resource
private LoginTicketMapper loginTicketMapper;

增加方法

//expiredSeconds 秒之后凭证会过期
public Map<String, Object> login(String username, String password, int expiredSeconds) {
    Map<String, Object> map = new HashMap<>();
    //空值处理
    if (StringUtils.isBlank(username)) {
        map.put("usernameMsg", "账号不能为空!");
        return map;
    }
    if (StringUtils.isBlank(password)) {
        map.put("passwordMsg", "密码不能为空!");
        return map;
    }
    //合法性验证
    User user = userMapper.selectByName(username);
    if (user == null) {
        map.put("usernameMsg", "该账号不存在!");
        return map;
    }
    if (user.getStatus() == 0) {
        map.put("usernameMsg", "该账号未激活!");
        return map;
    }
    //验证密码
    password = CommunityUtil.md5(password + user.getSalt());
    if (!password.equals(user.getPassword())) {
        map.put("passwordMsg", "密码不正确!");
        return map;
    }
    //生成登录凭证
    LoginTicket loginTicket = new LoginTicket();
    loginTicket.setUserId(user.getId());
    loginTicket.setTicket(CommunityUtil.generateUUID());
    loginTicket.setStatus(0);
    loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
    loginTicketMapper.insertLoginTicket(loginTicket);
    map.put("ticket", loginTicket.getTicket());
    return map;
}

5.4 controller

在 CommunityConstant 类中定义常量

//默认状态的登录超时时间
int DEFAULT_EXPIRED_SECONDS = 3600 * 12; //12小时
//记住我 状态下的登录超时时间
int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100; //100天

LoginController 类

注入属性

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

新增方法

形参中的对象类型,会被自动加到 model 中,可以在视图中直接取
java自带的数据类型不会被加到 model 中,如果在视图中想用,可以在 request 对象中取

@PostMapping("/login")
public String login(String username, String password, String code,
                    boolean rememberme, Model model, HttpSession session,
                    HttpServletResponse response) {
    //检查验证码
    String kaptcha = (String) session.getAttribute("kaptcha");
    if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
        model.addAttribute("codeMsg", "验证码不正确!");
        return "/site/login";
    }
    //检查账号、密码
    int expiredSeconds = rememberme ?  REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
    Map<String, Object> map = userService.login(username, password, expiredSeconds);
    if (map.containsKey("ticket")) {//只有登陆成功才会存ticket
        Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
        cookie.setPath(contextPath);
        cookie.setMaxAge(expiredSeconds);
        response.addCookie(cookie);
        return "redirect:/index";
    } else {
        model.addAttribute("usernameMsg", map.get("usernameMsg"));
        model.addAttribute("passwordMsg", map.get("passwordMsg"));
        return "/site/login";
    }

}

修改 site / login.html,对于每个框,增加 name 属性,name的值要和 controller 中的 login () 形参名保持一致

${param.username} 等价于 request.getParamter(“username”)

<!-- 内容 -->
<div class="main">
	<div class="container pl-5 pr-5 pt-3 pb-3 mt-3 mb-3">
		<h3 class="text-center text-info border-bottom pb-3">&nbsp;&nbsp;</h3>
		<form class="mt-5" method="post" th:action="@{/login}">
			<div class="form-group row">
				<label for="username" class="col-sm-2 col-form-label text-right">账号:</label>
				<div class="col-sm-10">
					<input type="text" th:class="|form-control ${usernameMsg != null ? 'is-invalid' : ''}|"
						   th:value="${param.username}"
						   id="username" name="username" placeholder="请输入您的账号!" required>
					<div class="invalid-feedback" th:text="${usernameMsg}">
						该账号不存在!
					</div>
				</div>
			</div>
			<div class="form-group row mt-4">
				<label for="password" class="col-sm-2 col-form-label text-right">密码:</label>
				<div class="col-sm-10">
					<input type="password" th:class="|form-control ${passwordMsg != null ? 'is-invalid' : ''}|"
						   th:value="${param.password}"
						   id="password" name="password" placeholder="请输入您的密码!" required>
					<div class="invalid-feedback" th:text="${passwordMsg}">
						密码长度不能小于8!
					</div>							
				</div>
			</div>
			<div class="form-group row mt-4">
				<label for="verifycode" class="col-sm-2 col-form-label text-right">验证码:</label>
				<div class="col-sm-6">
					<input type="text" th:class="|form-control ${codeMsg != null ? 'is-invalid' : ''}|"
						   id="verifycode" name="code" placeholder="请输入验证码!">
					<div class="invalid-feedback" th:text="${codeMsg}">
						验证码不正确!
					</div>
				</div>
				<div class="col-sm-4">
					<img th:src="@{/kaptcha}" id="kaptcha" style="width:100px;height:40px;" class="mr-2"/>
					<a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a>
				</div>
			</div>				
			<div class="form-group row mt-4">
				<div class="col-sm-2"></div>
				<div class="col-sm-10">
					<input type="checkbox" id="remember-me" name="rememberme"
						   th:checked="${param.rememberme}">
					<label class="form-check-label" for="remember-me">记住我</label>
					<a href="forget.html" class="text-danger float-right">忘记密码?</a>
				</div>
			</div>				
			<div class="form-group row mt-4">
				<div class="col-sm-2"></div>
				<div class="col-sm-10 text-center">
					<button type="submit" class="btn btn-info text-white form-control">立即登录</button>
				</div>
			</div>
		</form>				
	</div>
</div>

启动程序,测试,各项正常!

在这里插入图片描述

退出功能

UserService

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

LoginController

@GetMapping("/logout")
public String logout(@CookieValue("ticket") String ticket) {
    userService.logout(ticket);
    return "redirect:/login";
}

index.html
45行

<a class="dropdown-item text-center" th:href="@{/logout}">退出登录</a>

启动,测试,点击“退出登录”,成功返回登录页,数据库中的登录凭证状态值改为了1

在这里插入图片描述

6. 显示登录信息

在这里插入图片描述


因为需要经常用到 cookie,所以我们封装一个工具类

package com.nowcoder.community.util;

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

public class CookieUtil {

    //传入cookie的name,返回该cookie的value
    public static String getValue(HttpServletRequest request, String name) {
        if (request == null || name == null) {
            throw new IllegalArgumentException("参数为空!");
        }
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(name)) {
                    return cookie.getValue();
                }
            }
        }
        return null;
    }
}

UserService 新增方法

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

再写个工具类

package com.nowcoder.community.util;

import com.nowcoder.community.entity.User;
import org.springframework.stereotype.Component;

/**
 * 持有用户信息,用于代替 session 对象
 */
@Component
public class HostHolder {

    private ThreadLocal<User> users = new ThreadLocal<>();

    public void setUser(User user) {
        users.set(user);
    }

    public User getUser() {
        return users.get();
    }

    public void clear() {
        users.remove();
    }
}

写个拦截器 LoginTicketInterceptor

package com.nowcoder.community.controller.interceptor;

import com.nowcoder.community.entity.LoginTicket;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.UserService;
import com.nowcoder.community.util.CookieUtil;
import com.nowcoder.community.util.HostHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;

@Component
public class LoginTicketInterceptor implements HandlerInterceptor {

    @Resource
    private UserService userService;

    @Resource
    private HostHolder hostHolder;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //从 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());
                //让本次请求持有 user
                hostHolder.setUser(user);
            }
        }
        return true;
    }

    //请求之后、模板解析之前执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        User user = hostHolder.getUser();
        if (user != null && modelAndView != null) {
            modelAndView.addObject("loginUser", user);
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        hostHolder.clear();
    }
}

配到配置类中

package com.nowcoder.community.Config;

import com.nowcoder.community.controller.interceptor.LoginTicketInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Resource
    private LoginTicketInterceptor loginTicketInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginTicketInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
    }
}

修改 index.html,让没登陆时,该显示的显示,不该显示的不显示,登陆之后显示该显示的

<ul class="navbar-nav mr-auto">
	<li class="nav-item ml-3 btn-group-vertical">
		<a class="nav-link" th:href="@{/index}">首页</a>
	</li>
	<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser != null}">
		<a class="nav-link position-relative" href="site/letter.html">消息<span class="badge badge-danger">12</span></a>
	</li>
	<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser == null}">
		<a class="nav-link" th:href="@{/register}">注册</a>
	</li>
	<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser == null}">
		<a class="nav-link" th:href="@{/login}">登录</a>
	</li>
	<li class="nav-item ml-3 btn-group-vertical dropdown" th:if="${loginUser != null}">
		<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
			<img th:src="${loginUser.headerUrl}" class="rounded-circle" style="width:30px;"/>
		</a>
		<div class="dropdown-menu" aria-labelledby="navbarDropdown">
			<a class="dropdown-item text-center" href="site/profile.html">个人主页</a>
			<a class="dropdown-item text-center" href="site/setting.html">账号设置</a>
			<a class="dropdown-item text-center" th:href="@{/logout}">退出登录</a>
			<div class="dropdown-divider"></div>
			<span class="dropdown-item text-center text-secondary" th:utext="${loginUser.username}">nowcoder</span>
		</div>
	</li>
</ul>

启动,测试:
未登陆时,不显示 “消息”

在这里插入图片描述

登录 hahaha,123456,显示 “消息” 和头像!

在这里插入图片描述

7. 账号设置

写个 controller

package com.nowcoder.community.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

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

    @GetMapping("/setting")
    public String getSettingPage() {
        return "site/setting";
    }
    
}

修改 setting.html

2行

<html lang="en" xmlns:th="http://www.thymeleaf.org">

8-9行

<link rel="stylesheet" th:href="@{/css/global.css}" />
<link rel="stylesheet" th:href="@{/css/login.css}" />

15行

<!-- 头部 -->
<header class="bg-dark sticky-top" th:replace="index::header">

191行

<script th:src="@{/js/global.js}"></script>

修改 index.html

<a class="dropdown-item text-center" th:href="@{user/setting}">账号设置</a>

测试:
启动程序,在首页登录之后,点击“账号设置”,成功!

在这里插入图片描述

配置头像存储路径
application.properties

community.path.upload=d:/work/data/upload

UserService

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

UserController

package com.nowcoder.community.controller;

import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.UserService;
import com.nowcoder.community.util.CommunityUtil;
import com.nowcoder.community.util.HostHolder;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;

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

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

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

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

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

    @Resource
    private UserService userService;

    @Resource
    private HostHolder hostHolder;

    @PostMapping("/upload")
    public String uploadHeader(MultipartFile headerImage, Model model) {
        if (headerImage == null) {
            model.addAttribute("error", "您还没有选择图片");
            return "site/setting";
        }
        //判断后缀合法性
        String fileName = headerImage.getOriginalFilename();
        //文件后缀: png等
        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/随机名.png
        User user = hostHolder.getUser();
        String headerUrl = domain + contextPath + "/user/header/" + fileName;
        userService.updateHeader(user.getId(), headerUrl);

        return "redirect:/index";
    }

    @GetMapping("/header/{fileName}")
    public void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response) {
        //服务器存放路径
        fileName = uploadPath + "/" + fileName;
        //文件后缀:输出的时候要用
        String suffix = fileName.substring(fileName.lastIndexOf("."));
        //响应图片
        response.setContentType("image/" + suffix);
        try(FileInputStream fis = new FileInputStream(fileName)) {
            OutputStream os = response.getOutputStream();
            byte[] buffer = new byte[1024];
            int b = 0;
            while ((b = fis.read(buffer)) != -1) {
                os.write(buffer, 0, b);
            }
        } catch (IOException e) {
            logger.error("读取头像失败:" + e.getMessage());
        }
    }
    
    @GetMapping("/setting")
    public String getSettingPage() {
        return "site/setting";
    }
}

setting.html
67行

<form class="mt-5" method="post" enctype="multipart/form-data" th:action="@{/user/upload}">

71行

<div class="custom-file">
	<input type="file" th:class="|custom-file-input ${error != null ? 'is-invalid' : ''}|"
		   id="head-image" name="headerImage" lang="es" required="">
	<label class="custom-file-label" for="head-image" data-browse="文件">选择一张图片</label>
	<div class="invalid-feedback" th:text="${error}">
		该账号不存在!
	</div>
</div>

在 D 盘创建相应的路径 D:\work\data\upload

启动程序,测试

在这里插入图片描述
在这里插入图片描述


8. 检查登录状态

在这里插入图片描述

自定义一个注解类

package com.nowcoder.community.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}

UserController 类中 getSettingPage()uploadHeader() 的方法上加上 @LoginRequired 注解,表明这俩方法需要登录才可以访问

写个拦截器,拦截带有该注解的方法

package com.nowcoder.community.controller.interceptor;

import com.nowcoder.community.annotation.LoginRequired;
import com.nowcoder.community.util.HostHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {

    @Resource
    private HostHolder hostHolder;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {//如果拦截的是个方法的话
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            //获取 LoginRequired 类型的注解
            LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
            //有这个注解,说明需要登录,而实际没有登录
            if (loginRequired != null && hostHolder.getUser() == null) {
                //去登录
                response.sendRedirect(request.getContextPath() + "/login");
                return false; //不放行
            }
        }
        return true;
    }
}

将拦截器 LoginRequiredInterceptor 配置到配置类 WebMvcConfig 中

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Resource
    private LoginTicketInterceptor loginTicketInterceptor;

    @Resource
    private LoginRequiredInterceptor loginRequiredInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginTicketInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
        
        registry.addInterceptor(loginRequiredInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
    }
}

启动,测试,在未登录状态下访问 http://localhost:8080/community/user/setting,直接拦截到登录页!

【课后作业】

自行实现修改密码功能

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值