Cookie 和 Session
一、回忆 Cookie
在讲 HTTP 协议时,我们曾提到过 Cookie:https://blog.csdn.net/yyhgo_/article/details/128433565?spm=1001.2014.3001.5501
HTTP 协议自身是属于 “无状态” 协议。
“无状态” 的含义:默认情况下 HTTP 协议的客户端和服务器之间的这次通信,和下次通信之间没有直接的联系。
但是实际开发中,我们很多时候是需要知道请求之间的关联关系的!
例如登陆网站成功后,第二次访问的时候服务器就能知道该请求是否是已经登陆过了。
Cookie 就是浏览器在本地存储数据的一种机制 (存到硬盘上)。
Cookie 中存储了一个字符串,这个数据可能是客户端 (网页) 自行通过 JS 写入的,也可能来自于服务器 (服务器在 HTTP 响应的 header 中通过 Set-Cookie 字段给浏览器返回数据)!
浏览器提供的持久化存储方案,有好几种:
Cookie是最经典的一种方案 (最老);
LocalStorage是比较新的一种方案;
indexDB是更新的方案…
查看Cookie:
使用cookie作为保存数据的手段,只能存一些简单的键值对信息,简单的字符串~~ 你想让它存个图片、视频、存个flash… 万万做不到!
比如,可以使用cookie存:1.上次访问页面的时间;2.当前网页的访问次数;3.当前访问页面的身份信息 (身份标识 id)… 都是程序员自己控制的 ~
- Cookie从哪儿来?
Cookie是存在浏览器的,来源是服务器。
- Cookie到哪儿去?
Cookie典型的一个应用场景:在客户端维持登陆状态。
在某个网站上登陆成功后,浏览器就会记住当前登陆用户的身份信息,然后接下来访问网站的其他页面,服务器也能知道是谁在登陆。(总不能每次打开一个页面都重新登陆一下)
服务器要给很多客户端提供服务,每个客户端都对应一组键值对~~
很多网站都是基于这一套来实现的 (主流方式);也有一些使用localStorage等一些新的方法。
是不是非常类似于医院的就诊卡呢?在不同的科室这张卡都能标识身份信息~~
二、理解会话机制 (Session)
服务器同一时刻收到的请求是很多的。服务器需要清除的区分清楚每个请求是从属于哪个用户,就需要在服务器这边记录每个用户令牌以及用户的信息的对应关系。
在上面的例子中,就诊卡就是一张 “令牌”。要想让这个令牌能够生效,就需要医院这边通过系统记录每个就诊卡和患者信息之间的关联关系。
会话的本质就是一个 “哈希表”,存储了一些键值对结构。key 就是令牌的 ID(token/sessionId),value 就是用户信息 (用户信息可以根据需求灵活设计)。
sessionId 是由服务器生成的一个 “唯一性字符串”,从 session 机制的角度来看,这个唯一性字符串称为 “sessionId”。但是站在整个登录流程中看待,也可以把这个唯一性字符串称为 “token”。
sessionId 和 token 就可以理解成是同一个东西的不同叫法 (不同视角的叫法)。
- 当用户登陆的时候,服务器在 Session 中新增一个新记录,并把 sessionId / token 返回给客户端 (例
如通过 HTTP 响应中的 Set-Cookie 字段返回)。 - 客户端后续再给服务器发送请求的时候,需要在请求中带上 sessionId/ token (例如通过 HTTP 请求
中的 Cookie 字段带上)。 - 服务器收到请求之后,根据请求中的 sessionId / token 在 Session 信息中获取到对应的用户信息,再进行后续操作。
Servlet 的 Session 默认是保存在内存中的,如果重启服务器则 Session 数据就会丢失!
三、Cookie 和 Session 的区别
Cookie 是客户端的机制;Session 是服务器端的机制。
Cookie 和 Session 经常会在一起配合使用 (并不是必须)。
完全可以用 Cookie 来保存一些数据在客户端,这些数据不一定是用户身份信息,也不一定是 token / sessionId;Session 中的 token / sessionId 也不需要非得通过 Cookie / Set-Cookie 传递 ~~
四、核心方法
在 Servlet 中也专门提供了相关的API,让我们来操作 Cookie 和 Session。
4.1 HttpServletRequest 类中的相关方法
方法 | 描述 |
---|---|
HttpSession getSession() | 在服务器中获取会话。参数如果为 true, 则当不存在会话时新建会话;参数如果为 false, 则当不存在会话时返回 null |
Cookie[] getCookies() | 返回一个数组, 包含客户端发送该请求的所有的 Cookie 对象。会自动把 Cookie 中的格式解析成键值对 |
getSession() 使用模式有两种:
1)参数填写 false:判定当前会话是否存在 (根据请求中的 Cookie 里的 sessionld 来查哈希表)。如果不存在直接返回 null;存在则返回对应的HttpSession对象。
2)参数填写true:判定当前会话是否存在 (根据请求中的 Cookie 里的 sessionld 来查哈希表)。如果不存在就创建一个新的键值对,保存到哈希表中,并把生成的 sessionld 返回到浏览器这里;如果存在则直接返回对应的 HttpSession 对象。
getCookies():
直接把请求中的 Cookie 都获取到。Cookie 本身也是键值对结构。
请求里的 Cookie 可能有多组键值对,就直接使用数组表示。
4.2 HttpServletResponse 类中的相关方法
方法 | 描述 |
---|---|
void addCookie(Cookie cookie) | 把指定的 cookie 添加到响应中 |
返回响应,你想给浏览器返回哪些 cookie 都可以通过这个方法来添加。
在这里添加的键值对,都会体现在 HTTP 响应报文的 Set-Cookie 字段上。
4.3 HttpSession 类中的相关方法
一个 HttpSession 对象里面包含多个键值对,我们可以往 HttpSession 中存任何我们需要的信息!
方法 | 描述 |
---|---|
Object getAttribute(String name) | 该方法返回在该 session 会话中具有指定名称的对象,如果没有指定名称的对象,则返回 null |
void setAttribute(String name, Object value) | 该方法使用指定的名称绑定一个对象到该 session 会话 |
Object removeAttribute(String name) | 删除在该 session 会话中具有指定名称的对象 |
boolean isNew() | 判定当前是否是新创建出的会话 |
4.4 Cookie 类中的相关方法
每个 Cookie 对象就是一个键值对。
方法 | 描述 |
---|---|
String getName() | 该方法返回 cookie 的名称。名称在创建后不能改变 (这个值是 Set-Cookie 字段设置给浏览器的) |
String getValue() | 该方法获取与 cookie 关联的值 |
void setValue(String newValue) | 该方法设置与 cookie 关联的值 |
- HTTP 的 Cookei 字段中存储的实际上是多组键值对,每个键值对在 Servlet 中都对应了一个 Cookie 对象
- 通过 HttpServletRequest.getCookies() 获取到请求中的一系列 Cookie 键值对
- 通过 HttpServletResponse.addCookie() 可以向响应中添加新的 Cookie 键值对
五、代码示例: 实现用户登陆
实现简单的用户登陆逻辑。
这个代码中主要是通过 HttpSession 类完成,并不需要我们手动操作 Cookie 对象。
5.1 需求
1)登陆页面。用户可以填写用户名 + 密码 (login.html)
2)Servlet 来处理登陆请求。(LoginServlet.java)
3)使用另一个 Servlet 来生成主页内容 (登陆成功后,跳转到的页面) (IndexServlet)
5.2 登录页面的实现
注意代码注释!
login.html:
<!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>
<!-- 提交的数据交给 LoginServlet 来处理的 -->
<form action="login" method="post">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="登陆">
</form>
</body>
</html>
LoginServlet.java:
package login;
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 {
// 1. 从请求中获取到页面提交的用户名和密码
String username = req.getParameter("username");
String password = req.getParameter("password");
// 2. 验证用户名密码是否正确
// 正常这个操作要查数据库. 此处为了简单, 直接写成硬编码了. (把正确的用户名和密码写死了)
// 假定正确的密码是 zhangsan 123
if ("zhangsan".equals(username) && "123".equals(password)) {
// 登陆成功
// a) 创建一个会话. 用户刚登陆成功, 之前是没有会话的. 重新分配个新的会话给用户.
// getSession
// * 创建 sessionId 和一个 HttpSession 对象
// * 把这两个内容以键值对的形式插入到内存的 哈希表 里
// * 把 sessionId 通过 Set-Cookie 写到响应中.
HttpSession session = req.getSession(true);
// 随意的设置 "键值对" 了. (HttpSession 对象自身也相当于是一个 哈希表)
session.setAttribute("username", "zhangsan");
// b) 让响应重定向到 "主页"
resp.sendRedirect("index");
} else {
// 登陆失败
resp.setStatus(403);
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("登陆失败, 用户名或者密码错误");
}
}
}
这里通过
"zhangsan".equals(username)
比较,而不是username.equals("zhangsan")
,避免了空指针异常 ~~
- 此处的 getSession 参数为 true,表示查找不到 HttpSession 时会创建新的 HttpSession 对象,并生成一个 sessionId,以键值对的形式插入到 哈希表 中,并且把 sessionId 通过 Set-Cookie 返回给浏览器。
通过 http://127.0.0.1:8080/yyhjava/login.html 访问页面:
登录"zhangsan","123"后跳转页面:
此时还并没有实现跳转页面,所以就是 404 ~~
在登录用户名为 “zhangsan” 时,使用 Fiddler 抓包:
第一个为由 form 表单发出的 POST 请求:
请求:
响应:
第二个为页面跳转的 GET 请求,此时为 404:
请求:
响应:
Cookie 和 Session 细节:
1)在登录页面的响应中,有这样一行:
这就是通过 getSession 操作创建会话的同时,生成 sessionld 并且把这个 sessionld 通过 Set-Cookie 返回给浏览器了!!!
会话的 sessionld 是一个键值对结构:JSESSIONID=82ABD3C7775819D26A7F6ECA46C1355A
Key:JSESSIONID (Servlet生成的名字);Value:一串十六进制数字
每个会话的sessionld都不同!!!
2)在页面跳转的请求中,有这样一行:
这就是刚才设置的 sessionld 啊 ~ 说明已经保存在浏览器中 (本地)了!
当然也可以在浏览器查看:(点击 URL 前的小图标 ~)
是不是很眼熟?当然一模一样 ~
由此总结:
HttpSession session = req.getSession(true);
代码虽短,干的活却很多!
1)创建 sessionId 和一个 HttpSession 对象
2)把这两个内容以键值对的形式插入到内存的 哈希表 里
3)把 sessionId 通过 Set-Cookie 写到响应中
5.3 跳转页面的实现
package login;
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 {
// 首页中也先获取 session, 此处的 session 正是刚才登陆页的时候, 登陆成功的逻辑中创建出来的.
// 此处 参数 写作 false 即可. 表示不新建. 如果不存在, 就返回 null 就是了.
HttpSession session = req.getSession(false);
if (session == null) {
resp.setStatus(403);
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("您尚未登录, 不能访问主页!");
return;
}
String username = (String) session.getAttribute("username");
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("欢迎来到主页! " + username);
}
}
- 在这个代码中是看不到 “哈希表”,也看不到 sessionId 这样的概念的。getSession 操作内部提取到请求中的 Cookie 里的 sessionId,然后查找哈希表,获取到对应的 HttpSession 对象。
- getSession 参数为 false,则获取不到 HttpSession 对象,不会创建新的 HttpSession,而是返回 null。此时说明用户没有登陆。
保存了什么,才能取出什么 ~~
此处的会话是根据 sessionld 来查哈希表的。
因此只要把浏览器这里的 cookie 删了,不登录就直接访问 index 页面,就会触发这个 if 逻辑了:
这样删除:
要想触发这个 if 逻辑可以删掉 cookie,是不是也可以删掉服务器这里存储会话的哈希表?
这个哈希表是隐藏在 Servlet 背后的男人,没有 API 能直接访问!
有同学可能会想到:这个东西应该是作为内存中的变量,那么重启服务器 tomcat 不就把内存的数据给重置了嘛?!
不一定!这是 IDEA / smart tomcat 的锅 ~~
在开发阶段可以帮助我们调试代码的时候节省时间。
但在生产环境上部署所面临的情况,就是一般的 tomcat,这点一定要明确 ~~
此时就可以成功实现逻辑了!!!登录后: