# 开发登录、登出功能 --- 登录: - 验证账号、密码、验证码 - 成功时,生成登录凭证并将其状态设置为有效,发放给客户端(为什么不用 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> ```