1、访问登录页面
- 点击头部的登录按钮,打开登录页面,之前在注册功能的最后顺便已经实现了,(仿牛客论坛项目)02 - 开发注册功能。
2、登录
- 验证账号、密码、验证码
- 成功时,生成登录凭证,发放给客户端
- 失败时,跳转回登录页
2.1 新建 LoginTicket 的实体类
- 对应 login_ticket 表:
public class LoginTicket {
private int id;
private int userId;
private String ticket;
private int status;
private Date expired;
@Override
public String toString() {
return "LoginTicket{" +
"id=" + id +
", userId=" + userId +
", ticket='" + ticket + '\'' +
", status=" + status +
", expired=" + expired +
'}';
}
}
2.2 新建 LoginTicketMapper 层
- 这里使用注解方法实现 sql 语句,帮你拼接字符串;
- 好处:少写一个文件;缺点:阅读困难,且手写没有提示,例如要实现你插入数据的功能:
@insert({"",""}) ; - 注解的方式实现 sql 语句和 mapper 映射类实现 sql 语句,其中的 sql 语句没有变化;
- 如果要实现自增主键功能,并将这个值赋给 id :
@Options(useGeneratedKeys = true, keyProperty = "id") ; - 如果要实现动态 sql 拼接,其实还是和 mapper 映射类中差不多的方式,感觉写起来很麻烦。。。
<if test=\"ticket!=null\"> ; - 一般在字符串后面记得加空格,小心字符串拼接出问题。
@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);
}
2.3 编写测试类测试
@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);
}
2.4 UserService 层
- 登录功能,有多种情况,登陆成功、账号不存在、密码错误等等多种情况,所以使用 map 来接收;
@Override
public Map<String, Object> login(String username, String password, long 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 (!password.equals(user.getPassword())){
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;
}
2.5 LoginController 层
- 实现登录功能
int DEFAULT_EXPIRED_SECONDS = 3600 * 12;
int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;
- 检查账号密码:
- 调用 UserService 层来进行登录账户密码验证返回一个map,判断map中的所有数据
- 如果 map 中包含 ticket 表示没有问题,生成cookie,设置有效路径为整个项目,设置有效时长,并传给客户端,返回到首页页面
- 如果出问题,有可能是用户名错误,密码错误,放到 model 中返回到登录界面提示用户。
@RequestMapping(value = "/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",(String) map.get("ticket"));
cookie.setMaxAge(expiredSeconds);
cookie.setPath(contextPath);
response.addCookie(cookie);
return "redirect:/index";
}else {
model.addAttribute("usernameMsg", map.get("usernameMsg"));
model.addAttribute("passwordMsg", map.get("passwordMsg"));
return "/site/login";
}
}
2.6 修改 login.html 页面
-
将账户、密码、验证码、记住我的勾的name属性进行添加,这个值必须和 Controller 中形参定义的一致,否则对应不上的话,服务器形参获取到的值为空; -
错误信息展现:
- 用户输入的值还显示在页面上,可以从 request 中取值
th:value="${param.username}" ; - 记住我那个勾还和用户原来的选择保持一致:
th:checked="${param.rememberme}" ,这里是boolean值,返回 true 或者 false 也可以用来判断是否勾选; - 显示错误信息:
th:class="|form-control ${usernameMsg!=null?'is-invalid':''}|"
注意:
- 因为我们在 controller 中将 form 表单请求中传过来的值直接定义在了形参上,以常见数据类型 String 等等不会封装在 model 中;
- 也就是说 model 中可以自动封装形参中的实体类对象,而不能封装常用数据类型,所以我们要从请求中获取;
<form class="mt-5" th:action="@{/login}" method="post">
<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':''}|"
name="username" th:value="${param.username}"
id="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':''}|"
name="password" th:value="${param.password}"
id="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':''}|"
name="code"
id="verifycode" placeholder="请输入验证码!">
<div class="invalid-feedback" th:text="${codeMsg}">
验证码不正确!
</div>
</div>
<div class="col-sm-4">
<img th:src="@{/kaptcha}" id="kaptcha" style="width:100px;height:40px;" class="mr-2"/>
<a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a>
</div>
</div>
<div class="form-group row mt-4">
<div class="col-sm-2"></div>
<div class="col-sm-10">
<input type="checkbox" name="rememberme"
id="remember-me" th:checked="${param.remenberme}">
<label class="form-check-label" for="remember-me">记住我</label>
<a href="forget.html" class="text-danger float-right">忘记密码?</a>
</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>
2.7 测试页面
- 首次访问登录页面,什么信息都不显示,而且点击刷新验证码可以改变图片;
- 输入错误的用户名、错误的密码、错误的验证码查看一下是否会提示错误信息;
- 输入正确的查看是否成功(看一下数据库中是否生成了数据凭证)
3、退出
3.1 UserService层:
- 实现退出功能,即修改登录凭证的状态码为 1;
@Override
public void logout(String ticket) {
loginTicketMapper.updateStatus(ticket,1);
}
3.2 LoginController 层
- 要根据客户端的 cookie 中获取 ticket 信息
- 调用 UserService 层的 logout 方法修改 ticket 对应的状态码信息,并且返回到登陆页面
@RequestMapping(value = "/logout",method = RequestMethod.GET)
public String logout(@CookieValue("ticket")String ticket){
userService.logout(ticket);
return "redirect:/login";
}
3.3 修改index.html页面:
- 所有页面服用的头部代码中的退出登录超链接:
th:href="@{/logout}"
3.4 测试页面
- 查看数据库中的状态码 status 是否变为1
|