本部分代码来源于牛客网官方的仿牛客论坛项目,仅供个人学习和探讨使用。
DROP TABLE IF EXISTS `login_ticket`;
SET character_set_client = utf8mb4 ;
CREATE TABLE `login_ticket` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`ticket` varchar(45) NOT NULL,
`status` int(11) DEFAULT '0' COMMENT '0-有效; 1-无效;',
`expired` timestamp NOT NULL,
PRIMARY KEY (`id`),
KEY `index_ticket` (`ticket`(20))
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
相关参数说明:
id:标识唯一的登录凭证,自增
user_id:外键,通过这个值找到对应的用户
ticket:登录凭证,生成随机的字符串,唯一
status:登录状态,0-有效,1-无效
expired:登录到期时间
public class LoginTicket {
private int id;
private int userId;
private String ticket; // 登录凭证
private int status;
private Date expired;
// 省略 getter setter 和 toString 方法
}
编写sql语句。
复制代码记得更改包名。
package com.zcq.community.dao;
import com.zcq.community.entity.LoginTicket;
import org.apache.ibatis.annotations.*;
@Mapper
public interface LoginTicketMapper {
// 使用sql语句注解会比较直观,但是复杂的sql语句推荐使用xml
@Insert({
"insert into login_ticket(user_id, ticket, status, expired) ",
"values(#{userId}, #{ticket}, #{status}, #{expired}) "
})
@Options(useGeneratedKeys = true, keyProperty = "id") // 是否启用主键,将主键注入到哪个属性中
int insertLoginTicket(LoginTicket loginTicket);
/**
* 利用核心数据ticket在服务端找到用户信息进行查询
* @param ticket
* @return
*/
@Select({
"select id, user_id, ticket, status, expired ",
"from login_ticket where ticket = #{ticket} "
})
LoginTicket selectByTicket(String ticket);
/**
* 修改登录状态, 退出登录时用
* @param ticket
* @param status
* @return
*/
@Update({
"update login_ticket set status = #{status} where ticket = #{ticket} ",
})
int updateStatus(String ticket, int status);
// 通过注解方式也可以嵌套动态sql语句,比如if标签
/*""*/
}
由于注解sql语句的方式IDEA无法进行识别,如果出现语法错误会导致后续无法进行。
package com.zcq.community.dao;
import com.zcq.community.CommunityApplication;
import com.zcq.community.entity.LoginTicket;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Date;
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class LoginMapperTests {
@Autowired
private LoginTicketMapper loginTicketMapper;
@Test
public void testInsertLoginTicket() {
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(101);
loginTicket.setTicket("abc");
loginTicket.setStatus(0);
loginTicket.setExpired(new Date(System.currentTimeMillis() + 1000*60*10));
loginTicketMapper.insertLoginTicket(loginTicket);
}
@Test
public void testSelectLoginTicket() {
LoginTicket loginTicket = loginTicketMapper.selectByTicket("abc");
System.out.println(loginTicket);
loginTicketMapper.updateStatus("abc", 1);
loginTicket = loginTicketMapper.selectByTicket("abc");
System.out.println(loginTicket);
}
}
package com.zcq.community.util;
public interface CommunityConstant {
// 默认状态的登录凭证超时时间
int DEFAULT_EXPIRED_SECONDS = 3600 * 12;
// 记住状态下的登录凭证超时时间
int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;
}
放入 userService 中是因为登录操作与用户相关。
package com.zcq.community.service;
import com.zcq.community.dao.LoginTicketMapper;
import com.zcq.community.dao.UserMapper;
import com.zcq.community.entity.LoginTicket;
import com.zcq.community.entity.User;
import com.zcq.community.util.CommunityConstant;
import com.zcq.community.util.CommunityUtil;
import com.zcq.community.util.MailClient;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
@Service
public class UserService implements CommunityConstant {
@Autowired
private UserMapper userMapper;
@Autowired
private LoginTicketMapper loginTicketMapper;
// 此处省略了userService的其他代码
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;
}
}
package com.zcq.community.controller;
import com.google.code.kaptcha.Producer;
import com.zcq.community.service.UserService;
import com.zcq.community.util.CommunityConstant;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
@Controller
public class LoginController implements CommunityConstant {
@Autowired
private UserService userService;
@Value("${server.servlet.context-path}")
private String contextPath;
// 说明:特殊对象, 比如 user, springmvc 会将值装到 model 层里, 龚前端页面获取
// 其余参数可以从 request 中取值, 或者存到 model 层里, 从 model 层里取值
// 这里选用 post 请求的原因是我们需要传入一个表单
@RequestMapping(path = "/login", method = RequestMethod.POST)
public String login(String username, String password, String code, boolean rememberme,
Model model, HttpSession session, HttpServletResponse response) {
// 1 判断验证码正确与否
String kaptcha = (String) session.getAttribute("kaptcha");
// 后台的验证码为空 或 用户传入的验证码为空 或 忽略大小写后的值不等
if(StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
model.addAttribute("codeMsg", "验证码不正确");
return "/site/login";
}
// 2 检查账号,密码
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";
}
}
}
说明:html 代码中的设计部分已删除,此处指展示和后端业务交互的部分。
DOCTYPE html>
<html lang="en" xmlns="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>title>
head>
<body>
<form method="post" th:action="@{/login}">
<div>
<label>账号:label>
<div>
<input type="text" th:class="|form-control ${usernameMsg!=null?'is-invalid':''}|"
th:value="${param.username}"
id="username" name="username" placeholder="请输入您的账号" required>
<div class="invalid-feedback" th:text="${usernameMsg}">
该账号不存在
div>
div>
div>
<div>
<label for="password">密码:label>
<div>
<input type="password" th:class="|form-control ${passwordMsg!=null?'is-invalid':''}|"
th:value="${param.password}"
id="password" name="password" placeholder="请输入您的密码" required>
<div th:text="${passwordMsg}">
密码长度不能小于8位!
div>
div>
div>
<div>
<label for="verifycode">验证码:label>
<div>
<input type="text" th:class="|form-control ${codeMsg!=null?'is-invalid':''}|"
id="verifycode" name="code" placeholder="请输入验证码 不区分大小写">
<div th:text="${codeMsg}">
验证码不正确!
div>
div>
<div>
<img th:src="@{/kaptcha}">
<a href="javascript:refresh_kaptcha();">刷新验证码a>
div>
div>
<div>
<div>
<input type="checkbox" id="remember-me" name="rememberme"
th:checked="${param.rememberme}">
<label for="remember-me">记住我label>
<a href="forget.html">忘记密码?a>
div>
div>
<div class="form-group row mt-4">
<button type="submit">立即登录button>
div>
form>
body>
html>
代码编写完毕,可以进行测试了。