问答平台(2),登录和退出

登录

1
2
3
- 验证账号、密码、验证码。
- 成功时,生成登录凭证,发放给客户端。
- 失败时,跳转回登录页面。

LoginTicket

1
2
3
- login_ticket表保存用户的状态,核心字段为ticket。
- 登陆成功时,服务器将ticket字段发送给客户端。
- 客户端访问时,将ticket字段发送给服务器,然后服务器根据这个字段查询用户信息(id、用户状态、过期时间等)。

退出

1
2
- 将登录凭证修改为失效状态;
- 跳转到网站首页。

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// LoginTicket.java
public class LoginTicket {

private int id;
private int userId;
private String ticket;
private int status;
private Date expired;

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 getTicket() {
return ticket;
}

public void setTicket(String ticket) {
this.ticket = ticket;
}

public int getStatus() {
return status;
}

public void setStatus(int status) {
this.status = status;
}

public Date getExpired() {
return expired;
}

public void setExpired(Date expired) {
this.expired = expired;
}

@Override
public String toString() {
return "LoginTicket{" +
"id=" + id +
", userId=" + userId +
", ticket='" + ticket + '\'' +
", status=" + status +
", expired=" + expired +
'}';
}
}

工具类

1
2
3
4
5
6
7
8
9
10
// CommunityConstant.java
/**
* 默认状态下,登录凭证的超时时间(单位:s)
*/
int DEFAULT_EXPIRED_SECONDS = 3600 * 12;

/**
* 记住状态下,登录凭证的超时时间
*/
int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;

数据访问层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// LoginTicketMapper.java
@Mapper
@Repository
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);
}

业务层

1
2
3
4
5
流程如下:
1)空值判断
2)合法性验证:用户名不存在、密码错误、账号未激活
3)生成LoginTicket对象,调用Dao存入数据库
4)将ticket存入Map,返回给客户端保存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// UserService.java
public Map<String, Object> login(String username, String password, int expiredSecond) {
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() + expiredSecond * 1000));
loginTicketMapper.insertLoginTicket(loginTicket);

map.put("ticket", loginTicket.getTicket());
return map;
}

// 将登录凭证修改为失效状态:调用updateStatus方法,更新Ticket对象的状态码为1。
public void logout(String ticket) {
loginTicketMapper.updateStatus(ticket, 1);
}

表现层

1
2
3
4
5
流程如下:
1)验证码校验:session里面存了发送给客户端的验证码,直接取,然后与客户端传来的验证码做判断。
2)调用业务层,获取执行结果。
3)如果Map中返回的字段有ticket,则将其初始化cookie,设置cookie属性,将cookie加入response对象并返回给客户端。
4)如果失败则在model里面加入错误信息,跳转到登陆页面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// LoginController.java
@RequestMapping(path = "/login", method = RequestMethod.POST)
public String login(String username, String password, String code, boolean rememberme,
Model model, HttpSession session, HttpServletResponse response,
@CookieValue("kaptchaOwner") String kaptchaOwner) {
// 检查验证码
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";
}

页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<!-- login.html -->
<!-- 内容 -->
<h3 class="text-center text-info border-bottom pb-3">&nbsp;&nbsp;</h3>
<form class="mt-5" method="post" th:action="@{/login}">

<div class="form-group row">
<label for="username" class="col-sm-2 col-form-label text-right">账号:</label>
<div class="col-sm-10">
<input type="text" th:class="|form-control ${usernameMsg!=null?'is-invalid':''}|"
th:value="${param.username}"
id="username" name="username" placeholder="请输入您的账号!" required>
<div class="invalid-feedback" th:text="${usernameMsg}">
该账号不存在!
</div>
</div>
<div class="form-group row mt-4">
<label for="password" class="col-sm-2 col-form-label text-right">密码:</label>
<div class="col-sm-10">
<input type="password" th:class="|form-control ${passwordMsg!=null?'is-invalid':''}|"
th:value="${param.password}"
id="password" name="password" placeholder="请输入您的密码!" required>
<div class="invalid-feedback" th:text="${passwordMsg}">
密码长度不能小于8位!
</div>
</div>
</div>
<div class="form-group row mt-4">
<label for="verifycode" class="col-sm-2 col-form-label text-right">验证码:</label>
<div class="col-sm-6">
<input type="text" th:class="|form-control ${codeMsg!=null?'is-invalid':''}|"
id="verifycode" name="code" placeholder="请输入验证码!">
<div class="invalid-feedback" th:text="${codeMsg}">
验证码不正确!
</div>
</div>
</div>
<div class="form-group row mt-4">
<div class="col-sm-2"></div>
<div class="col-sm-10">
<input type="checkbox" id="remember-me" name="rememberme"
th:checked="${param.rememberme}">
<label class="form-check-label" for="remember-me">记住我</label>
<a href="forget.html" class="text-danger float-right">忘记密码?</a>
</div>
</div>

踩坑

1
2
3
4
5
<!-- index.html -->
原因:网络被墙
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
解决方法:保存在项目的js目录下
<script src="/js/jquery-3.3.1.min.js" crossorigin="anonymous"></script>

问答平台(2),登录和退出
https://lcf163.github.io/2020/04/28/问答平台(2),登录和退出/
作者
乘风的小站
发布于
2020年4月28日
许可协议