仿牛客网论坛网站开发流程一


技术栈

  • Spring Boot、MySQL
  • Spring、Spring MVC、MyBatis
  • Redis、Kafka、Elasticsearch、Zookeeper
  • Spring Security、Spring Actuator
  • Caffeine
  • Thymeleaf

创建项目

在intellij IDEA中利用Spring Initializer创建SpringBoot项目,使用Maven管理项目

maven常用命令:

mvn compile : 编译maven项目,会出现target目录 
mvn clean : 删除编译后的内容,target目录会被删除 
mvn test :执行test中的方法,会首先编译test类

Spring boot的核心作用:起步依赖,自动配置,端点监控

配置测试数据库连接

引入依赖

<!-- mysql数据库连接驱动-->  
<dependency>  
    <groupId>mysql</groupId>  
    <artifactId>mysql-connector-java</artifactId>  
</dependency>  
  
<!-- 引入Spring封装的jdbc,内部默认依赖了 HikariDataSource  数据源-->  
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-jdbc</artifactId>  
</dependency>  
  
<!-- mybatis -->  
<dependency>  
    <groupId>org.mybatis.spring.boot</groupId>  
    <artifactId>mybatis-spring-boot-starter</artifactId>  
    <version>2.2.0</version>  
</dependency>

配置文件application.yml

server:  
  port: 80  
  # 应用的上下文路径,也可以称为项目路径,是构成url地址的一部分  
  # 此时的访问方式为localhost:8088/community/xxxxxx  
  servlet:  
    context-path: /community

# Mybatis  
mybatis:  
  configuration:  
    # 自动转驼峰  
    map-underscore-to-camel-case: true  
    # 允许JDBC支持自动生成主键,需要驱动兼容  
    use-generated-keys: true  
    # 控制台标准日志  
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

spring:  
  # Thymeleaf 模板缓存  
  thymeleaf:  
    cache: false  
  
  # DataSource  
  datasource:  
    driver-class-name: com.mysql.cj.jdbc.Driver  
    url: jdbc:mysql://127.0.0.1:3306/community?characterEncoding=utf-8&useSSL=false&serverTimezone=Hongkong  
    username: root  
    password: root  
    type: com.zaxxer.hikari.HikariDataSource  
    hikari:  
      maximum-pool-size: 15  
      minimum-idle: 5  
      idle-timeout: 30000

创建entity包并创建User类

@Data  
@NoArgsConstructor  
@AllArgsConstructor  
@ToString  
public class User {  
  
    private int id;  
    private String username;  
    private String password;  
    private String salt;  
    private String email;  
    private int type;  
    private int status;  
    private String activationCode;  
    private String headerUrl;  
    private Date createTime;  
  
}

创建mapper包并创建UserMapper接口

@Mapper  
public interface UserMapper {  
  
    @Select("select * from user where id = #{id}")  
    User selectById(int id);  
  
    @Select("select * from user where username = #{username}")  
    User selectByName(String username);  
  
}

测试

@SpringBootTest  
public class UserMapperTest {  
  
    @Autowired  
    UserMapper userMapper;  
  
    @Test  
    public void selectByIdTest(){  
        User user = userMapper.selectById(166);  
        System.out.println(user);  
    }  
  
    @Test  
    public void selectByUsernameTest(){  
        User user = userMapper.selectByName("SYSTEM");  
        System.out.println(user);  
    }
}

社区首页

开发流程

  • 1次请求的执行过程
  • 分步实现
    • 开发社区首页,显示前10个帖子
    • 开发分页组件,分页显示所有的帖子

社区首页

  1. 开发社区首页,显示前10个帖子
    1. 创建实体类
    2. 写对应mapper
    3. 创建service,因为在首页得到的DiscussPost并不携带userid,所以需要创建DiscusspostService和UserService来实现首页展示10个帖子的功能
    4. 写controller,将查询到的数据注入到model中
    5. 修改index.html
  2. 开发分页组件,分页显示所有的帖子
    1. 创建page类,在Page中设置当前页码,显示上限,数据总数,查询路径
    2. 在开发社区首页的controller方法中,第一次访问自动注入page,page会自动注入到model中,然后我们让前端在跳转页面的时候在参数上添加页数,我们的SpringMVC会自动注入Page页面
    3. index.html首页中修改分页组件

社区首页

创建实体类

@Data  
@NoArgsConstructor  
@AllArgsConstructor  
@ToString  
public class DiscussPost {  
  
    @Id  
    private int id;  
    private int userId;  
    private String title;  
    private String content;  
    private int type;  
    private int status;  
    private Date createTime;  
    private int commentCount;  
    private double score;  
  
}

创建DiscussPostMapper接口

@Mapper  
public interface DiscussPostMapper {  
  
    @Select("select * from discuss_post where if (#{userId} != 0, status != 2 and user_id = #{userId}, status != 2) " +  
            "order by type desc,create_time desc " +  
            "limit #{offset},#{limit}")  
    List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit);  
  
    @Select("select count(id) from discuss_post where if (#{userId} != 0, status != 2 and user_id = #{userId}, status != 2)")  
    int selectDiscussPostRows(int userId);

}

创建service包
创建DiscussPostService类和UserService类

@Service  
public class DiscussPostService {  
  
    @Autowired  
    private DiscussPostMapper discussPostMapper;

	public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit){  
	    return discussPostMapper.selectDiscussPosts(userId,offset,limit);  
	}  

	public int findDiscussPostRows(int userId){  
	    return discussPostMapper.selectDiscussPostRows(userId);  
	}
}
@Service  
public class UserService {  
  
    @Autowired  
    private UserMapper userMapper;
    
    public User findUserById(int userId) {  
	    return userMapper.selectById(userId);  
    }
}

将静态资源(css image js等文件)放到static文件下,将模板文件(site index.html)放到templates文件下。Spring MVC将在resource/templates文件夹下查找html页面,返回给客户端

接下来开发视图层,创建controller包,创建HomeController类

@Controller  
public class HomeController {  
  
    @Autowired  
    private UserService userService;  
  
    @Autowired  
    private DiscussPostService discussPostService;

    /**  
     * 分页显示首页帖子  
     */  
    @RequestMapping(path = {"/index","/"},method = RequestMethod.GET)  
    public String getIndexPage(Model model){  
  
        List<DiscussPost> discussPostList = discussPostService.findDiscussPosts(0, 0, 10);  
  
        List<Map<String, Object>> discussPosts = new ArrayList<>();  
        if (discussPostList != null) {  
            for (DiscussPost post : discussPostList) {  
                Map<String, Object> map = new HashMap<>();  
                map.put("post", post);  
                User user = userService.findUserById(post.getUserId());  
                map.put("user", user);  
  
                discussPosts.add(map);  
            }  
        }  
        model.addAttribute("discussPosts", discussPosts);  
        return "/index";  
    }
}

更改Index.html文件,使用Thymeleaf对其中相对路径进行更改

<!doctype html>  
<html lang="en" xmlns:th="http://www.thymeleaf.org">  
<head>  
   <meta charset="utf-8">  
   <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">  
   <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" crossorigin="anonymous">  
   <link rel="stylesheet" th:href="@{/css/global.css}" />  
   <title>首页</title>  
</head>

......

<ul class="list-unstyled">
	<li class="media pb-3 pt-3 mb-3 border-bottom" th:each="map:${discussPosts}">
		<a href="site/profile.html">
			<img th:src="${map.user.headerUrl}" class="mr-4 rounded-circle" alt="用户头像" style="width:50px;height:50px;">
		</a>
		<div class="media-body">
			<h6 class="mt-0 mb-3">
				<a href="#" th:utext="${map.post.title}">备战春招,面试刷题跟他复习,一个月全搞定!</a>
				<span class="badge badge-secondary bg-primary" th:if="${map.post.type==1}">置顶</span>
				<span class="badge badge-secondary bg-danger" th:if="${map.post.status==1}">精华</span>
			</h6>
			<div class="text-muted font-size-12">
				<u class="mr-3" th:utext="${map.user.username}">寒江雪</u> 发布于 <b th:text="${#dates.format(map.post.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</b>
				<ul class="d-inline float-right">
					<li class="d-inline ml-2">赞 11</li>
					<li class="d-inline ml-2">|</li>
					<li class="d-inline ml-2">回帖 7</li>
				</ul>
			</div>
		</div>						
	</li>
</ul>

......

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

分页组件

开发分页组件,分页显示所有帖子

在entity包下创建Page类,用于记录分页数据

/**  
 * 封装分页相关的信息.  
 */public class Page {  
  
    // 当前页码  
    private int current = 1;  
    // 显示上限  
    private int limit = 10;  
    // 数据总数(用于计算总页数)  
    private int rows;  
    // 查询路径(用于复用分页链接)  
    private String path;  
  
    public int getCurrent() {  
        return current;  
    }  
  
    public void setCurrent(int current) {  
        if (current >= 1) {  
            this.current = current;  
        }  
    }  
  
    public int getLimit() {  
        return limit;  
    }  
  
    public void setLimit(int limit) {  
        if (limit >= 1 && limit <= 100) {  
            this.limit = limit;  
        }  
    }  
  
    public int getRows() {  
        return rows;  
    }  
  
    public void setRows(int rows) {  
        if (rows >= 0) {  
            this.rows = rows;  
        }  
    }  
  
    public String getPath() {  
        return path;  
    }  
  
    public void setPath(String path) {  
        this.path = path;  
    }  
  
    /**  
     * 获取当前页的起始行  
     *  
     * @return  
     */  
    public int getOffset() {  
        // current * limit - limit  
        return (current - 1) * limit;  
    }  
  
    /**  
     * 获取总页数  
     *  
     * @return  
     */  
    public int getTotal() {  
        // rows / limit [+1]  
        if (rows % limit == 0) {  
            return rows / limit;  
        } else {  
            return rows / limit + 1;  
        }  
    }  
  
    /**  
     * 获取起始页码  
     *  
     * @return  
     */  
    public int getFrom() {  
        int from = current - 2;  
        return from < 1 ? 1 : from;  
    }  
  
    /**  
     * 获取结束页码  
     *  
     * @return  
     */  
    public int getTo() {  
        int to = current + 2;  
        int total = getTotal();  
        return to > total ? total : to;  
    }  
  
}

修改HomeController的getIndexPage方法

@RequestMapping(path = {"/index","/"},method = RequestMethod.GET)  
public String getIndexPage(Model model, Page page){  

    page.setRows(discussPostService.findDiscussPostRows(0));  
    page.setPath("/index");  
  
    List<DiscussPost> discussPostList = discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit());  
  
    List<Map<String, Object>> discussPosts = new ArrayList<>();  
    if (discussPostList != null) {  
        for (DiscussPost post : discussPostList) {  
            Map<String, Object> map = new HashMap<>();  
            map.put("post", post);  
            User user = userService.findUserById(post.getUserId());  
            map.put("user", user);  
  
            discussPosts.add(map);  
        }  
    }  
    model.addAttribute("discussPosts", discussPosts);  
    return "/index";  
}

更改Index.html中分页组件

<!-- 分页 -->  
<nav class="mt-5" th:if="${page.rows>0}">  
   <ul class="pagination justify-content-center">  
      <li class="page-item">  
         <a class="page-link" th:href="@{${page.path}(current=1)}">首页</a>  
      </li>      
      <li th:class="|page-item ${page.current==1?'disabled':''}|">  
         <a class="page-link" th:href="@{${page.path}(current=${page.current - 1})}">上一页</a></li>  
      <li th:class="|page-item ${i==page.current?'active':''}|" th:each="i:${#numbers.sequence(page.from,page.to)}">  
         <a class="page-link" th:href="@{${page.path}(current=${i})}" th:text="${i}">1</a>  
      </li>      
      <li th:class="|page-item ${page.current==page.total?'disabled':''}|">  
         <a class="page-link" th:href="@{${page.path}(current=${page.current+1})}">下一页</a>  
      </li>      
      <li class="page-item">  
         <a class="page-link" th:href="@{${page.path}(current=${page.total})}">末页</a>  
      </li>   
	</ul>
</nav>

发送邮箱

开发流程

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

Spring Mail配置文件并加入到pom文件中

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

在application.yml文件中配置Mail的参数

需要在个人邮箱的网站设置启用授权码,验证手机

spring:
	# Mail  
	mail:  
	  host: smtp.qq.com  
	  port: 465  
	  username: 10xxxxxx51@qq.com  
	  password: zjnxxxxxxxxxbchb  
	  protocol: smtps  
	  default-encoding: UTF-8  
	banner:  
	  charset: UTF-8

创建util工具文件包,并在util包中创建MailClient类

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

注册

开发流程

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

首先导入一个常用的包 commons lang

主要是字符串判空等功能

<dependency>  
    <groupId>commons-lang</groupId>  
    <artifactId>commons-lang</artifactId>  
    <version>2.4</version>  
</dependency>

添加配置信息

因为开发、上线域名不一样所以用户打开邮箱激活的链接也不一样,所以写在yml配置文件中方便更改

Community:  
  path:  
    domain: http://127.0.0.1:80

在util中添加CommunityUtil工具类,对用户的密码加密

public class CommunityUtil {  
  
    // 生成随机字符串  
    public static String generateUUID() {  
        return UUID.randomUUID().toString().replaceAll("-", "");  
    }  
  
    // MD5加密  
    // hello -> abc123def456  
    // hello + 3e4a8 -> abc123def456abc    public static String md5(String key) {  
        if (StringUtils.isBlank(key)) {  
            return null;  
        }  
        return DigestUtils.md5DigestAsHex(key.getBytes());  
    }
}

在UserMapper接口中添加下面几个方法

@Select("select * from user where email = #{email}")  
User selectByEmail(String email);  
  
@Insert("insert into user(username, password, salt, email, type, status, activation_code, header_url, create_time) " +  
        "values(#{username}, #{password}, #{salt}, #{email}, #{type}, #{status}, #{activationCode}, #{headerUrl}, #{createTime})")  
int insertUser(User user);  
  
@Update("update user set status = #{status} where id = #{id}")  
int updateStatus(int id, int status);

在Service中更新UserService类,用于注册用户业务,并更新激活页面activation.html

@Autowired  
private UserMapper userMapper;  
  
@Autowired  
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 userId) {  
    return userMapper.selectById(userId);  
}  
  
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;  
    }  
  
    // 验证账号  
    if (userMapper.selectByName(user.getUsername()) != null) {  
        map.put("usernameMsg", "该账号已存在!");  
        return map;  
    }  
  
    // 验证邮箱  
    if (userMapper.selectByEmail(user.getEmail()) != 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);  
    //重新查询,因为表单没有id字段,默认0,user对象的id不是插入数据库的id  
    User user1 = userMapper.selectByName(user.getUsername());  
  
    // 激活邮件  
    Context context = new Context();  
    context.setVariable("email", user.getEmail());  
    // http://127.0.0.1:80/community/activation/170/code  
    String url = domain + contextPath + "/activation/" + user1.getId() + "/" + user1.getActivationCode();  
    context.setVariable("url", url);  
    String content = templateEngine.process("/mail/activation", context);  
    mailClient.sendMail(user.getEmail(), "激活账号", content);  
  
    return map;  
}
<!doctype html>  
<html lang="en" xmlns:th="http://www.thymeleaf.org">  
<head>  
   <meta charset="utf-8">  
   <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类

@Controller
public class LoginController implements CommunityConstant {
 
    @Autowired
    private UserService userService;
 
    @Value("${server.servlet.context-path}")
    private String contextPath;
 
	/**  
	 * 跳转到注册  
	 */  
	@RequestMapping(path = "/register", method = RequestMethod.GET)  
	public String getRegisterPage() {  
	    return "/site/register";  
	}  
	  
	/**  
	 * 跳转到登录  
	 */  
	@RequestMapping(path = "/login", method = RequestMethod.GET)  
	public String getLoginPage() {  
	    return "/site/login";  
	}  
	  
	  
	/**  
	 * 注册  
	 */  
	@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";  
	    }  
	}
}

更改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>

更改operate-result页面

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

......

<script>  
   $(function(){  
      setInterval(function(){  
         var seconds = $("#seconds").text();  
         $("#seconds").text(--seconds);  
         if(seconds == 0) {  
            location.href = $("#target").attr("href");  
         }  
      }, 1000);  
   });  
</script>

激活注册账号

util包中创建CommunityConstant接口,定义几个激活状态常量

public interface CommunityConstant {  
  
    /**  
     * 激活成功  
     */  
    int ACTIVATION_SUCCESS = 0;  
  
    /**  
     * 重复激活  
     */  
    int ACTIVATION_REPEAT = 1;  
  
    /**  
     * 激活失败  
     */  
    int ACTIVATION_FAILURE = 2;
}

UserService实现此接口,增加新方法

public int activation(int userId, String code) {  
    User user = userMapper.selectById(userId);  
    if (user == null){  
        return ACTIVATION_FAILURE;  
    }  
    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类

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

会话管理

HTTP的基本性质

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

Cookie

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

Session

  • 是JavaEE的标准,用于在服务端记录客户端信息。
  • 数据存放在服务端更加安全,但是也会增加服务端的内存压力。
  • 服务器分布式部署的时候存放session并没有十分完美的解决方案,所以一般我们都把数据存放进数据库中(redis)解决此问题。

生成验证码

Kaptcha

  • 导入依赖
  • 编写 Kaptcha 配置类
  • 生成随机字符、生成图片
<dependency>  
    <groupId>com.github.penggle</groupId>  
    <artifactId>kaptcha</artifactId>  
    <version>2.3.2</version>  
</dependency>

配置

在config包下新建KaptchaConfig

@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增加新方法

@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 outputStream = response.getOutputStream();
		ImageIO.write(image,"png",outputStream);
	} catch (IOException e) {
		logger.error("响应验证码获取失败:"+e.getMessage());
	}
}

修改login.html

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

......

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

global.js中

var CONTEXT_PATH = "/community";

登陆、退出

登录

  • 访问登录页面
    • 点击顶部区域内的链接,打开登录页面。
  • 登录
    • 验证账号、密码、验证码。
    • 成功时,生成登录凭证,发放给客户端。
    • 失败时,跳转回登录页。
  • 退出
    • 将登录凭证修改为失效状态。
    • 跳转至网站首页。

登陆凭证目前存到MySQL数据库中

entity包中新建LoginTicket类

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

mapper包中新建LoginTicketMapper接口

@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({  
            "<script>",  
            "update login_ticket set status=#{status} where ticket=#{ticket} ",  
            "<if test=\"ticket!=null\"> ",  
            "and 1=1 ",  
            "</if>",  
            "</script>"  
    })  
    int updateStatus(String ticket, int status);  
}

UserService中添加新方法

@Autowired
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;
	}else 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(user.getStatus());
	loginTicket.setExpired(new Date(System.currentTimeMillis()+expiredSeconds*1000));
	loginTicketMapper.insertLoginTicket(loginTicket);
	map.put("loginTicket",loginTicket.getTicket());
	return map;
}

LoginController中添加新方法

@RequestMapping(path = "/login", method = RequestMethod.POST)  
public String login(String username, String password, String code, boolean rememberMe,  
					Model model, HttpSession session, HttpServletResponse response) {  
	// 检查验证码  
     String kaptcha = (String) session.getAttribute("kaptcha");  

	//redis检查验证码  
	String kaptcha = null;  
	if (StringUtils.isNotBlank(kaptchaOwner)) {  
		String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);  
		kaptcha = (String) redisTemplate.opsForValue().get(redisKey);  
	}  

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

		User user = userService.findUserById((Integer) map.get("userId"));  

		return "redirect:/index";  
	} else {  
		model.addAttribute("usernameMsg", map.get("usernameMsg"));  
		model.addAttribute("passwordMsg", map.get("passwordMsg"));  
		return "/site/login";  
	}  
}

login.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="@{/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}">  
                  密码不能为空  
               </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

@RequestMapping(path = "/logout", method = RequestMethod.GET)  
public String logout(@CookieValue("ticket") String ticket, HttpServletResponse response) {  
  
    //删除ticket cookie  
    Cookie cookie = new Cookie("ticket", "");  
    cookie.setMaxAge(0);  
    response.addCookie(cookie);  
  
    userService.logout(ticket);  
    return "redirect:/login";  
}

登录信息

每次请求携带登录用户的信息,用拦截器实现

  • 拦截器实现

    • 定义拦截器,实现HandlerInterceptor
    • 配置拦截器,为它指定拦截、排除的路径
  • 拦截器应用

    • 在请求开始时查询登录用户
    • 在本次请求中持有用户数据
    • 在模板视图上显示用户数据
    • 在请求结束时清理用户数据

创建interceptor包,新建一个拦截器LoginTicketInterceptor

@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之后执行,把用户信息传递给模板  
    @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);  
        }  
    }  
  
    // 在TemplateEngine之后执行  
    @Override  
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {  
        hostHolder.clear();  
    }  
}

util包下HostHolder类模拟服务端的Session功能,存放是否有登陆的User
ThreadLocal保证线程安全

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

根据key取cookie,封装成一个工具类CookieUtil

public class CookieUtil {  
    public static String getValue(HttpServletRequest request, String name) {  
        if (request == null || name == null) {  
            throw new IllegalArgumentException("参数为空!");  
        }  
        //从Cookie[]中获取指定name的一个cookie  
        Cookie[] cookies = request.getCookies();  
        if (cookies != null) {  
            for (Cookie cookie : cookies) {  
                if (cookie.getName().equals(name)) {  
                    return cookie.getValue();  
                }  
            }  
        }  
        return null;  
    }  
}

在config包下创建配置类WebMvcConfig,配置拦截器

@Configuration  
public class WebMvcConfig implements WebMvcConfigurer {  
    @Autowired  
    private LoginTicketInterceptor loginTicketInterceptor;  

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

index.html中的header

<ul class="navbar-nav mr-auto">  
   <li class="nav-item ml-3 btn-group-vertical">  
      <a class="nav-link" th:href="@{/}">首页</a>  
   </li>  
   <li class="nav-item ml-3 btn-group-vertical">  
      <a class="nav-link position-relative" th:href="@{/letter/list}">  
         消息<span class="badge badge-danger" th:text="${allUnreadCount!=0?allUnreadCount:''}">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" th:href="@{|/user/profile/${loginUser.id}|}">个人主页</a>  
         <a class="dropdown-item text-center" th:href="@{/user/setting}">账号设置</a>  
         <a class="dropdown-item text-center" th:href="@{/logout}">退出登录</a>  
         <div class="dropdown-divider"></div>  
      </div>   
    </li>
</ul>

检查登录状态

  • 使用拦截器
    • 在方法前标注自定义注解
    • 拦截所有请求,只处理带有该注解的方法
    • 自定义注解

解决问题:用户没有登录但是知道url也可直接到相关界面

常用的元注解

  • @Target:声明自定义注解可以作用在什么类型上,类上方法上等
  • @Retention:声明自定义注解保留的时间
  • @Document:声明自定义注解生成文档的时候要不要把注解也带上去
  • @Inherited:用于继承的,一个子类继承父类,父类有注解,子类要不要继承这个注解

读取注解的方法

  • Method.getDeclaredAnnotations()
  • Method.getAnnotation(Class< T > annotationClass)

定义自定义注解,主要作用就是标识

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

新建拦截器LoginRequiredInterceptor进行处理

@Component  
@Deprecated  
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类上加上这个拦截器

@Configuration  
public class WebMvcConfig implements WebMvcConfigurer {  
    @Autowired  
    private LoginTicketInterceptor loginTicketInterceptor;  
    @Autowired  
    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");  
    }  
}

最后对需要拦截的方法上加此注解

账号设置

  • 上传文件
    • 请求:必须是POST请求
    • 表单:enctype=“multipart/form-data”
    • Spring MVC:通过 MultipartFile 处理上传文件

新建UserController类

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

修改index.html文件

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

上传头像

//上传头像  
//@LoginRequired  
@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();  
	model.addAttribute("fileName", fileName);  

	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";  
}
@Update("update user set header_url = #{headerUrl} where id = #{id}")  
int updateHeader(int id, String headerUrl);

获取头像

//服务器路径显示头像  
@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());  
    }  
}

修改密码

//修改密码  
@RequestMapping(value = "/updatePassword", method = RequestMethod.POST)  
public String updatePassword(String oldPassword, String newPassword, Model model) {  
    if (StringUtils.isBlank(newPassword) || StringUtils.isBlank(oldPassword)) {  
        model.addAttribute("oldPasswordMsg", "密码不能为空");  
        return "site/setting";  
    }  
    if (oldPassword.equals(newPassword)) {  
        model.addAttribute("oldPasswordMsg", "新旧密码不能一致");  
        return "site/setting";  
    }  
    User user = hostHolder.getUser();  
    oldPassword = CommunityUtil.md5(oldPassword + user.getSalt());  
    if (!oldPassword.equals(user.getPassword())) {  
        model.addAttribute("oldPasswordMsg", "密码错误");  
        return "site/setting";  
    }  
    newPassword = CommunityUtil.md5(newPassword + user.getSalt());  
    userService.updatePassword(user.getId(), newPassword);  
    user.setPassword(newPassword);  
    hostHolder.setUser(user);  
  
    return "redirect:/index";  
}
@Update("update user set password = #{password} where id = #{id}")  
int updatePassword(int id, String password);

修改setting.html

<div class="main">  
   <div class="container p-5 mt-3 mb-3">  
      <!-- 上传头像 -->  
      <h6 class="text-left text-info border-bottom pb-2">上传头像</h6>  
      <form class="mt-5" method="post" enctype="multipart/form-data" th:action="@{/user/upload}">  
         <div class="form-group row mt-4">  
            <label for="head-image" class="col-sm-2 col-form-label text-right">选择头像:</label>  
            <div class="col-sm-10">  
               <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="文件" th:text="${fileName}">选择一张图片</label>  
                  <div class="invalid-feedback" th:text="${error}">  
                     该账号不存在!  
                  </div>  
               </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>      <!-- 修改密码 -->  
      <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':''}|" 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" class="form-control" 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>   
	</div>
</div>

参考:https://www.cnblogs.com/xiaochenNN/p/14567258.html


文章作者: Aiaa
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Aiaa !
  目录