主要技术架构:

SpringBoot Spring SpringMVC MyBatis Redis Kakfa Elasticsearch Spring Security Spring Actator

1.配置项目环境

在Spring Boot Initializr中或者Idea中初始化一个SpringBoot项目并导出

使用Idea打开导出的项目

2.MyBatis配置

各个层之间的关系如下

在Maven Repository搜索MySql Maven配置文件,在resources文件包内的pom.xml文件中导入相关的配置文件依赖,并在application.properties文件中配置相关的参数。

# ServerProperties
server.port=8080
server.servlet.context-path=/community# ThymeleafProperties
spring.thymeleaf.cache=false# DataSourceProperties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/community?characterEncoding=utf-8&useSSL=false&serverTimezone=Hongkong
#数据库的名称、密码等
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
#最大连接数、超时时间等
spring.datasource.hikari.maximum-pool-size=15
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000# MybatisProperties
#mapper扫描路径
mybatis.mapper-locations=classpath:mapper/*.xml
#在communtiy下创建实体类
mybatis.type-aliases-package=com.nowcoder.community.entity
mybatis.configuration.useGeneratedKeys=true
mybatis.configuration.mapUnderscoreToCamelCase=true

在community文件下创建config entity文件包,在resources文件下创建mapper文件包

在entity文件下创建User类

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;public int getId() {return id;}public void setId(int id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public String getSalt() {return salt;}public void setSalt(String salt) {this.salt = salt;}public String getEmail() {return email;}public void setEmail(String email) {this.email = email;}public int getType() {return type;}public void setType(int type) {this.type = type;}public int getStatus() {return status;}public void setStatus(int status) {this.status = status;}public String getActivationCode() {return activationCode;}public void setActivationCode(String activationCode) {this.activationCode = activationCode;}public String getHeaderUrl() {return headerUrl;}public void setHeaderUrl(String headerUrl) {this.headerUrl = headerUrl;}public Date getCreateTime() {return createTime;}public void setCreateTime(Date createTime) {this.createTime = createTime;}@Overridepublic String toString() {return "User{" +"id=" + id +", username='" + username + ''' +", password='" + password + ''' +", salt='" + salt + ''' +", email='" + email + ''' +", type=" + type +", status=" + status +", activationCode='" + activationCode + ''' +", headerUrl='" + headerUrl + ''' +", createTime=" + createTime +'}';}}

在dao文件下创建UserMapper接口访问数据库

@Mapper
public interface UserMapper {User selectById(int id);User selectByName(String username);User selectByEmail(String email);int insertUser(User user);int updateStatus(int id, int status);int updateHeader(int id, String headerUrl);int updatePassword(int id, String password);}

使用Mapper注解,并在mapper文件下创建user-mapp.xml,使得方法与Sql语句相关联,Mybatis 的xml配置可以在官网找到相关的配置。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nowcoder.community.dao.UserMapper"><sql id="insertFields">username, password, salt, email, type, status, activation_code, header_url, create_time</sql><sql id="selectFields">id, username, password, salt, email, type, status, activation_code, header_url, create_time</sql><select id="selectById" resultType="User">select <include refid="selectFields"></include>from userwhere id = #{id}</select><select id="selectByName" resultType="User">select <include refid="selectFields"></include>from userwhere username = #{username}</select><select id="selectByEmail" resultType="User">select <include refid="selectFields"></include>from userwhere email = #{email}</select><insert id="insertUser" parameterType="User" keyProperty="id">insert into user (<include refid="insertFields"></include>)values(#{username}, #{password}, #{salt}, #{email}, #{type}, #{status}, #{activationCode}, #{headerUrl}, #{createTime})</insert><update id="updateStatus">update user set status = #{status} where id = #{id}</update><update id="updateHeader">update user set header_url = #{headerUrl} where id = #{id}</update><update id="updatePassword">update user set password = #{password} where id = #{id}</update></mapper>

可以使用@Test注解测试相关方法是否正常使用

3.开发社区首页功能:

3.1 开发社区首页显示前10个帖子

在Entity创建DiscussPost类,用于表示发送的相关数据,并在dao文件中创建DiscussPostMapper接口

public class DiscussPost {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;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 getTitle() {return title;}public void setTitle(String title) {this.title = title;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}public int getType() {return type;}public void setType(int type) {this.type = type;}public int getStatus() {return status;}public void setStatus(int status) {this.status = status;}public Date getCreateTime() {return createTime;}public void setCreateTime(Date createTime) {this.createTime = createTime;}public int getCommentCount() {return commentCount;}public void setCommentCount(int commentCount) {this.commentCount = commentCount;}public double getScore() {return score;}public void setScore(double score) {this.score = score;}@Overridepublic String toString() {return "DiscussPost{" +"id=" + id +", userId=" + userId +", title='" + title + ''' +", content='" + content + ''' +", type=" + type +", status=" + status +", createTime=" + createTime +", commentCount=" + commentCount +", score=" + score +'}';}
}@Mapper
public interface DiscussPostMapper {// 考虑到后期分页功能加入offset 和 limit变量// @Param注解用于给参数取别名,// 如果只有一个参数,并且在<if>里使用,则必须加别名.List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit);int selectDiscussPostRows(@Param("userId") int userId);}

在mapper文件下创建相关的xml文件用于数据库操作

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nowcoder.community.dao.DiscussPostMapper"><sql id="selectFields">id, user_id, title, content, type, status, create_time, comment_count, score</sql><select id="selectDiscussPosts" resultType="DiscussPost">select <include refid="selectFields"></include>from discuss_postwhere status != 2<if test="userId!=0">and user_id = #{userId}</if>order by type desc, create_time desclimit #{offset}, #{limit}</select><select id="selectDiscussPostRows" resultType="int">select count(id)from discuss_postwhere status != 2<if test="userId!=0">and user_id = #{userId}</if></select></mapper>

在service文件下创建DiscussPostService类,用于服务层使用(使用@Service注解)

@Service
public class DiscussPostService {@Autowiredprivate 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);}}

将静态资源(css image js等文件)放到static文件下,将模板文件(site index.html)放到templates文件下。

接下来开发视图层,新建一个HomeController在controller文件下使用@Controller注解

//Controller访问路径可以省略
@Controller
public class HomeController {//注入对象@Autowiredprivate DiscussPostService discussPostService;@Autowiredprivate UserService userService;//使用GET方法@RequestMapping(path = "/index", method = RequestMethod.GET)List<DiscussPost> list = discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit());List<Map<String, Object>> discussPosts = new ArrayList<>();if (list != null) {for (DiscussPost post : list) {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对其中相对路径进行更改,并显示相关的帖子。

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

完整的HTML文件如下:

<!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="icon" href="https://static.nowcoder.com/images/logo_87_87.png"/><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>
<body>    <div class="nk-container"><!-- 头部 --><header class="bg-dark sticky-top"><div class="container"><!-- 导航 --><nav class="navbar navbar-expand-lg navbar-dark"><!-- logo --><a class="navbar-brand" href="#"></a><button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><!-- 功能 --><div class="collapse navbar-collapse" id="navbarSupportedContent"><ul class="navbar-nav mr-auto"><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link" href="index.html">首页</a></li><li class="nav-item ml-3 btn-group-vertical"><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"><a class="nav-link" href="site/register.html">注册</a></li><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link" href="site/login.html">登录</a></li><li class="nav-item ml-3 btn-group-vertical dropdown"><a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><img src="http://images.nowcoder.com/head/1t.png" 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" href="site/login.html">退出登录</a><div class="dropdown-divider"></div><span class="dropdown-item text-center text-secondary">nowcoder</span></div></li></ul><!-- 搜索 --><form class="form-inline my-2 my-lg-0" action="site/search.html"><input class="form-control mr-sm-2" type="search" aria-label="Search" /><button class="btn btn-outline-light my-2 my-sm-0" type="submit">搜索</button></form></div></nav></div></header><!-- 内容 --><div class="main"><div class="container"><div class="position-relative"><!-- 筛选条件 --><ul class="nav nav-tabs mb-3"><li class="nav-item"><a class="nav-link active" href="#">最新</a></li><li class="nav-item"><a class="nav-link" href="#">最热</a></li></ul><button type="button" class="btn btn-primary btn-sm position-absolute rt-0" data-toggle="modal" data-target="#publishModal">我要发布</button></div><!-- 弹出框 --><div class="modal fade" id="publishModal" tabindex="-1" role="dialog" aria-labelledby="publishModalLabel" aria-hidden="true"><div class="modal-dialog modal-lg" role="document"><div class="modal-content"><div class="modal-header"><h5 class="modal-title" id="publishModalLabel">新帖发布</h5><button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><div class="modal-body"><form><div class="form-group"><label for="recipient-name" class="col-form-label">标题:</label><input type="text" class="form-control" id="recipient-name"></div><div class="form-group"><label for="message-text" class="col-form-label">正文:</label><textarea class="form-control" id="message-text" rows="15"></textarea></div></form></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button><button type="button" class="btn btn-primary" id="publishBtn">发布</button></div></div></div></div><!-- 提示框 --><div class="modal fade" id="hintModal" tabindex="-1" role="dialog" aria-labelledby="hintModalLabel" aria-hidden="true"><div class="modal-dialog modal-lg" role="document"><div class="modal-content"><div class="modal-header"><h5 class="modal-title" id="hintModalLabel">提示</h5></div><div class="modal-body" id="hintBody">发布完毕!</div></div></div></div><!-- 帖子列表 --><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><!-- 分页 --><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" href="#" 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></div></div><!-- 尾部 --><footer class="bg-dark"><div class="container"><div class="row"><!-- 二维码 --><div class="col-4 qrcode"><img src="https://uploadfiles.nowcoder.com/app/app_download.png" class="img-thumbnail" style="width:136px;" /></div><!-- 公司信息 --><div class="col-8 detail-info"><div class="row"><div class="col"><ul class="nav"><li class="nav-item"><a class="nav-link text-light" href="#">关于我们</a></li><li class="nav-item"><a class="nav-link text-light" href="#">加入我们</a></li><li class="nav-item"><a class="nav-link text-light" href="#">意见反馈</a></li><li class="nav-item"><a class="nav-link text-light" href="#">企业服务</a></li><li class="nav-item"><a class="nav-link text-light" href="#">联系我们</a></li><li class="nav-item"><a class="nav-link text-light" href="#">免责声明</a></li><li class="nav-item"><a class="nav-link text-light" href="#">友情链接</a></li></ul></div></div><div class="row"><div class="col"><ul class="nav btn-group-vertical company-info"><li class="nav-item text-white-50">公司地址:北京市朝阳区大屯路东金泉时代3-2708北京牛客科技有限公司</li><li class="nav-item text-white-50">联系方式:010-60728802(电话)&nbsp;&nbsp;&nbsp;&nbsp;admin@nowcoder.com</li><li class="nav-item text-white-50">牛客科技?2018 All rights reserved</li><li class="nav-item text-white-50">京ICP备14055008号-4 &nbsp;&nbsp;&nbsp;&nbsp;<img src="http://static.nowcoder.com/company/images/res/ghs.png" style="width:18px;" />京公网安备 11010502036488号</li></ul></div></div></div></div></div></footer></div><script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" crossorigin="anonymous"></script><script th:src="@{/js/global.js}"></script><script th:src="@{js/index.js}"></script>
</body>
</html>

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

在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 - limitreturn (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,加入分页的方法。

//Controller访问路径可以省略
@Controller
public class HomeController {//注入对象@Autowiredprivate DiscussPostService discussPostService;@Autowiredprivate UserService userService;//使用GET方法@RequestMapping(path = "/index", method = RequestMethod.GET)public String getIndexPage(Model model, Page page) {// 方法调用钱,SpringMVC会自动实例化Model和Page,并将Page注入Model.// 所以,在thymeleaf中可以直接访问Page对象中的数据.page.setRows(discussPostService.findDiscussPostRows(0));page.setPath("/index");List<DiscussPost> list = discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit());List<Map<String, Object>> discussPosts = new ArrayList<>();if (list != null) {for (DiscussPost post : list) {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" href="#" 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></div></div>

效果图如下:

4.开发社区登录模块

4.1邮件发送

在Maven Repository搜索Spring Mail配置文件并加入到poml文件中,在application.properties文件中配置Mail的参数。

# ServerProperties
server.port=8080
server.servlet.context-path=/community# ThymeleafProperties
spring.thymeleaf.cache=false# DataSourceProperties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/community?characterEncoding=utf-8&useSSL=false&serverTimezone=Hongkong
spring.datasource.username=root
spring.datasource.password=lihonghe
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.maximum-pool-size=15
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000# MybatisProperties
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.nowcoder.community.entity
mybatis.configuration.useGeneratedKeys=true
mybatis.configuration.mapUnderscoreToCamelCase=true# MailProperties
spring.mail.host=smtp.sina.com
spring.mail.port=465
#自己的邮箱
spring.mail.username=nowcoder@sina.com
spring.mail.password=nowcoder123
spring.mail.protocol=smtps
spring.mail.properties.mail.smtp.ssl.enable=true

如果发送不了邮件,需要在个人邮箱的网站设置启用授权码,验证手机,并修改properties文件中关于Email的配置

# MailProperties
spring.mail.host=smtp.sina.com
#spring.mail.port=465
spring.mail.username=nowcoder@sina.com
spring.mail.password=3398c6c71399f9fe
#spring.mail.protocol=smtps
#spring.mail.properties.mail.smtp.ssl.enable=true
spring.mail.properties.mail.smtl.auth=true

在community文件下创建util工具文件包,并在util包中创建MailClient类使用@Component注解,并创建发送邮件的方法。

@Component
public class MailClient {private static final Logger logger = LoggerFactory.getLogger(MailClient.class);@Autowiredprivate 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());}}}

4.2注册功能

在Controller层下创建LoginController类,实现登录界面跳转到注册页面,使用@Controller注解。

@Controller
public class LoginController implements CommunityConstant {@RequestMapping(path = "/register", method = RequestMethod.GET)public String getRegisterPage() {return "/site/register";}@RequestMapping(path = "/login", method = RequestMethod.GET)public String getLoginPage() {return "/site/login";}
}

使用模板引擎thymeleaf修改注册页面regist.html。

<!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="icon" href="https://static.nowcoder.com/images/logo_87_87.png"/><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}" /><link rel="stylesheet" th:href="@{/css/login.css}" /><title>牛客网-注册</title>
</head>
<body><div class="nk-container"><!-- 头部 --><header class="bg-dark sticky-top" th:replace="index::header"><div class="container"><!-- 导航 --><nav class="navbar navbar-expand-lg navbar-dark"><!-- logo --><a class="navbar-brand" href="#"></a><button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><!-- 功能 --><div class="collapse navbar-collapse" id="navbarSupportedContent"><ul class="navbar-nav mr-auto"><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link" href="../index.html">首页</a></li><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link position-relative" href="letter.html">消息<span class="badge badge-danger">12</span></a></li><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link" href="register.html">注册</a></li><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link" href="login.html">登录</a></li><li class="nav-item ml-3 btn-group-vertical dropdown"><a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><img src="http://images.nowcoder.com/head/1t.png" class="rounded-circle" style="width:30px;"/></a><div class="dropdown-menu" aria-labelledby="navbarDropdown"><a class="dropdown-item text-center" href="profile.html">个人主页</a><a class="dropdown-item text-center" href="setting.html">账号设置</a><a class="dropdown-item text-center" href="login.html">退出登录</a><div class="dropdown-divider"></div><span class="dropdown-item text-center text-secondary">nowcoder</span></div></li></ul><!-- 搜索 --><form class="form-inline my-2 my-lg-0" action="search.html"><input class="form-control mr-sm-2" type="search" aria-label="Search" /><button class="btn btn-outline-light my-2 my-sm-0" type="submit">搜索</button></form></div></nav></div></header><!-- 内容 --><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><!-- 尾部 --><footer class="bg-dark"><div class="container"><div class="row"><!-- 二维码 --><div class="col-4 qrcode"><img src="https://uploadfiles.nowcoder.com/app/app_download.png" class="img-thumbnail" style="width:136px;" /></div><!-- 公司信息 --><div class="col-8 detail-info"><div class="row"><div class="col"><ul class="nav"><li class="nav-item"><a class="nav-link text-light" href="#">关于我们</a></li><li class="nav-item"><a class="nav-link text-light" href="#">加入我们</a></li><li class="nav-item"><a class="nav-link text-light" href="#">意见反馈</a></li><li class="nav-item"><a class="nav-link text-light" href="#">企业服务</a></li><li class="nav-item"><a class="nav-link text-light" href="#">联系我们</a></li><li class="nav-item"><a class="nav-link text-light" href="#">免责声明</a></li><li class="nav-item"><a class="nav-link text-light" href="#">友情链接</a></li></ul></div></div><div class="row"><div class="col"><ul class="nav btn-group-vertical company-info"><li class="nav-item text-white-50">公司地址:北京市朝阳区大屯路东金泉时代3-2708北京牛客科技有限公司</li><li class="nav-item text-white-50">联系方式:010-60728802(电话)&nbsp;&nbsp;&nbsp;&nbsp;admin@nowcoder.com</li><li class="nav-item text-white-50">牛客科技?2018 All rights reserved</li><li class="nav-item text-white-50">京ICP备14055008号-4 &nbsp;&nbsp;&nbsp;&nbsp;<img src="http://static.nowcoder.com/company/images/res/ghs.png" style="width:18px;" />京公网安备 11010502036488号</li></ul></div></div></div></div></div></footer></div><script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" crossorigin="anonymous"></script><script th:src="@{/js/global.js}"></script><script th:src="@{/js/register.js}"></script>
</body>
</html>

在Maven Repository搜索commons lang配置文件并加入到poml文件中。在properties文件中加入community的路径。

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

在util中添加CommunityUtil工具类,方便生成密码。

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

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

@Service
public class UserService implements CommunityConstant {@Autowiredprivate UserMapper userMapper;@Autowiredprivate MailClient mailClient;@Autowiredprivate TemplateEngine templateEngine;@Value("${community.path.domain}")private String domain;@Value("${server.servlet.context-path}")private String contextPath;@Autowiredprivate LoginTicketMapper loginTicketMapper;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/codeString 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;}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;}}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;}}<!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,处理注册完成的请求。

@Controller
public class LoginController implements CommunityConstant {private static final Logger logger = LoggerFactory.getLogger(LoginController.class);@Autowiredprivate UserService userService;@Autowiredprivate Producer kaptchaProducer;@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";}}}

更新operate-result页面。

<!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="icon" href="https://static.nowcoder.com/images/logo_87_87.png"/><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>
<body class="bg-white"><div class="nk-container"><!-- 头部 --><header class="bg-dark sticky-top" th:replace="index::header"><div class="container"><!-- 导航 --><nav class="navbar navbar-expand-lg navbar-dark"><!-- logo --><a class="navbar-brand" href="#"></a><button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><!-- 功能 --><div class="collapse navbar-collapse" id="navbarSupportedContent"><ul class="navbar-nav mr-auto"><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link" href="../index.html">首页</a></li><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link position-relative" href="letter.html">消息<span class="badge badge-danger">12</span></a></li><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link" href="register.html">注册</a></li><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link" href="login.html">登录</a></li><li class="nav-item ml-3 btn-group-vertical dropdown"><a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><img src="http://images.nowcoder.com/head/1t.png" class="rounded-circle" style="width:30px;"/></a><div class="dropdown-menu" aria-labelledby="navbarDropdown"><a class="dropdown-item text-center" href="profile.html">个人主页</a><a class="dropdown-item text-center" href="setting.html">账号设置</a><a class="dropdown-item text-center" href="login.html">退出登录</a><div class="dropdown-divider"></div><span class="dropdown-item text-center text-secondary">nowcoder</span></div></li></ul><!-- 搜索 --><form class="form-inline my-2 my-lg-0" action="search.html"><input class="form-control mr-sm-2" type="search" aria-label="Search" /><button class="btn btn-outline-light my-2 my-sm-0" type="submit">搜索</button></form></div></nav></div></header><!-- 内容 --><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><!-- 尾部 --><footer class="bg-dark"><div class="container"><div class="row"><!-- 二维码 --><div class="col-4 qrcode"><img src="https://uploadfiles.nowcoder.com/app/app_download.png" class="img-thumbnail" style="width:136px;" /></div><!-- 公司信息 --><div class="col-8 detail-info"><div class="row"><div class="col"><ul class="nav"><li class="nav-item"><a class="nav-link text-light" href="#">关于我们</a></li><li class="nav-item"><a class="nav-link text-light" href="#">加入我们</a></li><li class="nav-item"><a class="nav-link text-light" href="#">意见反馈</a></li><li class="nav-item"><a class="nav-link text-light" href="#">企业服务</a></li><li class="nav-item"><a class="nav-link text-light" href="#">联系我们</a></li><li class="nav-item"><a class="nav-link text-light" href="#">免责声明</a></li><li class="nav-item"><a class="nav-link text-light" href="#">友情链接</a></li></ul></div></div><div class="row"><div class="col"><ul class="nav btn-group-vertical company-info"><li class="nav-item text-white-50">公司地址:北京市朝阳区大屯路东金泉时代3-2708北京牛客科技有限公司</li><li class="nav-item text-white-50">联系方式:010-60728802(电话)&nbsp;&nbsp;&nbsp;&nbsp;admin@nowcoder.com</li><li class="nav-item text-white-50">牛客科技?2018 All rights reserved</li><li class="nav-item text-white-50">京ICP备14055008号-4 &nbsp;&nbsp;&nbsp;&nbsp;<img src="http://static.nowcoder.com/company/images/res/ghs.png" style="width:18px;" />京公网安备 11010502036488号</li></ul></div></div></div></div></div></footer></div><script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" crossorigin="anonymous"></script><script>$(function(){setInterval(function(){var seconds = $("#seconds").text();$("#seconds").text(--seconds);if(seconds == 0) {location.href = $("#target").attr("href");}}, 1000);});</script>
</body>
</html>

在utill中创建一个CommunityConstant接口,用于表示注册状态码。

public interface CommunityConstant {/*** 激活成功*/int ACTIVATION_SUCCESS = 0;/*** 重复激活*/int ACTIVATION_REPEAT = 1;/*** 激活失败*/int ACTIVATION_FAILURE = 2;/*** 默认状态的登录凭证的超时时间*/int DEFAULT_EXPIRED_SECONDS = 3600 * 12;/*** 记住状态的登录凭证超时时间*/int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;}

更新LoginController的功能,并更改Index.html文件相对应位置的模板参数。

@Controller
public class LoginController implements CommunityConstant {private static final Logger logger = LoggerFactory.getLogger(LoginController.class);@Autowiredprivate UserService userService;@Autowiredprivate Producer kaptchaProducer;@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";}}// 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 == 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";}}

4.3会话管理

使用Cookie保存一些信息,可以使用浏览器插件查看Cookie(F12控制台)

有关Cookie的一个小示例:

// cookie示例@RequestMapping(path = "/cookie/set", method = RequestMethod.GET)@ResponseBodypublic String setCookie(HttpServletResponse response) {// 创建cookieCookie cookie = new Cookie("code", CommunityUtil.generateUUID());// 设置cookie生效的范围cookie.setPath("/community/alpha");// 设置cookie的生存时间cookie.setMaxAge(60 * 10);// 发送cookieresponse.addCookie(cookie);return "set cookie";}@RequestMapping(path = "/cookie/get", method = RequestMethod.GET)@ResponseBodypublic String getCookie(@CookieValue("code") String code) {System.out.println(code);return "get cookie";}// session示例@RequestMapping(path = "/session/set", method = RequestMethod.GET)@ResponseBodypublic String setSession(HttpSession session) {session.setAttribute("id", 1);session.setAttribute("name", "Test");return "set session";}@RequestMapping(path = "/session/get", method = RequestMethod.GET)@ResponseBodypublic String getSession(HttpSession session) {System.out.println(session.getAttribute("id"));System.out.println(session.getAttribute("name"));return "get session";}

cookie和session的区别

后期可以考虑将Session数据传输到redis数据库中,用于保存一些登陆凭证

4.4生成验证码

验证码使用Kaptcha jar包用于随机生成字符和图片。在Maven Repository搜索Kaptcha配置文件,在resources文件包内的pom.xml文件中导入相关的配置文件依赖。在community目录下创建config文件包并创建KaptchaConfig配置类,使用@Configuration注解。

@Configuration
public class KaptchaConfig {@Beanpublic 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文件中完善验证码功能,并完善登陆页面.html文件中验证码的模板,并实现刷新验证码的功能。

 @RequestMapping(path = "/kaptcha", method = RequestMethod.GET)public void getKaptcha(HttpServletResponse response, HttpSession session) {// 生成验证码String text = kaptchaProducer.createText();BufferedImage image = kaptchaProducer.createImage(text);// 将验证码存入sessionsession.setAttribute("kaptcha", text);// 将突图片输出给浏览器response.setContentType("image/png");try {OutputStream os = response.getOutputStream();ImageIO.write(image, "png", os);} catch (IOException e) {logger.error("响应验证码失败:" + e.getMessage());}}@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";}

刷新验证码方法:

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

4.5登录、退出功能

密码使用MD5加密

数据库中有关于登录凭证的ticket,使用这个ticket作为登陆凭证。涉及到数据库的操作,就要处理LoginTicketMapper。

LoginTicketMapper(这里是mapper的另外一种写法,不用再resource里面创建mapper文件):

@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中增加登陆的方法,并在login.html中增加相应的修改:

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

增加Controller层中的登录方法:

@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";}}@RequestMapping(path = "/logout", method = RequestMethod.GET)public String logout(@CookieValue("ticket") String ticket) {userService.logout(ticket);return "redirect:/login";}

4.6显示登陆信息

使用拦截器实现

在controller层下创建Interceptor文件包,

拦截器的方法

 // 在Controller之前执行@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {logger.debug("preHandle: " + handler.toString());return true;}// 在Controller之后执行@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {logger.debug("postHandle: " + handler.toString());}// 在TemplateEngine之后执行@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {logger.debug("afterCompletion: " + handler.toString());}

在cpnfig文件下创建配置类WebMvcConfig,配置拦截器。

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {//测试拦截器方法@Autowiredprivate AlphaInterceptor alphaInterceptor;@Autowiredprivate LoginTicketInterceptor loginTicketInterceptor;@Autowiredprivate LoginRequiredInterceptor loginRequiredInterceptor;@Overridepublic 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");}}

在请求开始之前查询登录用户:

在Interceptor文件下创建LoginTicketInterceptor,实现拦截器的方法。

@Component
public class LoginTicketInterceptor implements HandlerInterceptor {@Autowiredprivate UserService userService;@Autowiredprivate HostHolder hostHolder;@Overridepublic 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;}@Overridepublic 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);}}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {hostHolder.clear();}
}

在Util文件中创建Cookie工具,以及HostHolder工具用于代替session对象。完成过后修改相应的html文件。

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;}}/*** 持有用户信息,用于代替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();}}

4.7账号设置

用户自己上传头像,请求必须是POST请求,表单:enctype = “multipart/form-data”,SpringMVC通过MutipartFile上传文件。

创建Usercontroller

@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;@Autowiredprivate UserService userService;@Autowiredprivate HostHolder hostHolder;@LoginRequired@RequestMapping(path = "/setting", method = RequestMethod.GET)public String getSettingPage() {return "/site/setting";}@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();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.pngUser 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的静态资源路径

<!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="icon" href="https://static.nowcoder.com/images/logo_87_87.png"/><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}" /><link rel="stylesheet" th:href="@{/css/login.css}" /><title>牛客网-账号设置</title>
</head>
<body><div class="nk-container"><!-- 头部 --><header class="bg-dark sticky-top" th:replace="index::header"><div class="container"><!-- 导航 --><nav class="navbar navbar-expand-lg navbar-dark"><!-- logo --><a class="navbar-brand" href="#"></a><button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><!-- 功能 --><div class="collapse navbar-collapse" id="navbarSupportedContent"><ul class="navbar-nav mr-auto"><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link" href="../index.html">首页</a></li><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link position-relative" href="letter.html">消息<span class="badge badge-danger">12</span></a></li><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link" href="register.html">注册</a></li><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link" href="login.html">登录</a></li><li class="nav-item ml-3 btn-group-vertical dropdown"><a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><img src="http://images.nowcoder.com/head/1t.png" class="rounded-circle" style="width:30px;"/></a><div class="dropdown-menu" aria-labelledby="navbarDropdown"><a class="dropdown-item text-center" href="profile.html">个人主页</a><a class="dropdown-item text-center" href="setting.html">账号设置</a><a class="dropdown-item text-center" href="login.html">退出登录</a><div class="dropdown-divider"></div><span class="dropdown-item text-center text-secondary">nowcoder</span></div></li></ul><!-- 搜索 --><form class="form-inline my-2 my-lg-0" action="search.html"><input class="form-control mr-sm-2" type="search" aria-label="Search" /><button class="btn btn-outline-light my-2 my-sm-0" type="submit">搜索</button></form></div></nav></div></header><!-- 内容 --><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="文件">选择一张图片</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">                <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" class="form-control" id="old-password" placeholder="请输入原始密码!" required><div class="invalid-feedback">密码长度不能小于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" 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><!-- 尾部 --><footer class="bg-dark"><div class="container"><div class="row"><!-- 二维码 --><div class="col-4 qrcode"><img src="https://uploadfiles.nowcoder.com/app/app_download.png" class="img-thumbnail" style="width:136px;" /></div><!-- 公司信息 --><div class="col-8 detail-info"><div class="row"><div class="col"><ul class="nav"><li class="nav-item"><a class="nav-link text-light" href="#">关于我们</a></li><li class="nav-item"><a class="nav-link text-light" href="#">加入我们</a></li><li class="nav-item"><a class="nav-link text-light" href="#">意见反馈</a></li><li class="nav-item"><a class="nav-link text-light" href="#">企业服务</a></li><li class="nav-item"><a class="nav-link text-light" href="#">联系我们</a></li><li class="nav-item"><a class="nav-link text-light" href="#">免责声明</a></li><li class="nav-item"><a class="nav-link text-light" href="#">友情链接</a></li></ul></div></div><div class="row"><div class="col"><ul class="nav btn-group-vertical company-info"><li class="nav-item text-white-50">公司地址:北京市朝阳区大屯路东金泉时代3-2708北京牛客科技有限公司</li><li class="nav-item text-white-50">联系方式:010-60728802(电话)&nbsp;&nbsp;&nbsp;&nbsp;admin@nowcoder.com</li><li class="nav-item text-white-50">牛客科技?2018 All rights reserved</li><li class="nav-item text-white-50">京ICP备14055008号-4 &nbsp;&nbsp;&nbsp;&nbsp;<img src="http://static.nowcoder.com/company/images/res/ghs.png" style="width:18px;" />京公网安备 11010502036488号</li></ul></div></div></div></div></div></footer></div><script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/bs-custom-file-input/dist/bs-custom-file-input.js" crossorigin="anonymous"></script><script th:src="@{/js/global.js}"></script><script>$(function(){bsCustomFileInput.init();});</script>
</body>
</html>

修改配置文件中,上传文件的保存位置。

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

4.8检查登陆状态

防止未登录访问某些资源,可以使用拦截器和注解。

使用注解,在community创建annotation文件创建Login注解。

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

给Usercontroller中的方法,加入@LoginRequire注解,并创建拦截器拦截带注解的方法。

创建一个新的拦截器LoginRequireInterceptor。

@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {@Autowiredprivate HostHolder hostHolder;@Overridepublic 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;}
}

项目代码及相关资源:Ming-XMU (Yiming Zhang) · GitHub

麻烦点点小星星!!!!!!

CSDN下载需要积分基于SpringBoot仿牛客网讨论社区项目-Java文档类资源-CSDN下载

从零开始—仿牛客网讨论社区项目(一)_芙蓉铁蛋的博客-CSDN博客

?从零开始—仿牛客网讨论社区项目(二)_芙蓉铁蛋的博客-CSDN博客

从零开始—仿牛客网讨论社区项目(三)_芙蓉铁蛋的博客-CSDN博客

从零开始—仿牛客网讨论社区项目(四)_芙蓉铁蛋的博客-CSDN博客

从零开始—仿牛客网讨论社区项目(五)_芙蓉铁蛋的博客-CSDN博客

从零开始—仿牛客网讨论社区项目(六)_芙蓉铁蛋的博客-CSDN博客

仿牛客网讨论社区项目—优化网站性能_芙蓉铁蛋的博客-CSDN博客

仿牛客网讨论社区项目—项目总结及项目常见面试题_芙蓉铁蛋的博客-CSDN博客

先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

从零开始—仿牛客网讨论社区项目(一)相关推荐

  1. 从零开始—仿牛客网讨论社区项目(六)

    主要技术架构: SpringBoot Spring SpringMVC MyBatis Redis Kakfa Elasticsearch Spring Security Spring Actator ...

  2. 仿牛客网讨论社区项目—优化网站性能

    性能优化: 1.考虑加入缓存优化 优化热门帖子列表 GitHub中搜索caffeine 在Maven Repository搜索caffeine配置文件,在resources文件包内的pom.xml文件 ...

  3. Java牛客项目课_仿牛客网讨论区_第八章

    文章目录 第八章.项目发布与总结 8.1. 单元测试 8.2.项目监控 8.3.项目部署 宝塔面板.yum.rpm.压缩包 安装 unzip.Java1.8.Maven.MySQL.Redis.Kaf ...

  4. 框架复习笔记-Java-案例:牛客网讨论社区

    文章目录 搭建开发环境 Spring入门 Spring IoC实例分析 Spring MVC入门 请求 响应 Mybatis入门 案例:开发者社区首页 调试技巧 日志 版本控制-Git 发送邮件 开发 ...

  5. 【仿牛客网笔记】项目进阶,构建安全高效的企业服务——热帖排行

    p:投票数 T:发布时间间隔 G:系数,通常为1.5,1.8 计算帖子的分数 注入RedisTemplate 帖子刷新 实现定时任务 刷新帖子 实现更新帖子分数 刷新帖子分数任务 配置Trigger ...

  6. 仿牛客网社区项目 全栈总结

    学习仿牛客网社区项目 代码&资源 各章节总结 第一章 第二章 第三章 第四章 第五章 第六章 第七章 第八章 争取让每个知识点都有链接可点 项目总结 网站架构图 常见面试题 MySQL Red ...

  7. 云服务器上部署仿牛客网项目

    云服务器上部署仿牛客网项目 安装JRE 安装Maven 安装MySQL 给mysql导入数据 安装Redis 安装kafka 安装ElasticSearch Wkhtmltopdf 安装tomcat ...

  8. 2021-04-10 仿牛客网第六章

    一.Elasticsearch入门 仿牛客网 内容部分引用至 https://blog.csdn.net/weixin_44406146 目录 一.Elasticsearch入门 Elasticsea ...

  9. 【牛客网C++服务器项目学习】-Day09-网络模型个人总结

    项目学习地址:[牛客网C++服务器项目学习] day09 项目学习进入了第四章--网络编程.这一章的学习,前半段是对计算机网络体系进行一个大致的讲解.我自己在今年4月份系统性的看了<计算机网络自 ...

最新文章

  1. java swing 外观框架_【GUI】一、Swing外观框架BeautyEye使用
  2. java 线程假醒_Java并发基础05. 传统线程同步通信技术
  3. jquery的each()详细介绍
  4. 深入理解文档/视图框架体系_九宫格项目开发感悟
  5. scratch跳一跳游戏脚本_涂鸦骑士3D版强势屠榜,腾讯跳一跳“宝刀未老” | 休闲新游周报...
  6. POJ 3984 迷宫问题 (Dijkstra)
  7. java socket 读不到数据_Java Socket通信以及可能出现的问题解决
  8. mysql在线修复主从同步
  9. 【热门主题:银魂win7主题】
  10. 64位win10安装不了64位java(点了安装没反应)
  11. 排版怎么排?八大技巧提升版面设计感
  12. gif一键抠图 在线_在线抠图网站,轻松搞定抠图,效果堪比PS!
  13. 前端开发学习(七七)
  14. QT 科学计算器与谷歌拼音输入法实现
  15. dell服务器系统备份软件,使用 AlienRespawn 备份 Alienware 电脑的系统
  16. RM-MEDA的理解与分析
  17. 解决C# WPF的xaml突然报错XDG000 标签全不识别
  18. 如何获取微信公众平台图文消息的永久链接
  19. 开源星空照片_如何拍摄星空的好照片
  20. ciscn_2019_n_7(exit_hook)、wdb_2018_1st_babyheap(fsop的例子)

热门文章

  1. Python实现替换照片人物背景,精细到头发丝(附上代码) | 机器学习
  2. 【Bilibili视频嵌入技巧】如何嵌入720PBilibili视频
  3. JavaScript 教程「9」:DOM 元素获取、属性修改
  4. Armijo条件,Wolfe条件,Goldstein条件
  5. lenovo L480 进入bios_梅捷主板如何在bios中设置u盘启动【详细步骤】
  6. 巧推网站seo优化推动网站快速排名
  7. aspx连接mysql木马_让你变成ASP木马高手_安全教程_脚本之家
  8. 赛尔号桌面版_4399赛尔号官方版下载-4399赛尔号经典版电脑版 - 极光下载站
  9. Oracle EBS AP预付款发票核销可用金额查询SQL
  10. DDN4.9实践 - Source版的安装