项目1在线交流平台-2.开发交流社区注册登录模块-5.开发登录退出功能

参考牛客网高级项目教程

1.登录模块功能需求

  • 通过输入用户名、密码、验证码,选择登录有效期,后台可以验证用户
    • 验证成功,跳转主页,并可以访问多个页面请求
    • 验证失败,返回登录表单,并提示错误信息
  • 点击退出登录,将用户登录凭证状态改变,并返回登录页面

登录表单实现

  • 在前面的2.2注册功能中的处理激活账户的模块中已经处理好基本页面显示
  • 2.4开发验证码模块,已经处理好登录页面的动态验证码刷新显示功能
在这里插入图片描述

2. 登录功能实现

登录表单显示

  • 在前面的2.2注册功能中的处理激活账户的模块中已经处理好基本页面显示
  • 2.4开发验证码模块,已经处理好登录页面的动态验证码刷新显示功能
在这里插入图片描述

1.1 处理dao层数据准备

1)会话管理策略
  • 由于用户登录后,要能够有权限处理网页其他功能,因此,需要增加请求的可会话性
  • 2.3已经分析可知,
    • 由于用户权限凭证信息比较敏感,故不适合用cookie储存
    • 由于session不适用于分布式部署,故将登录凭证信息储存于数据库中,目前先用mysql储存,后期访问频繁后,再更新迁移至redis非关系型数据库
2)设计储存用户登录凭证的数据表
  • 关联的用户id

  • 每个用户颁发的唯一标识凭证:ticket,字符串

  • 用户登录状态:static:0,凭证有效,1.凭证无效

    • 修改用户登录状态:通过设定static,而不是删除表单数据
  • 凭证有效时间:即自动登录有效时间

    CREATE TABLE `login_ticket` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `user_id` int(11) NOT NULL,
      `ticket` varchar(45) NOT NULL,
      `status` int(11) DEFAULT '0' COMMENT '0-有效; 1-无效;',
      `expired` timestamp NOT NULL,
      PRIMARY KEY (`id`),
      KEY `index_ticket` (`ticket`(20))
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
    
    在这里插入图片描述
3)实体类与接口设计
  • 实体类创建

    @Component
    public class LoginTicket {
        private int id;
        private int userId;
        private String ticket;
        private Data expired;
        ...
    }
    
  • 接口方法声明

    @Mapper
    public interface LoginTicketMapper {
        // 增-将创建的登录凭证入库
        public int insertLoginTicket(LoginTicket loginTicket);
        
        // 查询-根据ticket查询
        // 查询结果是一个类
        public LoginTicket selectByTicket(String ticket);
        
        // 改-修改登录状态-根据ticket查询到一行对象数据
        public int updateLoginTicket(String ticket, int status);
    }
    
4)编写sql
  • 可以使用xml配置文件编写
  • 对于简单的sql语句,也可以直接使用注解进行编写,本例采用后者
@Insert({" ", " "})
  • 注解中,直接用" ", " "会自动拼接字符串,完成sql语句的编写
@Options(useGeneratedKeys = true, keyProperty = “id”)
  • 在注解中设置,

    • useGeneratedKeys :是否自动生成主键
    • keyProperty:生成的主键数据注入到entity类的哪个属性上
    	// 增-将创建的登录凭证入库
    	@Insert({
                "insert into login_ticket(user_id, ticket, status, expired) ",
                "values(#{userId}, #{ticket}, #{status}, #{expired})"
        })
        @Options(useGeneratedKeys = true, keyProperty = "id")
        int insertLoginTicket(LoginTicket loginTicket);
    
"<if test = " "> ", “”,
  • 也可以编写动态sal,只不过比较麻烦:里面的""需要专业

  • 外面也必须有""包裹

    	@Update({
                "<script>",
                "update login_ticket set status = #{status} where ticket = #{ticket} ",
                "<if test = \"ticket != null\"> ",
                "and 1 = 1 ",
                "</if>",
                "</script>"
        })
        int updateLoginTicket(String ticket, int status);
    
5)sql测试
  • 先进行操作数据库的测试,检测sql语句或方法名类型书写的正确性
System.currentTimeMillis()
  • 单位是毫秒,故要转为秒,需要*1000
// 登录凭证数据的测试
    // 添加数据
    @Test
    public void testInsertLoginTicket() {
        LoginTicket loginTicket = new LoginTicket();
        loginTicket.setUserId(199);
        loginTicket.setTicket("test");
        loginTicket.setStatus(0);
        loginTicket.setExpired(new Date(System.currentTimeMillis() + 1000 * 60 * 10)); // 10分钟后过期
        loginTicketMapper.insertLoginTicket(loginTicket);
    }

    // 查询和修改数据
    @Test
    public void testSelectLoginTicket() {
        LoginTicket ticket = loginTicketMapper.selectByTicket("test");
        System.out.println(ticket);

        loginTicketMapper.updateLoginTicket("test", 1);
        ticket = loginTicketMapper.selectByTicket("test");
        System.out.println(ticket.getStatus());
    }
LoginTicket{id=1, userId=199, ticket='test', status=0, expired=Fri Mar 04 18:49:56 CST 2022}
1

1.2 Servive层处理登录业务

1)返回类型、传参
  • 由于封装信息比较多,用map封装

  • 传参出了用户信息,还有其他相关信息,直接传参,就不像2.2注册开发只传进来一个user对象

    /**
     * 登录业务处理
     * @param username
     * @param password
     * @param expiredSeconds 过期时间,以秒为单位
     * @return map
     */
    public Map<String, Object> login(String username, String password, int expiredSeconds) {
        Map<String, Object> map = new HashMap<>();
        
        return map;
    }
    
2)验证处理逻辑
  • 1.先判断空值

  • 2.验证合法性

    • 账号,从库中查询比对
      • 是否有
      • 有的话,状态对不对,是否激活
    • 密码,将明文与salt进行MD5加密,再与密文比对
    • 验证码,没有保存在数据库,而是session中,因此在视图层处理
  • 3.验证成功后,登录跳转到主页前,给用户生成登录凭证

    • 服务端:将登录凭证保存入库,留做比对
    • 客户端:也要保存登录凭证进cookie,只需要保存ticket字符串即可
      • 因此,需要将ticket封装进map传给视图层处理
    /**
     * 登录业务处理
     * @param username
     * @param password
     * @param expiredSeconds 过期时间
     * @return map
     */
    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;
        }
        // 验证密码
        String salt = user.getSalt();
        if(!user.getPassword().equals(CommunityUtil.md5(password + salt))) {
            map.put("passwordMsg", "密码不正确!");
            retrun map;
        }
    
        // 登录前创建登录凭证,并在服务端和客户端储存
        LoginTicket loginTicket = new LoginTicket();
        loginTicket.setUserId(user.getId());
        loginTicket.setTicket(CommunityUtil.generateUUID());
        loginTicket.setStatus(0); // 0有效,1无效,登出时设置为1
        // 注意日期转换的格式单位:毫秒
        loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000L));
        loginTicketMapper.insertLoginTicket(loginTicket);
        // 将登录凭证ticket字符串传给视图层
        map.put("ticket", loginTicket.getTicket());
        return map;
    }
    

1.3 Controller层处理请求

1)请求方式,接收参数
  • 由于是表单提交数据,且数据信息敏感,因此才有POST请求

  • 接收参数,即表单上填入的信息

    • 用户基本信息:username, password
    • 验证码信息:
      • 用户填入的验证码:code
      • 服务端存在session中的验证码:HttpSession
    • 登录凭证有效期选择:remenberMe,布尔变量
    • 登录成功后,要将登录凭证通过cookie响应给浏览器:HttpServletResponse
    • model
    // 登录请求表单处理
    @RequestMapping(path = "/login", method = RequestMethod.POST)
    public String login(Model model, 
                        String username, 
                        String password, 
                        String code,
                        boolean rememberMe, 
                        HttpSession session, 
                        HttpServletResponse response) {
        
        return "/site/login";
    }
    
2)验证处理封装信息
1.先验证表单页面的验证码信息
  • 从session中获取验证码字符串

  • 与用户提供的验证码比对

    • 注意,也要进行判空处理
    // 先验证表单页面的验证码信息
    // 从session中获取验证码字符串
    String kaptcha = (String)session.getAttribute("kaptcha");
    // 与用户输入验证码比对,注意要判空
    if(StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) 
    	|| !kaptcha.equals(code)) {
        model.addAttribute("codeMsg", "验证码不正确!");
        return "/site/login";
    }
    
2.验证账号密码
与service层交互
  • 将请求参数传给service处理

  • 先将用户选择的登录凭证生效时间读取出来,定义在常量接口中

    /** 默认状态的登录凭证的超时时间, 单位为秒*/
    int DEFAULT_EXPIRED_SECONDS = 3600 * 12; // 12小时
    
    /** 记住状态的登录凭证超时时间, 单位为秒*/
    int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100; // 100天
    
  • 接收业务层处理后传过来的map数据信息

    // 生成登录凭证需要,先读取用户选择的登录凭证生效时间,从常量接口中获取
    int expiredSeconds = rememberMe ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
    Map<String, Object> map = userService.login(username, password, expiredSeconds);
    
验证成功后的处理
  • 验证成功后,记得向浏览器颁发登录凭证ticket

  • 重定向到主页

    // 验证成功后,记得向浏览器颁发登录凭证ticket
    if(map.containsKey("ticket")) { // 验证成功,才能发送ticket,不是map为null
        // 创建cookie储存ticket
        Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
        // 设置cookie范围,时间
        cookie.setPath(contextPath);
        // cookie是以秒为单位计时间的,但java中的Date类是以毫秒为单位计时间的
        cookie.setMaxAge(expiredSeconds); // 以凭证的有效时间来定cookie存活时间
        // 向浏览器响应cookie
        response.addCookie(cookie);
        // 重定向到主页
        return "redirect:/index";
    }
    
验证失败后的处理
  • 证失败,将错误信息装进model,返回给模板页面,

  • 并重新返回到模板页面

    else { // 验证失败,将错误信息装进model,返回给模板页面,并返回到模板页面
            model.addAttribute("usernameMsg", map.get("usernameMsg"));
            model.addAttribute("passwordMsg", map.get("passwordMsg"));
            return "/site/login";
        }
    }
    

1.4 View视图模板页面处理

表头请求方式、路径
<form class="mt-5" method="post" th:action="@{/login}">    
添加name属性
id="username" name="username" placeholder="请输入您的账号!" required>
默认从request中取之前传入的数据
param.username
th:checked
  • th:checked="${param.rememberMe}",动态判断单选框内容,可以是true,false
th:value="${param.username}"
动态显示错误信息
  • ’is-invalid’,要带单引号
<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>

测试结果

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3. 退出登录功能实现

1.1 处理service层业务逻辑

  • 通过传过来的ticket来更改指定凭证的状态

    /** 登出业务处理*/
    public void logout(String ticket) {
        loginTicketMapper.updateLoginTicket(ticket, 1);
    }
    

1.2 Controller处理请求

  • 获取请求头中携带的指定key的kookie数据,传给业务层处理

  • 跳转回登录页面,重定向,默认get请求

    // 退出登录请求
    @RequestMapping(path = "/logout", method = RequestMethod.GET)
    public String logout(@CookieValue("ticket") String ticket) {
        userService.logout(ticket);
        return "redirect:/login";
    }
    

1.3 View视图模板页面处理

  • 修改主页头部,登出链接改成thymeleaf动态链接

    <a class="dropdown-item text-center" th:href="@{/logout}">退出登录</a>
    

测试结果

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值