目录

  • 1、访问登录页面
  • 2、登录
    • 2.1 新建 LoginTicket 的实体类
    • 2.2 新建 LoginTicketMapper 层
    • 2.3 编写测试类测试
    • 2.4 UserService 层
    • 2.5 LoginController 层
    • 2.6 修改 login.html 页面
    • 2.7 测试页面
  • 3、退出
    • 3.1 UserService层:
    • 3.2 LoginController 层
    • 3.3 修改index.html页面:
    • 3.4 测试页面

1、访问登录页面

  1. 点击头部的登录按钮,打开登录页面,之前在注册功能的最后顺便已经实现了,(仿牛客论坛项目)02 – 开发注册功能。

2、登录

  • 验证账号、密码、验证码
  • 成功时,生成登录凭证,发放给客户端
  • 失败时,跳转回登录页

2.1 新建 LoginTicket 的实体类

  1. 对应 login_ticket 表:

public class LoginTicket {private int id;private int userId;private String ticket;//给登录的用户一张通行证private int status;//0-有效; 1-无效;private Date expired;//通行证有效期@Overridepublic String toString() {return "LoginTicket{" +"id=" + id +", userId=" + userId +", ticket='" + ticket + '\'' +", status=" + status +", expired=" + expired +'}';}}

2.2 新建 LoginTicketMapper 层

  1. 这里使用注解方法实现 sql 语句,帮你拼接字符串;
  2. 好处:少写一个文件;缺点:阅读困难,且手写没有提示,例如要实现你插入数据的功能:@insert({"",""})
  3. 注解的方式实现 sql 语句和 mapper 映射类实现 sql 语句,其中的 sql 语句没有变化;
  4. 如果要实现自增主键功能,并将这个值赋给 id :@Options(useGeneratedKeys = true, keyProperty = "id")
  5. 如果要实现动态 sql 拼接,其实还是和 mapper 映射类中差不多的方式,感觉写起来很麻烦。。。
  6. 一般在字符串后面记得加空格,小心字符串拼接出问题。
@Mapperpublic 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}"})//根据唯一的ticket查找登录凭证实体类LoginTicket selectByTicket(String ticket);@Update({"","update login_ticket set status=#{status} where ticket=#{ticket} "," ","and 1=1 ","",""})//当用户退出登录时,或者用户凭证过期时,根据ticket,修改状态int updateStatus(String ticket, int status);}

2.3 编写测试类测试

@Testpublic 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);}@Testpublic 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 层

  1. 登录功能,有多种情况,登陆成功、账号不存在、密码错误等等多种情况,所以使用 map 来接收;
  • 空值处理:

    • 账号不能为空
    • 密码不能为空
  • 验证状态:

    • 调用 DAO 层查询用户,账号不存在
    • 账号状态是否未激活
  • 验证密码:

    • 将用户输入的密码使用 MD5 加密算法进行加密得到加密后的密码,将这个密码和从数据库查出来的密码进行比较,如果不一致,代表输入的密码错误
  • 生成登录凭证:

    • 新建一个 LoginTicket 对象,并给它除了 id 值之外的所有数值设值
    • 调用 DAO 层插入登录凭证数据
  • 将生成的 ticket 值放在 map 集合中返回,也就是最终要传给客户端的一个cookie(因为 ticket 这个信息不敏感,一串随机字符而已);

@Overridepublic 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);//将生成的ticket值放在map集合中返回,也就是最终要传给客户端的一个cookiemap.put("ticket", loginTicket.getTicket());return map;}

2.5 LoginController 层

  1. 实现登录功能
  • 验证码判断(先判断验证码,如果不正确,就不用和数据库进行交互了):

    • session中存放的 kaptcha 值是否为空;
    • 用户输入的 code 值是否为空;
    • 将 session 存放的验证码和 用户传入的 code 进行比较,忽略大小写的情况下是否一致
    • 上述三种情况不正确的情况下,向 model 中存放错误信息,然后返回登陆页面
  • 记录登录凭证时长:

    • 用户可能勾选记住我,或者不勾选记住我,需要判断这个登录凭证时长,在工具类中定义两种不同的存在时长;
/** * 默认状态的登录凭证的超时时间,12h */int DEFAULT_EXPIRED_SECONDS = 3600 * 12;/** * 记住状态的登录凭证超时时间,100天 */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) {//先判断验证码,需要从session中取之前我们自己保存的一份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 " />: DEFAULT_EXPIRED_SECONDS;Map<String, Object> map = userService.login(username, password, expiredSeconds);//账号密码是否正确,正确发送cookie,错误反馈错误信息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 页面

  1. 将账户、密码、验证码、记住我的勾的name属性进行添加,这个值必须和 Controller 中形参定义的一致,否则对应不上的话,服务器形参获取到的值为空;

  2. 错误信息展现:

    • 用户输入的值还显示在页面上,可以从 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" >该账号不存在!</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">密码长度不能小于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">验证码不正确!</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 测试页面

  1. 首次访问登录页面,什么信息都不显示,而且点击刷新验证码可以改变图片;

  1. 输入错误的用户名、错误的密码、错误的验证码查看一下是否会提示错误信息;

  1. 输入正确的查看是否成功(看一下数据库中是否生成了数据凭证)

3、退出

  • 将登录凭证修改为失效状态。
  • 跳转至网站首页。

3.1 UserService层:

  1. 实现退出功能,即修改登录凭证的状态码为 1;
@Overridepublic void logout(String ticket) {loginTicketMapper.updateStatus(ticket,1);}

3.2 LoginController 层

  1. 要根据客户端的 cookie 中获取 ticket 信息
  2. 调用 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页面:

  1. 所有页面服用的头部代码中的退出登录超链接:th:href="@{/logout}"

3.4 测试页面

  1. 查看数据库中的状态码 status 是否变为1