重点:登录时,浏览器提交表单后,先检查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