主页讨论区分页查询功能!
1.首先设计Dao层接口(实体类略)
以下是查询功能不包括分页 (其中userId在DiscussPost类中作为外键)
//查询
//userId=0为所有帖子,1为我的帖子
//每个参数必须加@Param("")
List<DiscussPost> selectDiscussPosts(@Param("userId") int userId,@Param("offset")int offset,@Param("limit")int limit);
//为分页查询服务的查询总条数
//给参数起别名,如果只有一个参数并且要在<if>里使用,则必须加别名
int selectDiscussRows(@Param("userId")int userId);
<!------------- Mapper.xml ------------->
<sql id="selectFields">
id,user_id,title,content,type,status,create_time,comment_count,score
</sql>
<sql id="insertFields">
user_id,title,content,type,status,create_time,comment_count,score
</sql>
<!--查询不是被拉黑的帖子并且userId不为0按照type指定,时间排序-->
<select id="selectDiscussPosts" resultType="DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where status!=2
<if test="userId!=0">
and user_id=#{userId}
</if>
<if test="orderMode==0">
order by type desc,create_time desc
</if>
<if test="orderMode==1">
order by type desc,score desc,create_time desc
</if>
limit #{offset},#{limit}
</select>
<!--userId=0查所有;userId!=0查个人发帖数-->
<select id="selectDiscussRows" resultType="int">
select count(id)
from discuss_post
where status!=2
<if test="userId!=0">
and user_id=#{userId}
</if>
</select>
2.然后设计Service层调用Dao层接口
@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.selectDiscussRows(userId);
}
3.其次封装分页功能
封装分页功能相关信息在Page类!!
public class Page {
//当前页面
private int current=1;
//显示上限
private int limit=6;
//数据总数(用于计算总页数)
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;
}
/** 获取当前页的起始行**/
public int getOffset(){
//current*limit-limit
return (current-1)*limit;
}
/**获取总页数**/
public int getTotal(){
//rows/limit[+1]
if (rows%limit==0){
return rows/limit;
}else{
return rows/limit+1;
}
}
/**获取起始页码**/
public int getFrom(){
int from=current-2;
return from < 1 ? 1 : from;
}
/**获取结束页码**/
public int getTo(){
int to=current+2;
int total=getTotal();
return to > total ? total : to;
}
}
4.最后设计Controller层
@Autowired
private DiscussPostService discussPostService;
@Autowired
private UserService userService;
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String getIndexPage(Model model, Page page){//传入model参数是因为要返回值给View
/*方法调用前,springMVC自动实例化Model和Page,并将Page注入Model
在thymeleaf中可以直接访问Page对象中的数据 */
//分页
page.setRows(discussPostService.findDiscussPostRows(0));
page.setPath("/community/index");
//查询所有,起始为page.getOffset(),终止为page.getLimit()个帖子,
List<DiscussPost> list=discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit());
/*将查询的post帖子和user用户名拼接后放入map中,最后把全部map放入新的List中,
因为UserId是外键,需要显示的是对应的名字即可 */
List<Map<String,Object>> discussPost =new ArrayList<>();
if (list!=null){
for(DiscussPost post:list){
HashMap<String, Object> map = new HashMap<>();
// 将查询到的帖子放入map
map.put("post",post);
// 将发布帖子对应的用户id作为参数
User user = userService.findUser(post.getUserId());
// 将发帖子的所有用户放入map
map.put("user",user);
// 显示帖子点赞数量
long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId());
map.put("likeCount", likeCount);
//将组合的map放入List<>
discussPost.add(map);
}
}
model.addAttribute("discussPosts",discussPost);
return "/index";
}
5.前端页面设计(Thymeleaf)
5.1查询页面
<!-- 帖子列表 -->
<ul class="list-unstyled">
<!--th:each="map:${discussPosts}循环遍历model.addAttribute传过来的discussPosts这个集合,每次循环得到map对象-->
<li class="media pb-3 pt-3 mb-3 border-bottom" th:each="map:${discussPosts}">
<a href="site/profile.html">
<!--th:src="${map.user.headerUrl}"底层是map.get("user")->user.get("headerUrl")-->
<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">
<!--th:utext可以转义文本中特殊字符-->
<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">
<!--th:text="${#dates.format(map.post.createTime)} #是引用thymeleaf自带的工具-->
<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>
5.2分页功能页面
<!-- 分页 -->
<nav class="mt-5" th:if="${page.rows>0}" th:fragment="pagination">
<ul class="pagination justify-content-center">
<li class="page-item">
<!--th:href="@{${page.path}(current=1,limit=5)}"等效于/index?current=1&limit=5-->
<a class="page-link" th:href="@{${page.path}(current=1)}">首页</a>
</li>
<!--th:class="|page-item ${page.current==1?'disabled':''}|" 动态上一页禁用 固定数据+变量使用方法:加|| -->
<li th:class="|page-item ${page.current==1?'disabled':''}|">
<a class="page-link" th:href="@{${page.path}(current=${page.current-1})}">上一页</a>
</li>
<!--#numbers.sequence #调用thymelead自带工具numbers,从from到to的数组-->
<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>
注册登录功能
发送邮件
1.邮箱设置:启用SMTP服务
2.SpringEmail
2.1配置xml文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>2.6.6</version>
</dependency>
2.2在application.yml配置邮箱参数
# 配置邮箱
spring:
mail:
host: smtp.qq.com
port: 465
username: xxx@qq.com //本网站的发送方
password: xxx //密码为生成授权码后给的密码
protocol: smtps
2.3创建MailClient邮箱工具类
@Component
public class MailClient {
private static final Logger logger= LoggerFactory.getLogger(MailClient.class);
@Autowired
private JavaMailSender javaMailSender;
@Value("${spring.mail.username}")//将yml的属性注入到from
private String from;
public void sendMail(String to,String subject,String content){
try {
//MimeMessage用于封装邮件相关信息
MimeMessage message = javaMailSender.createMimeMessage();
//需要一个邮件帮助器,负责构建MimeMessage对象
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
//支持HTML文本
helper.setText(content,true);
//发送邮件都有JavaMailSender来做
javaMailSender.send(helper.getMimeMessage());
}catch (MessagingException e){
logger.error("发送邮件失败:"+e.getMessage());
}
}
}
2.4测试类
@Autowired
private MailClient mailClient;
@Autowired
private TemplateEngine templateEngine;//注入HTML模板引擎类,模板格式化
@Test
public void testTextMail(){//发送文本类型邮件
mailClient.sendMail("xmy981022@163.com","Test","Welcome");
}
@Test
public void testHTMLMail(){//发送thymeleaf html类型文件
Context context = new Context();
context.setVariable("username","Nevermore");
String content = templateEngine.process("/mail/activation", context);
mailClient.sendMail("xmy981022@163.com","HTML",content);
}
注意:JavaMailSender和TemplateEngine会被自动注入到spring中
注册功能
1.配置application.properties文件
community.path.domain: http://localhost:8080
server.servlet.context-path: /community
2.创建工具类(处理MD5加密、生成随机数、激活标志接口)
public class CommunityUtil {
/*
* 生成随机字符串
* 用于邮件激活码,salt5位随机数加密
**/
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;
}
//MD5加密方法
return DigestUtils.md5DigestAsHex(key.getBytes());
//参数是bytes型
}
}
public interface CommuityConstant {
/* 以下用于注册功能 */
/** 激活成功*/
int ACTIVATION_SUCCESS=0;
/** 重复激活 */
int ACTIVATION_REPEAT=1;
/** 激活失败 */
int ACTIVATION_FAILURE=2;
/* 以下用于登录功能* /
/**
* 默认状态的登录凭证的超时时间
*/
int DEFAULT_EXPIRED_SECONDS=3600*12;
/**
* 记住状态的登录凭证超时时间
*/
int REMEMBER_EXPIRED_SECONDS=3600*24*7;
}
3.编写Service业务层(实现CommuityConstant接口)
3.1注册业务
//..注入userMapper,mailClient,templateEngine
@Value("${community.path.domain}")
private String domain;
@Value("${server.servlet.context-path}")
private String contextPath;
//注册功能
/**为什么返回的是Map类型,因为用Map来存各种情况下的信息,返回给前端页面* */
public Map<String,Object> register(User user){
HashMap<String, Object> map = new HashMap<>();
/*
判输入
*/
if (user == null) {
throw new IllegalArgumentException("参数不能为空!");
}
if (StringUtils.isBlank(user.getUsername())){
map.put("usernameMsg","账户不能为空");
}
if (StringUtils.isBlank(user.getPassword())){
map.put("passwordMsg","密码不能为空");
}
if (StringUtils.isBlank(user.getEmail())){
map.put("emailMsg","邮箱不能为空");
}
/*
判存在
*/
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;
}
/*
注册账户
1.设置salt加密(随机5位数加入密码)
2.设置密码+salt
3.设置随机数激活码
4.设置status,type=0,时间
5.设置头像(动态)
user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png",new Random().nextInt(1000))
*/
user.setSalt(CommunityUtil.generateUUID().substring(0,5));
user.setPassword(CommunityUtil.md5(user.getPassword()+user.getSalt()));
user.setActivationCode(CommunityUtil.generateUUID());
user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000)));
user.setStatus(0);
user.setType(0);
user.setCreateTime(new Date());
userMapper.insertUser(user);
/*
激活邮件
1.创建Context对象-->context.setVariable(name,value)将name传入前端
为thymeleaf提供变量
2.设置email和url
3.templateEngine.process执行相应HTML
4.发送邮件
*/
Context context = new Context();
context.setVariable("email",user.getEmail());
//http://localhost:8080/community/activation/101/code激活链接
String url=domain+contextPath+"/activation/"+user.getId()+"/"+user.getActivationCode();
context.setVariable("url",url);
String content = templateEngine.process("/mail/activation", context);
mailClient.sendMail(user.getEmail(),"激活账号",content);
return map;
}
3.2激活邮件业务
/**激活邮件功能* */
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;
}
}
4.编写Controller层
//注册Controller
@RequestMapping(value = "/register",method = RequestMethod.POST)
public String register(Model model, User user){
Map<String, Object> map = userService.register(user);
if (map == null || map.isEmpty()){
map.put("msg","注册成功,我们已经向您的邮件发送了一封激活邮件,请尽快激活!");
map.put("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";
}
}
/**激活邮件Controller**/
//http://localhost:8080/community/activation/101/code激活链接
@RequestMapping(value = "/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";
}
5.编写前端Thymeleaf页面核心点
/**注册页面 */
<form class="mt-5" method="post" th:action="@{/register}">
<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的显示与is-invalid有关-->
</div>
<button type="submit" class="btn btn-info text-white form-control">立即注册</button>
</div>
</form>
/**账号激活中间页* */
<div class="jumbotron">
<p class="lead" th:text="${msg}">激活状态信息</p>
<p>
系统会在 <span id="seconds" class="text-danger">8</span> 秒后自动跳转,
您也可以点此 <a id="target" th:href="@{${target}}" class="text-primary">链接</a>, 手动跳转!
</p>
</div>
<!--自动跳转Js -->
<script>
$(function(){
setInterval(function(){
var seconds = $("#seconds").text();
$("#seconds").text(--seconds);
if(seconds == 0) {
location.href = $("#target").attr("href");
}
}, 1000);
});
</script>
/**邮箱模板页* */
<div>
<p><b th:text="${email}">xxx@xxx.com</b>, 您好!</p>
<p>
您正在注册xxx, 这是一封激活邮件, 请点击
<a th:href="${url}">此链接</a>,
激活您的xxx账号!
</p>
</div>
生成验证码
参考网站 :code.google.com/archive/p/k…
注意:1.Producer是Kaptcha的核心接口 2.DefaultKaptcha是Kaptcha核心接口的默认实现类
3.Spring Boot没有为Kaptcha提供自动配置
1.引入pom.xml
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
2.创建配置类装配第三方bean
@Configuration
public class KaptchaConfig {
@Bean
public Producer KaptchaProducer(){
/**
* 手动创建properties.xml配置文件对象*
* 设置验证码图片的样式,大小,高度,边框,字体等
*/
Properties properties=new Properties();
properties.setProperty("kaptcha.border", "yes");
properties.setProperty("kaptcha.border.color", "105,179,90");
properties.setProperty("kaptcha.textproducer.font.color", "black");
properties.setProperty("kaptcha.image.width", "110");
properties.setProperty("kaptcha.image.height", "40");
properties.setProperty("kaptcha.textproducer.font.size", "32");
properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
properties.setProperty("kaptcha.textproducer.char.length", "4");
properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑");
DefaultKaptcha Kaptcha=new DefaultKaptcha();
Config config=new Config(properties);
Kaptcha.setConfig(config);
return Kaptcha;
}
}
3.编写Controller接口
@RequestMapping(value = "/kaptcha",method = RequestMethod.GET)
public void getKaptcha(HttpServletResponse response, HttpSession session){
//生成验证码
String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);
//将验证码存入session
session.setAttribute("kaptcha",text);
//将图片输出给浏览器
response.setContentType("image/png");
try {
ServletOutputStream os = response.getOutputStream();
ImageIO.write(image,"png",os);
}catch (IOException e){
logger.error("响应验证码失败:"+e.getMessage());
}
}
4.Thymeleaf前端页面核心点
<div class="col-sm-4">
<img th:src="@{/kaptcha}" id="kaptchaImage" style="width:100px;height:40px;" class="mr-2"/>
<a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a>
</div>
<!--对应的Js-->
<script>
function refresh_kaptcha() {
var path = CONTEXT_PATH + "/kaptcha?p=" + Math.random();
$("#kaptchaImage").attr("src", path);
}
</script>
<!--全局Js配置文件-->
var CONTEXT_PATH="/community";
登录功能
验证账号,密码,验证码(成功:生成登录凭证ticket,发放给客户端 失败:跳转回登录页 )
1.创建登录凭证实体类(登录凭证相当于Session的作用)
注意 :为什么要搞一个登录凭证,因为最好不要将User信息存入Model返回给前端,敏感信息尽量不要返回给浏览器,不安全,而是选择ticket凭证,通过ticket可以在服务器端得到User
2.编写Dao层接口(注解方式实现)
@Insert({
"insert into login_ticket(user_id,ticket,status,expired) ",
"values (#{userId},#{ticket},#{status},#{expired})"
})
@Options(useGeneratedKeys = true,keyProperty = "id")
//登录功能需要添加登录凭证ticket
int insertLoginTicket(LoginTicket loginTicket);
@Select({
"select id,user_id,ticket,status,expired ",
"from login_ticket ",
"where ticket=#{ticket}"
})
//检查登录状态
LoginTicket selectByTicket(String ticket);
/**
* 一定要加@Param()不然会报错
* 退出功能需要修改status状态
* @return error:com.mysql.jdbc.MysqlDataTruncation:Data truncation:Truncated incorrect DOUBLE value:...
*/
@Update({
"update login_ticket set status=#{status} where ticket=#{ticket} "
})
int updateStatus(@Param("ticket") String ticket, @Param("status") int status);
3.编写Service层登录业务
/**登录功能**/
public Map<String,Object> login(String username,String password,int expiredSeconds){
HashMap<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;
}
//生成登录凭证(相当于记住我这个功能==session)
LoginTicket ticket = new LoginTicket();
ticket.setUserId(user.getId());
ticket.setTicket(CommunityUtil.generateUUID());
ticket.setStatus(0);
//当前时间的毫秒数+过期时间毫秒数
ticket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
loginTicketMapper.insertLoginTicket(ticket);
map.put("ticket",ticket.getTicket());
return map;
}
4.编写Controller层
/**
* 登录功能
* @param username
* @param password
* @param code 用于校验验证码
* @param rememberme 记住我(登录凭证)
* @param model 用于将数据传递给前端页面
* @param session 用于获取kaptcha验证码
* @param response 用于浏览器接受cookie
* @return
*/
@RequestMapping(value = "/login",method = RequestMethod.POST)
/**注意username,password这些没有封装进model* */
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";
}
/**
* 1.验证用户名和密码(重点)
* 2.传入浏览器cookie=ticket
*/
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";
}
}
5.编写前端Thymeleaf页面核心点
<div class="col-sm-10">
<input type="text"
th:class="|form-control ${usernameMsg!=null?'is-invalid':''}|"
<!--注意param,因为username,password这些没有封装进model-->
th:value="${param.username}"
id="username" name="username" placeholder="请输入您的账号!" required>
<div class="invalid-feedback" th:text="${usernameMsg}">
该账号不存在!
</div>
</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>
退出登录功能
将登录凭证loginTicket中的status置为无效
1.编写Service层
public void logout(String ticket){
loginTicketMapper.updateStatus(ticket,1);//来源于LoginTicket的Dao层
}
2.编写Controller层
/**
* 退出登录功能
* @CookieValue()注解:将浏览器中的Cookie值传给参数
*/
@RequestMapping(value = "/logout",method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket){
userService.logout(ticket);
return "redirect:/login";//重定向
}
显示登录信息
涉及到 :拦截器,多线程
拦截器Demo示例
注意:
1. 拦截器需实现HandlerInterceptor接口而配置类需实现WebMvcConfigurer接口。
2. preHandle方法在Controller之前执行,若返回false,则终止执行后续的请求。
3. postHandle方法在Controller之后、模板页面之前执行。
4. afterCompletion方法在模板之后执行。
5. 通过addInterceptors方法对拦截器进行配置
1.创建拦截器类,实现HandlerInterceptor接口
@Component
public class DemoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle:在Controller之前执行");
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion:在模板之后执行");
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle:在Controller之后,前端模板引擎页面渲染之前执行");
}
}
2.创建拦截器配置类,实现WebMvcConfigurer接口
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private DemoInterceptor demoInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(demoInterceptor)
.excludePathPatterns("/ **/ *.css","/* */*.js","/**/ *.png","/* */*.jpg","/ **/ *.jpeg")
.addPathPatterns("/register","/login");
}
1.首先创建两个工具类降低耦合(Request获取Cookie工具类,获取凭证ticket多线程工具类)
注意:1.ThreadLocal采用线程隔离的方式存放数据,可以避免多线程之间出现数据访问冲突。
2.ThreadLocal提供set方法,能够以当前线程为key存放数据。get方法,能够以当前线程为key获取数据。
3.ThreadLocal提供remove方法,能够以当前线程为key删除数据。
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;
}
}
@Component //放入容器里不用设为静态方法
public class HostHolder {
//key就是线程对象,值为线程的变量副本
private ThreadLocal<User> users = new ThreadLocal<>();
/**以线程为key存入User* */
public void setUser(User user){
users.set(user);
}
/**从ThreadLocal线程中取出User* */
public User getUser(){
return users.get();
}
/**释放线程* */
public void clear(){
users.remove();
}
}
2.编写Service层
/**通过Cookie=ticket获取登录用户* */
public LoginTicket getLoginTicket(String ticket){
return loginTicketMapper.selectByTicket(ticket);
}
3.创建登录凭证拦截器类(等同于Controller类)
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Autowired
private HostHolder hostHolder;
@Override
/**在Controller访问所有路径之前获取凭证* */
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/**从浏览器Cookie中获取凭证* */
String ticket=CookieUtil.getValue(request,"ticket");
if (ticket!=null){
//查询凭证
LoginTicket loginTicket = userService.getLoginTicket(ticket);
//检查凭证是否有效(after:当前时间之后)
if (loginTicket!=null&&loginTicket.getStatus()==0&&loginTicket.getExpired().after(new Date())){
//根据凭证查询用户
User user = userService.findUserById(loginTicket.getUserId());
/**在本次请求中持有用户
* 类似于存入Map,只是考虑到多线程
*/
hostHolder.setUser(user);
}
}
return true;
}
@Override
/**模板之前处理数据* */
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
User user = hostHolder.getUser();
if (user!=null && modelAndView !=null){
modelAndView.addObject("loginUser",user);
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//释放线程资源
hostHolder.clear();
}
}
4.编写拦截器配置类
@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");
}}
5.前端页面核心点修改
th:if="${loginUser!=null}" 存在凭证显示<li>,不存在则不显示
<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser!=null}">
<a class="nav-link position-relative" href="site/letter.html">消息<span class="badge badge-danger">12</span></a>
</li>
拦截未登录页面的路径访问(自定义拦截器注解)
常用的元注解: @Target:注解作用目标(方法or类) @Retention:注解作用时间(运行时or编译时) @Document:注解是否可以生成到文档里 @Inherited:注解继承该类的子类将自动使用@Inherited修饰
注意: 若有2个拦截器,拦截器执行顺序为注册在WebMvcConfig配置类中的顺序
1.自定义拦截方法类注解(annotation包)并加在需要拦截的方法上
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
/**
* 标记未登录时要拦截的路径访问方法
*/
public @interface LoginRequired {
}
/**加在需要拦截的方法**/
@LoginRequired
2.编写拦截器类实现HandlerInterceptor父类
@Autowired
//注入hostHolder工具类获取当前状态登录用户
private HostHolder hostHolder;
@Override
/**在请求路径前执行该方法* */
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断拦截的目标是不是一个方法
if (handler instanceof HandlerMethod){
//如果是一个方法,将handler转化我HandlerMethod类型
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//获取方法上的自定义注解
LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
/**
* 如果没有登录并且有自定义注解(需要登录才能访问的方法注解)
* 通过response来重定向,这里不可以通过return 重定向
*/
if (hostHolder.getUser()==null&&loginRequired!=null){
response.sendRedirect(request.getContextPath() + "/login");
return false;
}
}
return true;
}
3.注册进拦截器配置类WebMvcConfig
@Autowired
private LoginRequiredInterceptor loginRequiredInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginRequiredInterceptor)
.excludePathPatterns("/* */*.css","/**/ *.js","/* */*.png","/ **/ *.jpg","/* */*.jpeg");
}
修改密码
1.编写Dao层
int updatePassword(@Param("id") int id,@Param("password")String password);
<update id="updatePassword">
update user set password=#{password} where id=#{id}
</update>
2.编写Service层
/**修改密码**/
public Map<String,Object> updatePassword(int userId,String oldPassword,String newPassword){
HashMap<String, Object> map = new HashMap<>();
// 空值处理
if (StringUtils.isBlank(oldPassword)) {
map.put("oldPasswordMsg", "原密码不能为空!");
return map;
}
if (StringUtils.isBlank(newPassword)) {
map.put("newPasswordMsg", "新密码不能为空!");
return map;
}
// 验证原始密码
User user = userMapper.selectById(userId);
oldPassword = CommunityUtil.md5(oldPassword + user.getSalt());
if (!user.getPassword().equals(oldPassword)){
map.put("oldPasswordMsg","您输入的原密码错误!");
return map;
}
newPassword = CommunityUtil.md5(newPassword + user.getSalt());
userMapper.updatePassword(userId,newPassword);
return map;
}
3.编写Controller层
/**修改密码 **/
@RequestMapping(value = "/updatePassword",method = RequestMethod.POST)
public String updatePassword(String oldPassword, String newPassword, Model model){
User user = hostHolder.getUser();
Map<String, Object> map = userService.updatePassword(user.getId(), oldPassword, newPassword);
if (map == null || map.isEmpty()){
/**如果更改密码成功,退出登录,并跳到登录页面 **/
return "redirect:/logout";
}else{
model.addAttribute("oldPasswordMsg",map.get("oldPasswordMsg"));
model.addAttribute("newPasswordMsg",map.get("newPasswordMsg"));
return "/site/setting";
}
}
忘记密码
1.编写Service层
// 判断邮箱是否已注册
public boolean isEmailExist(String email) {
User user = userMapper.selectByEmail(email);
return user != null;
}
/**
* 重置忘记密码
*/
public Map<String, Object> resetPassword(String email, String password) {
HashMap<String, Object> map = new HashMap<>();
//空值处理
if (StringUtils.isBlank(email)) {
map.put("emailMsg", "邮箱不能为空!");
return map;
}
if (StringUtils.isBlank(password)) {
map.put("passwordMsg", "密码不能为空!");
return map;
}
//根据邮箱查找用户
User user = userMapper.selectByEmail(email);
if (user == null) {
map.put("emailMsg", "该邮箱尚未注册!");
return map;
}
//重置密码
password = CommunityUtil.md5(password + user.getSalt());
userMapper.updatePassword(user.getId(), password);
// 清理缓存
clearCache(user.getId());
//注意这里!
map.put("user", user);
return map;
}
2.编写Controller层
/**
* 忘记密码页面
*/
@RequestMapping(path = "/forget", method = RequestMethod.GET)
public String getForgetPage() {
return "/site/forget";
}
/**
* 重置密码
*/
@RequestMapping(path = "/forget/password", method = RequestMethod.POST)
public String resetPassword(String email, String verifyCode, String password, Model model, HttpSession session) {
String code = (String) session.getAttribute(email + "_verifyCode");
if (StringUtils.isBlank(verifyCode) || StringUtils.isBlank(code) || !code.equalsIgnoreCase(verifyCode)) {
model.addAttribute("codeMsg", "验证码错误!");
return "/site/forget";
}
Map<String, Object> map = userService.resetPassword(email, password);
if (map.containsKey("user")) {
return "redirect:/login";
} else {
model.addAttribute("emailMsg", map.get("emailMsg"));
model.addAttribute("passwordMsg", map.get("passwordMsg"));
return "/site/forget";
}
}
3.编写前端核心部分
<form method="post" th:action="@{/forget/password}">
<div>
<label class="col-sm-2" for="your-email">邮箱:</label>
<div>
<input id="your-email" name="email" placeholder="请输入您的邮箱!" required
th:class="|form-control ${emailMsg!=null?'is-invalid':''}|" th:value="${param.email}"
type="email">
<div th:text="${emailMsg}">
<input **id="your-email" name="email" placeholder="请输入您的邮箱!" required
th:class="|form-control ${emailMsg!=null?'is-invalid':''}|" th:value="${param.email}"
type="email">**
<div **th:text="${emailMsg}"**>
该邮箱已被注册!
</div>
</div>
</div>
<div >
<label class="col-sm-2" for="verifycode">验证码:</label>
<div>
<input id="verifycode" name="verifyCode" placeholder="请输入验证码!"
th:class="|form-control ${codeMsg!=null?'is-invalid':''}|" th:value="${param.verifyCode}"
type="text">
<input **id="verifycode" name="verifyCode" placeholder="请输入验证码!"
th:class="|form-control ${codeMsg!=null?'is-invalid':''}|" th:value="${param.verifyCode}"
type="text">**
<div th:text="${codeMsg}">
验证码不正确!
</div>
</div>
<div>
<a class="btn" id="verifyCodeBtn">获取验证码</a>
</div>
</div>
<div>
<label class="col-sm-2" for="your-password">新密码:</label>
<div class="col-sm-10">
<input id="your-password" name="password" placeholder="请输入新的密码!" required
th:class="|form-control ${passwordMsg!=null?'is-invalid':''}|"
th:value="${param.password}" type="password">
<input **id="your-password" name="password" placeholder="请输入新的密码!" required
th:class="|form-control ${passwordMsg!=null?'is-invalid':''}|"
th:value="${param.password}" type="password">**
<div class="invalid-feedback" th:text="${passwordMsg}">
密码长度不能小于8位!
</div>
</div>
</div>
<button type="submit" class="btn">重置密码</button>
</form>
优化登录功能(使用Redis)
使用Redis存储验证码
1.编写RedisUtil工具类设置验证码key值
public class RedisKeyUtil {
// 验证码
private static final String PREFIX_KAPTCHA = "kaptcha";
/**登录验证码**/
public static String getKaptchaKey(String owner) {
return PREFIX_KAPTCHA + SPLIT + owner;
}
}
2.优化LoginController验证码相关代码(优化前是存在session中的)
@Autowired
private RedisTemplate redisTemplate;
/**
* 验证码功能 (Redis优化)
* @param response
* /
@RequestMapping(value = "/kaptcha", method = RequestMethod.GET)
public void getKaptcha(HttpServletResponse response) {
//生成验证码
String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);
//优化前:将验证码存入session.....
//优化后:生成验证码的归属传给浏览器Cookie
String kaptchaOwner = CommunityUtil.generateUUID();
Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);
cookie.setMaxAge(60);
cookie.setPath(contextPath);
response.addCookie(cookie);
//优化后:将验证码存入Redis
String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
redisTemplate.opsForValue().set(redisKey, text, 60 , TimeUnit.SECONDS);
//将图片输出给浏览器
response.setContentType("image/png");
try {
ServletOutputStream os = response.getOutputStream();
ImageIO.write(image, "png", os);
} catch (IOException e) {
logger.error("响应验证码失败:" + e.getMessage());
}
}
/**
* 登录功能
* @param redisKey 用于获取kaptcha验证码
* @param @CookieValue用于浏览器接受cookie
* @return
*/
@RequestMapping(value = "/login", method = RequestMethod.POST)
/**注意username,password这些没有封装进model**/
public String login(String username, String password, String code, boolean rememberme,
Model model, HttpServletResponse response,
@CookieValue("kaptchaOwner") String kaptchaOwner) {
/**
* 优化前:首先检验验证码(从session取验证码)
* String kaptcha = (String) session.getAttribute("kaptcha");
*/
// 优化后:从redis中获取kaptcha的key
String kaptcha = null;
// 判断从浏览器传来的Cookie是否为空
if (StringUtils.isNotBlank(kaptchaOwner)) {
String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
// 获取key为验证码的redis数据
kaptcha = (String) redisTemplate.opsForValue().get(redisKey);
}
if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
model.addAttribute("codeMsg", "验证码不正确!");
return "/site/login";
}
/**
* 1.验证用户名和密码(重点)
* 2.传入浏览器cookie=ticket
*/
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";
}
}
使用Redis存存登录凭证
1.编写RedisUtil工具类设置登录凭证key值
// 登录凭证
private static final String PREFIX_TICKET = "ticket";
/**登录凭证**/
public static String getTicketKey(String ticket) {
return PREFIX_TICKET + SPLIT + ticket;
}
2.优化UserService中LoginTicket相关代码(废弃LoginTicket数据库表,使用redis)
@Autowired
private RedisTemplate redisTemplate;
/**
* 登录功能(redis优化)
*/
public Map<String, Object> login(String username, String password, int expiredSeconds) {
HashMap<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;
}
//生成登录凭证(相当于记住我这个功能==session)
LoginTicket ticket = new LoginTicket();
ticket.setUserId(user.getId());
ticket.setTicket(CommunityUtil.generateUUID());
ticket.setStatus(0);
//当前时间的毫秒数+过期时间毫秒数
ticket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds* 1000));
// 优化前:loginTicketMapper.insertLoginTicket(ticket);
// 优化后:loginticket对象放入redis中
String redisKey = RedisKeyUtil.getTicketKey(ticket.getTicket());
// opsForValue将ticket对象序列化为json字符串
redisTemplate.opsForValue().set(redisKey, ticket);
map.put("ticket", ticket.getTicket());
return map;
}
/**
* 通过Cookie=ticket获取登录用户(redis优化)
*/
public LoginTicket getLoginTicket(String ticket) {
//优化前: return loginTicketMapper.selectByTicket(ticket);
String redisKey = RedisKeyUtil.getTicketKey(ticket);
return (LoginTicket) redisTemplate.opsForValue().get(redisKey);
}
使用Redis缓存用户信息
1.编写RedisUtil工具类设置用户缓存key值
// 用户缓存
private static final String PREFIX_USER = "user";
/**用户缓存**/
public static String getUserKey(int userId) {
return PREFIX_USER + SPLIT + userId;
}
2.优化UserService中findUserById和userMapper.updateXXX方法
/**
* 因为经常使用这个方法,所以将它用redis缓存优化
* 若缓存中有访问的用户直接从缓存中取出,否则从数据库查询后加入redis中作为缓存
*/
public User findUserById(int userId) {
// return userMapper.selectById(userId);
// 从redis缓存中取值
User user = getCache(userId);
if (user == null) {
user = initCache(userId);
}
return user;
}
/**
* QQ:260602448 --作者:xumingyu
* 更新头像
*/
public int updateHeader(int userId, String headerUrl) {
/** 同时处理mysql和redis事务的方法,报错回滚* */
int rows = userMapper.updateHeader(userId, headerUrl);
clearCache(userId);
return rows;
}
// 1.优先从缓存中取值
private User getCache(int userId) {
String redisKey = RedisKeyUtil.getUserKey(userId);
return (User) redisTemplate.opsForValue().get(redisKey);
}
// 2.取不到时初始化缓存数据(redis存值)
private User initCache(int userId) {
User user = userMapper.selectById(userId);
String redisKey = RedisKeyUtil.getUserKey(userId);
redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS);
return user;
}
// 3.数据变更时清除缓存(删除redis的key)
private void clearCache(int userId) {
String redisKey = RedisKeyUtil.getUserKey(userId);
redisTemplate.delete(redisKey);
}