HTTP 协议自身是属于 “无状态” 协议,默认情况下 HTTP 协议的客户端和服务器之间的这次通信和下次通信之间没有直接的联系。
但是实际开发中,我们很多时候是需要知道请求之间的关联关系的。例如登陆网站成功后,第二次访问的时候服务器就能知道该请求是否是已经登陆过了。
Cookie和Session的主要目的就是为了弥补HTTP的无状态特性
Cookie和Session
一. 基本概念
1. Cookie
- Cookie是HTTP协议中的一个字段,同时也是浏览器在客户端这边保存数据的一种比较古老的方式。
- Cookie是服务器产生的,通过HTTP响应的Set-Cookie字段来进行设置返回给浏览器的。Cookie在浏览器这边存储,浏览器会根据域名/地址来分别存储Cookie,并与下一个请求一起发送到服务器,通常,它用于判断两个请求是否来自同一用户,来决定返回什么样的页面。比如不同用户的购物车,游戏得分信息都是不一样的
- Cookie存的是键值对结构的字符串,此处的键值对都是程序员自定义的。
Cookie是在浏览器这边存储的机制
举个栗子:
我们去医院看病时应该先挂号,挂号时候需要提供身份证,同时得到了一张 “就诊卡”,这个就诊卡就相当于患者的 “令牌”。后续去各个科室进行检查、诊断、开药等操作,都不必再出示身份证了,只要凭就诊卡即可识别出当前患者的身份。
当我们看完病了之后,不想要就诊卡了,就可以注销这个卡。此时患者的身份和就诊卡的关联就销毁了(类似于网站的注销操作)。当我们再来看病时,可以办一张新的就诊卡,此时就得到了一个新的 “令牌”
此时在服务器这边就需要记录令牌信息,以及令牌对应的用户信息,这个就是 Session 机制所做的工作
2. Session
服务器同一时刻收到的请求是很多的,而且还需要清楚地区分出不同的请求是从属于哪个用户,就需要在服务器这边记录每个用户令牌以及用户的信息的对应关系;在上面的例子中,就诊卡就是一张 “令牌”。要想让这个令牌能够生效,就需要医院这边通过系统记录每个就诊卡和患者信息之间的关联关系。
- Session的本质就是一个 “哈希表”,存储了一些键值对结构。key 就是令牌的 ID(token/sessionId),value 就是用户信息(用户信息可以根据需求灵活设计)。
- sessionId 是由服务器生成的一个 “唯一性字符串”,从 session机制的角度来看,这个唯一性字符串称为 “sessionId”。但是站在整个登录流程中看待,也可以把这个唯一性字符串称为 “token”.
- sessionId 和token就可以理解成是同一个东西的不同叫法(不同视角的叫法)
- 当用户登陆的时候, 服务器在Session中新增一个新记录, 并把 sessionId / token 通过Set-Cookie字段返回给客户端
- 客户端后续再给服务器发送请求的时候,需要在请求中带上 sessionId/ token(例如通过HTTP请求中的 Cookie 字段带上).
- 服务器收到请求之后,根据请求中的sessionId / token在 Session信息中获取到对应的用户信息后再进行后续操作
- Servlet 的Session默认是保存在内存中的,如果重启服务器则Session数据就会丢失
举个栗子,当用户第一次登录某网站添加了相关商品到自己购物车并退出后,第二次想要再次查看自己的购物车信息时,其流程如下:
Session是在服务器这边存储的机制
二. 联系
- Cookie和Session之间往往要相互配合,但不是非得配合
- Cookie 是客户端的机制,Session是服务器端的机制
- 完全可以用Cookie来保存一些数据在客户端,这些数据不一定是用户身份信息,也不一定是token / sessionId
- Session中的 token / sessionId 也不需要非得通过 Cookie / Set-Cookie 传递
三. 用户登录案例
1. 具体实现思路
我们用前面讲的知识可以自己实现一个用户登录的案例,其具体思路是:
- 首先要有一个
html
,包含用户名密码的输入框,以及登录按钮然后要有一个LogServlet
,来处理登录请求。 - 然后要有一个
LogServlet
,来处理登录请求。 - 最后再搞一个
IndexServlet
,模拟登录完成后,跳转到的主页。在这个主页里面就能够获取到用户的身份信息(这里就可以存程序员自定义的用户数据。比如可以存一个当前用户访问的次数)
2. 编写静态登录页面
我们可以简单写一个登录页面(没必要太复杂,主要是学会核心登录逻辑,大家可以自行美化)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录页面</title>
</head>
<body>
<form action="login" method="post">
用户名:<input type="text" name="username">
<br>
密码:<input type="password" name="password">
<br>
<input type="submit" value="登录">
</form>
</body>
</html>
3. 处理登录请求
(1)LoginServlet类
step1:从请求的 body 中读取用户名和密码
String username = req.getParameter("username");
String password = req.getParameter("password");
step2:判定一下用户名密码是否正确(此处就不读数据库了, 直接固定用户名密码,我们直接将用户名设置成zhangsan,将密码设置成123
if (!"zhangsan".equals(username) || !"123".equals(password)) {
// 登录失败!!
resp.getWriter().write("登录失败!");
return;
}
step3:登陆成功
System.out.println("登录成功");
- 登录成功,则创建出一个会话来,会话不存在就创建
- 会话是根据请求中的sessionId来查的,sessionId是在 Cookie中的,此处是首次登录,此时请求中是没有Cookie (Cookie 是服务器返回),就会触发 “找不到就创建” 这样的流程
- 同时这里进行的操作:先创建出一个HttpSession对象(作为 value),再生成一个随机的字符串,作为sessionId(key);然后把这个key和value插入到哈希表中
- 同时把这个生成的sessionId通过Set-Cookie字段返回给浏览器
HttpSession httpSession = req.getSession(true);
// 还可以存入程序员自定义的数据, 可以存入身份信息(用户名和登录次数)
httpSession.setAttribute("username", "zhangsan");
httpSession.setAttribute("loginCount", 0);//自动装箱机制帮助我们把整数0转换成包装类
step4:让页面跳转到主页, 使用重定向的方式实现即可
resp.sendRedirect("index");
完整的LoginServlet类代码:
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf-8");
String username = req.getParameter("username");
String password = req.getParameter("password");
if (!"zhangsan".equals(username) || !"123".equals(password)) {
resp.getWriter().write("登录失败!");
return;
}
System.out.println("登录成功");
HttpSession httpSession = req.getSession(true);
httpSession.setAttribute("username", "zhangsan");
httpSession.setAttribute("loginCount", 0);
resp.sendRedirect("index");
}
}
(2)IndexServlet类
核心步骤:
- 判断用户是否成功登录
- 获取用户信息
- 返回一个html页面
完整的IndexServlet类代码:
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/index")
public class IndexServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 根据当前用户请求中的 sessionId, 获取到用户信息, 并显示到页面上.
resp.setContentType("text/html; charset=utf-8");
// 1. 判定当前用户是否已经登录了. (请求中有没有 sessionId, 以及 sessionId 是否合法)
// 如果会话不存在, 就不能创建了~ 只是查询, 不是登录
HttpSession httpSession = req.getSession(false);
if (httpSession == null) {
// 当前没有找到合法会话, 当前用户尚未登录, 重定向到 login.html, 让用户进行登录
resp.sendRedirect("login.html");
return;
}
// 2. 如果用户已经登录, 就可以从 HttpSession 中拿到用户信息了.
String username = (String) httpSession.getAttribute("username");
Integer loginCount = (Integer) httpSession.getAttribute("loginCount");
loginCount = loginCount + 1;
httpSession.setAttribute("loginCount", loginCount);
// 3. 返回一个 HTML 页面
StringBuilder html = new StringBuilder();
html.append("<div>用户: " + username + "</div>");
html.append("<div>访问次数: " + loginCount + "</div>");
resp.getWriter().write(html.toString());
}
}
4. 验证程序
在完成前面所有的步骤后,我们就可以验证程序了(我们通过Smart Tomcat进行打包部署程序)
登录成功后,我们也可以通过Fiddler捕捉到浏览器和服务器之间的两次交互了:
客户端和服务器的第一次交互为一个POST请求,body中就包含了用户名和密码的信息:
也可以看到响应中的sessionId字段:
这个Location就表示我们要重定向到的位置,正是因为这个重定向机制,就触发了浏览器和服务器之间的第二次交互:
第二次交互中的请求是一个GET请求
其中也包括了和之前服务器返回的sessionId相同的字段
最后,可以看到响应中也是有相应的html内容的。