226 lines
6.1 KiB
Markdown
226 lines
6.1 KiB
Markdown
# 开发登录、登出功能
|
||
|
||
---
|
||
|
||
登录:
|
||
|
||
- 验证账号、密码、验证码
|
||
- 成功时,生成登录凭证并将其状态设置为有效,发放给客户端(为什么不用 session,而是存放在数据库表中?因为 session 会给服务器增加压力,且不利于分布式部署)
|
||
- 失败时,跳转回登录页
|
||
|
||
登出:
|
||
|
||
- 将登录凭证修改为失效状态
|
||
- 跳转至网站首页
|
||
|
||
|
||
|
||
## DAO
|
||
|
||
```java
|
||
@Mapper
|
||
public interface LoginTicketMapper {
|
||
|
||
/**
|
||
* 插入一条 LoginTicket
|
||
* @param loginTicket
|
||
* @return
|
||
*/
|
||
int insertLoginTicket(LoginTicket loginTicket);
|
||
|
||
/**
|
||
* 根据 ticket 查询 LoginTicket
|
||
* @param ticket
|
||
* @return
|
||
*/
|
||
LoginTicket selectByTicket(String ticket);
|
||
|
||
/**
|
||
* 更新凭证状态,0:有效,1:无效
|
||
* @param ticket
|
||
* @param status
|
||
* @return
|
||
*/
|
||
int updateStatus(String ticket, int status);
|
||
|
||
}
|
||
```
|
||
|
||
对应的 `mapper.xml`
|
||
|
||
```xml
|
||
<sql id = "insertFields">
|
||
user_id, ticket, status, expired
|
||
</sql>
|
||
|
||
<sql id = "selectFields">
|
||
id, user_id, ticket, status, expired
|
||
</sql>
|
||
|
||
<!--根据 ticket 查询凭证信息-->
|
||
<select id="selectByTicket" resultType="LoginTicket">
|
||
select <include refid="selectFields"></include>
|
||
from login_ticket
|
||
where ticket = #{ticket}
|
||
</select>
|
||
|
||
<!--插入凭证信息-->
|
||
<insert id="insertLoginTicket" parameterType="LoginTicket" keyProperty="id">
|
||
insert into login_ticket (<include refid="insertFields"></include>)
|
||
values(#{userId}, #{ticket}, #{status}, #{expired})
|
||
</insert>
|
||
|
||
<!--根据 ticket 修改凭证状态-->
|
||
<update id="updateStatus">
|
||
update login_ticket set status = #{status} where ticket = #{ticket}
|
||
</update>
|
||
```
|
||
|
||
## Service
|
||
|
||
```java
|
||
/**
|
||
* 用户登录(为用户创建凭证)
|
||
* @param username
|
||
* @param password
|
||
* @param expiredSeconds 多少秒后凭证过期
|
||
* @return Map<String, Object> 返回错误提示消息以及 ticket(凭证)
|
||
*/
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* 用户退出(将凭证状态设为无效)
|
||
* @param ticket
|
||
*/
|
||
public void logout(String ticket) {
|
||
loginTicketMapper.updateStatus(ticket, 1);
|
||
}
|
||
```
|
||
|
||
|
||
|
||
## Controller
|
||
|
||
```java
|
||
/**
|
||
* 用户登录
|
||
* @param username 用户名
|
||
* @param password 密码
|
||
* @param code 验证码
|
||
* @param rememberMe 是否记住我(点击记住我后,凭证的有效期延长)
|
||
* @param model
|
||
* @param session 从 session 中取出验证码
|
||
* @param response
|
||
* @return
|
||
*/
|
||
@PostMapping("/login")
|
||
public String login(@RequestParam("username") String username,
|
||
@RequestParam("password") String password,
|
||
@RequestParam("code") String code,
|
||
@RequestParam(value = "rememberMe", required = false) boolean rememberMe,
|
||
Model model, HttpSession session, HttpServletResponse response) {
|
||
// 检查验证码
|
||
String kaptcha = (String) session.getAttribute("kaptcha");
|
||
if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
|
||
model.addAttribute("codeMsg", "验证码错误");
|
||
return "/site/login";
|
||
}
|
||
|
||
// 凭证过期时间(是否记住我)
|
||
int expiredSeconds = rememberMe ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
|
||
// 验证用户名和密码
|
||
Map<String, Object> map = userService.login(username, password, expiredSeconds);
|
||
if (map.containsKey("ticket")) {
|
||
// 账号和密码均正确,则服务端会生成 ticket,浏览器通过 cookie 存储 ticket
|
||
Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
|
||
cookie.setPath(contextPath); // cookie 有效范围
|
||
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";
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* 用户登出
|
||
* @param ticket 设置凭证状态为无效
|
||
* @return
|
||
*/
|
||
@GetMapping("/logout")
|
||
public String logout(@CookieValue("ticket") String ticket) {
|
||
userService.logout(ticket);
|
||
return "redirect:/login";
|
||
}
|
||
```
|
||
|
||
|
||
|
||
```java
|
||
@RequestParam(value = "rememberMe", required = false) boolean rememberMe
|
||
```
|
||
|
||
表示前端没有传来 rememberMe 参数也可,因为用户是可能不勾选记住我的
|
||
|
||
## 前端
|
||
|
||
```html
|
||
<form class="mt-5" method="post" th:action="@{/login}">
|
||
|
||
<input type="text" th:class="|form-control ${usernameMsg != null ? 'is-invalid' : ''}|"
|
||
th:value="${param.username}"
|
||
name="username" required>
|
||
<div class="invalid-feedback" th:text="${usernameMsg}"></div>
|
||
```
|
||
|