Echo/docs/50-开发登录登出功能.md
2021-01-19 13:01:26 +08:00

6.1 KiB
Raw Blame History

开发登录、登出功能


登录:

  • 验证账号、密码、验证码
  • 成功时,生成登录凭证并将其状态设置为有效,发放给客户端(为什么不用 session而是存放在数据库表中因为 session 会给服务器增加压力,且不利于分布式部署)
  • 失败时,跳转回登录页

登出:

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

DAO

@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

<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

/**
 * 用户登录(为用户创建凭证)
 * @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

/**
 * 用户登录
 * @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";
}
@RequestParam(value = "rememberMe", required = false) boolean rememberMe

表示前端没有传来 rememberMe 参数也可,因为用户是可能不勾选记住我的

前端

<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>