2.5仿牛客社区项目———登录退出(创建登录凭证ticket,放入cookie)

重点:登录时,浏览器提交表单后,先检查session中的验证码是否正确,然后看账号、密码是否为空,是否正确,最后生成凭证ticket存在login_ticket这个表中。这个ticket是generateUUID()生成的,这个表还存了userId,status(退出时变为1),失效时间(如果点击记住我,失效时间延长)。创建cookie,ticket放入,再把cookie放到response对象里,发给客户端。
至于为什么验证码要用session和ticket为什么要用cookie还是不太理解,后续填坑。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
login_ticket表:
在这里插入图片描述
id是主键
ticket凭证(UUID生成)
status登录状态(0有效 1无效)
expired过期时间
ticket由服务器发给浏览器,作为cookie保存在浏览器,其他信息在服务端保存。下次服务端收到了cookie识别出是来自哪个浏览器,并可取出其他信息。

写程序的顺序:数据访问层->业务->表现层

一、登录功能

1、Dao数据访问层(data access objects)

实体类、sql表、Mapper接口、Mapper映射器相对应~~

(1) 实体类entity:LoginTicket.class

5个属性id、userId、ticket、status、expired,getset方法,重写toString

public class LoginTicket {

    private int id;
    private int userId;
    private String ticket;//凭证字符串
    private int status;//状态0有效1无效
    private Date expired;//到期日期

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public String getTicket() {
        return ticket;
    }

    public void setTicket(String ticket) {
        this.ticket = ticket;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public Date getExpired() {
        return expired;
    }

    public void setExpired(Date expired) {
        this.expired = expired;
    }

    @Override
    public String toString() {
        return "LoginTicket{" +
                "id=" + id +
                ", userId=" + userId +
                ", ticket='" + ticket + '\'' +
                ", status=" + status +
                ", expired=" + expired +
                '}';
    }
}

(2) 数据访问层接口LoginTicketMapper.java
三个方法:插入、查询、退出
这里就没在xml上写了,直接用注解实现sql语句。

@Mapper
public interface LoginTicketMapper {

    //实现sql语句:可以在xml里写,也可以通过注解,字符串拼接成,书写方便,每行字符串加一个空格,主键自动生成
    @Insert({
            "insert into login_ticket(user_id,ticket,status,expired) ",
            "values(#{userId},#{ticket},#{status},#{expired})"
    })
    @Options(useGeneratedKeys = true, keyProperty = "id")//希望主键id是自动生成的,生成的值注入给对象,指定属性id
    int insertLoginTicket(LoginTicket loginTicket);//插入输出,影响行数

    @Select({
            "select id,user_id,ticket,status,expired ",
            "from login_ticket where ticket=#{ticket}"
    })
    LoginTicket selectByTicket(String ticket);//查询,依据ticket,ticket是凭证,发送给cookie浏览器存,其他的在服务器存,下次cookie给服务器,服务器查到其他的数据,ticket唯一标识

    //退出,状态改变
    @Update({
            "<script>",
            "update login_ticket set status=#{status} where ticket=#{ticket} ",
            "<if test=\"ticket!=null\"> ",
            "and 1=1 ",
            "</if>",
            "</script>"
    })//动态sql,if怎么用? <script>脚本,是一个演示
    int updateStatus(String ticket, int status);

}

测试sql是否写正确了,在MapperTests写测试,先@Autowired注入loginTicketMapper:

    @Test
    public void testInsertLogin() {
        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、业务层UserService.java

注入mapper
输入:用户名、密码、多久后失效
输出:Map<String, Object>,用Map装多个情况,登录失败的原因(账号没输入、没存在、没激活),登录成功的凭证
处理逻辑:

  • 空值处理,username是否为空,返回“usernameMsg”消息,同理密码。
  • 验证账号,username看库里是否有,如果有库里账号是否一致,是否被激活,密码是否一致(加密后(加盐)看与库里存的是否一致)。
  • 登录成功,生成登录凭证,创建loginTicket实体,存入信息,往数据库存mapper.insert(loginTicket)。凭证是随机生成字符串CommunityUtil.generateUUID()
  • 把凭证发给客户端,放loginTicket对象和ticket凭证都可以
    map.put(“ticket”, loginTicket.getTicket());

补充在UserService.java的登录代码:

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

4、表现层LoginController.java

收到表单(POST)给UserService处理,登录成功回首页、登录失败回登录页面.
补充在LoginController.java

  • 访问路径相同时,method的方法不同也能区分开,一个是GET,一个是POST.

    @RequestMapping(path = "/login", method = RequestMethod.POST)
    
  • 表单传入条件:账号username 密码password 验证码code “记住我”rememberme;返回数据、响应时需要model,页面传的验证码code要和服务器生成的的验证码(在session里)比较,要从session中取出来需要HttpSession session,登录成功要把ticket发给客户端让它保存,用cookie保存需要HttpServletResponse response

        public String login(String username, String password, String code, boolean rememberme,
                            Model model, HttpSession session, HttpServletResponse response) {
    
  • 先检查验证码(从session里get),为空或不匹配,返回登录页面

            // 检查验证码
            String kaptcha = (String) session.getAttribute("kaptcha");
            if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
                model.addAttribute("codeMsg", "验证码不正确!");
                return "/site/login";
            }
    
  • 调用userService的login方法,返回map

    		int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
    		Map<String, Object> map = userService.login(username, password, expiredSeconds);
    
  • 含有ticket凭证,给ticket创建cookie,设置路径,最长登录时间,加到responsed对象里,响应时发送给浏览器,登录成功返回首页。

            if (map.containsKey("ticket")) {
                Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
                cookie.setPath(contextPath);
                cookie.setMaxAge(expiredSeconds);
                response.addCookie(cookie);
                return "redirect:/index";
    
  • 不含有ticket凭证,返回账号、密码的错误信息,从map里面get。map.get返回null也不会有影响,不是usernameMsg有问题,就是passwordMsg有问题。

            } else {
                model.addAttribute("usernameMsg", map.get("usernameMsg"));
                model.addAttribute("passwordMsg", map.get("passwordMsg"));
                return "/site/login";
            }
    

一些细节处理:

  • 是否点击“记住我”:在util包下补充CommunityConstant.java ,记录常量:登录凭证的超时时间(默认12小时,“记住我”3个月),该类实现CommunityConstant接口可以利用里面的常量public class LoginController implements CommunityConstant
        /**
         * 默认状态的登录凭证的超时时间
         */
        int DEFAULT_EXPIRED_SECONDS = 3600 * 12;
    
        /**
         * 记住状态的登录凭证超时时间
         */
        int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;
    
  • cookie要设置路径,在resources的application.properties配置文件中,有server.servlet.context-path=/community,即所有路径都在/community下,可以通过注解注入或repuest对象获得,这里用注解注入方式@Value(),将变量server.servlet.context-path给到属性contextPath.
            @Value("${server.servlet.context-path}")
            private String contextPath;
    

5、设置登录页面login.html

<form class="mt-5" method="post" th:action="@{/login}">

每一个框上加name="username",name="password",name="code"和LoginController中login的方法的参数要一致。
如果出错,服务器给浏览器的默认值还是上一次请求中的值:th:value="${param,username}"th:value="${param.password}"th:checked="${param.rememberme}"
规则:如果参数是实体,这里是user,Spring MVC会把user放到model里,页面上可以直接得到user对象的数据(这是从model里面获得的);如果是普通参数,如基本类型,字符串,不会把参数放到model里,可以认为放到model里,但是是存在request对象里的,可以通过request.getParameter获得,因为是请求里携带过来的,当程序执行到html文件时,可以从request里面取值。
thymleaf里语法是th:value="${param,username}"th:value="${param.password}"
每次验证码要刷新,所以不用给它默认值
“记住我”出错,√应该还在,是通过check设置的,动态判断th:checked="${param.rememberme}",从request取参数。

其他错误提示:账号相关提示,动态

			<div class="invalid-feedback" th:text="${usernameMsg}">
					该账号不存在!
			</div>
			<div class="invalid-feedback" th:text="${passwordMsg}">
				密码长度不能小于8!
			</div>
			<div class="invalid-feedback" th:text="${codeMsg}">
				验证码不正确!
			</div>

是否显示该样式’is-invalid’:如果确实有问题Msg才显示这个样式’is-invalid’,否则为空。class不应该写死,是固定样式+动态样式。

			<input type="text" th:class="|form-control ${usernameMsg!=null?'is-invalid':''} |"
				   th:value="${param.username}"
				   id="username" name="username" placeholder="请输入您的账号!" required>
			<input type="password" th:class="|form-control ${passwordMsg!=null?'is-invalid':''}|"
				   th:value="${param.password}"
				   id="password" name="password" placeholder="请输入您的密码!" required>
			<input type="text" th:class="|form-control ${codeMsg!=null?'is-invalid':''}|"
				   id="verifycode" name="code" placeholder="请输入验证码!">

二、写退出功能

退出就是把login_ticket表中的status属性从0变成1,根据ticket改status。并不改变其他的记录。

1、数据层接口写过了LoginTicketMapper.java

写过了,这里再看一下

    //退出,状态改变
    @Update({
            "<script>",
            "update login_ticket set status=#{status} where ticket=#{ticket} ",
    })
    int updateStatus(String ticket, int status);

2、业务层UserService.java

    public void logout(String ticket) {
        loginTicketMapper.updateStatus(ticket, 1);
    }

3、表现层LoginController.java

  • 通过注解@CookieValue()接收浏览器的cookie,key为"ticket",String ticket为用该参数接收key为ticket的cookie。
  • 调用业务层的logout方法,登出
  • 返回首页(重定向,getpost默认get请求)
    @RequestMapping(path = "/logout", method = RequestMethod.GET)
    public String logout(@CookieValue("ticket") String ticket) {
        userService.logout(ticket);
        return "redirect:/login";
    }

修改index.html中退出登录按键,改退出路径th:href=:@{/logout}

三、测试

先检查验证码是否正确,验证码正确后才检查账号,密码。
在这里插入图片描述

在浏览器登录后,开检查选项,在Application下选择cookies选项,看到对应的ticket(32位UUID)
在这里插入图片描述
在这里插入图片描述
status是是否登录状态,为1的是退出后的(不会删除数据),0是当前登录的。

注:html中,@{}路径 ${}变量 #{}取id

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值