文章目录
参考牛客网高级项目教程
1.登录模块功能需求
- 通过输入用户名、密码、验证码,选择登录有效期,后台可以验证用户
- 验证成功,跳转主页,并可以访问多个页面请求
- 验证失败,返回登录表单,并提示错误信息
- 点击退出登录,将用户登录凭证状态改变,并返回登录页面
登录表单实现
- 在前面的2.2注册功能中的处理激活账户的模块中已经处理好基本页面显示
- 在2.4开发验证码模块,已经处理好登录页面的动态验证码刷新显示功能
![在这里插入图片描述](https://img-blog.csdnimg.cn/39a9bdcf551e4dad82526221f58eed98.png)
2. 登录功能实现
登录表单显示
- 在前面的2.2注册功能中的处理激活账户的模块中已经处理好基本页面显示
- 在2.4开发验证码模块,已经处理好登录页面的动态验证码刷新显示功能
![在这里插入图片描述](https://img-blog.csdnimg.cn/39a9bdcf551e4dad82526221f58eed98.png)
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>
测试结果
![在这里插入图片描述](https://img-blog.csdnimg.cn/ff40aee67852492080219afbfa82e25c.png)
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>