文章目录
- 一. Cookie与Session
- 1. Cookie与Session
- 2. Servlet会话管理操作
- 二. 登录逻辑的实现
一. Cookie与Session
1. Cookie与Session
首先, 在学习过 HTTP 协议的基础上, 我们需要知道 Cookie 是 HTTP 请求报头中的一个关键字段, 本质上是浏览器在本地存储数据的一种机制, 要清楚 Cookie 是从哪里来, 要到哪里去.
Cookie
是来自于服务器的, 通过响应报文中的 Set-Cookie
字段将数据返回保存在浏览器本地的; 后续当浏览器访问服务器的时候, 就会把本地的 Cookie
通过 HTTP
请求给带过去.
HTTP 协议是 “无状态” 协议, 这里的 “无状态” 指的是默认情况下 HTTP 协议的客户端和服务器之间的每次通信, 和它之前之后的通信是没有直接的联系的, 但是在实际开发中多次通信是需要建立起 “上下文” 联系的, 用户发起请求通过 Cookie 字段将 Cookie 中的内容发送到服务器, 服务器就能知道和客户端的上一次通信处于什么样的状态, 此时 Cookie 这种机制就有了用武之地, 最典型的一种应用, 就是使用 Cookie 来标识用户的身份信息.
常见的网站登录, 比如淘宝, 我们登录一次网站后, 后续再使用访问淘宝的其他页面, 是不需要再次登录的, 还有自动登录功能, 隔了一段时间再次访问淘宝网站, 我们会发现此时并不需要再次输入账号密码登录, 网站就会自动地帮我们登录.
为了实现这种网站自动登录和访问的功能, 就可以将 Session
和 Cookie
搭配使用, 在用户在输入账号密码登录在淘宝服务器查询数据库验证通过后, 服务器会创建一个 Session 会话来保存当前用户的数据和信息, 并生成一个 Cookie 数据.
该 Session 中包含用户一些关 '键身份信息, 服务器会给这个用户分配一个表示身份的序号, 是具有唯一性的整数/字符串 (SessionId), 服务器使用类似于 Hash
表这样的结构把身份序号 (SessionId
) 作为 Key
, 身份信息 (Session
) 作为 Value
存储起来, 这样的每一对键值对就是一个 Session 会话.
而服务器给客户端返回的 Cookie 里面就包含 SessionId, 浏览器就会在本地将这个 Cookie 储存起来, 后续浏览器发送请求的时候就会带上这个 Cookie, 服务器收到 Cookie 中的身份序号后, 就会查询 Session 会话表, 如果存在就会可以正常访问, 不用重复的输入账号与密码, 否则就需要用户重新输入账号密码进行登录.
有时候我们会发现登录网站后隔一段时间再次登录, 会出现让我们再次输入账号密码的情况, 此时就是登录状态失效过期了, 这种情况可能是可能是客户端把 Cookie 删了, 也可能是服务器这把对应的身份信息删了.
举个生活中里例子, 去医院看病需要先挂号, 如果你是第一次去这家医院的, 就会给你新办理一个就诊卡, 这个就诊卡里面就含有你的一些关键身份信息, 并且会在医院的服务器上新建一个档案, 拿着这个就诊卡, 你就可以在该医院的各个科室进行刷卡, 如果你之前在这个医院有就诊记录, 你一刷卡就可以查询到你所有在医院的就诊信息.
这个例子中的就诊卡就相当于是 Cookie, 上面有你最基本的身份信息, 在医院服务器上所储存有关你的详细信息, 就相当于一个 Session 会话, 服务器上包含很多用户的就诊信息, 即会有多个 Session 会话对应不同的用户.
🎯要注意理解 Cookie 和 Session 之间的区别和关联.
- 关联: 在网站登录功能中可以搭配使用.
- 区别:
- Cookie 是客户端的存储机制, Session 是服务器的存储机制.
- Cookie 里面可以存各种键值对 (还可以存除 SessionId 以外的), Session 则专门用来保存用户信息.
- Cookie 完全可以单独使用, 不搭配 Session (实现非登录的场景), Session 也可以不搭配 Cookie (手机 App 登录服务器, 此时也需要 Session, 但这里没有 Cookie 的概念, Cookie 和浏览器强相关的).
- Cookie 是 HTTP 协议中的一个部分, Session 则可以和 HTTP 无关 (TCP, WebSocket …也可以用 Session).
2. Servlet会话管理操作
在 HttpServletRequest
类中, 可以使用 getSession
来获取或者创建会话, getCookies
可以获取请求中的 Cookie 列表.
方法 | 描述 |
---|---|
HttpSession getSession() | 在服务器中获取会话. 参数如果为 true, 则当不存在会话时新建会话; 参数如果为 false, 则当不存在会话时返回 null |
Cookie[] getCookies() | 返回一个数组, 包含客户端发送该请求的所有的 Cookie 对象. 会自动把Cookie 中的格式解析成键值对. |
调用 getSession
方法所做的事情:
getSession
有一个 boolean
类型的参数, 如果参数是 true
, 它有如下行为:
- 读取
cookie
里的sessionId
字段. - 根据
sessionld
来查询对应的HttpSession
对象在服务器上是否存在. - 如果不存在, 就创建一个新的会话, 即创建一个新的
HttpSession
对象, 并生成一个唯一的sessionId
, 会以新生成的sessionId
作为Key
, 生成的HttpSession
对象作为Value
, 以键值对形式储存到类似于Hash
的结构中, 然后将sessionId
设置到响应报文中的set-Cookie
字段返回给浏览器. - 如果存在就直接返回查询到的
HttpSession
对象.
如果参数是 false
, 行为如下:
- 读取
cookie
里的sessionId
字段. - 根据
sessionld
来查询对应的HttpSession
对象在服务器上是否存在. - 如果不存在, 直接返回
null
. - 如果存在就直接返回查询到的
HttpSession
对象.
总之就是, getSession
的参数为true
时允许创建 Session
会话, 为false
时不允许创建 Session
会话.
🍂关于HttpSession
这个对象也可以看作是一个哈希表, 是以键值对的形式存储数据的, 并且允许程序员在对象中储存任意的键值对数据, 但是 Key
必须是 String
类型, Value
的类型是 Object
, 设置就比较随意了.
该类中提供了两个方法可以用来获取该对象中键值和设置键值对.
方法 | 描述 |
---|---|
Object getAttribute(String name) | 查询 session 会话中指定键的键值, 查不到则返回 null. |
void setAttribute(String name, Object value) | 绑定一个键和值到该 session 会话中 |
boolean isNew() | 判定当前是否是新创建出的会话 |
所以服务器组织会话的方式就如下图:
二. 登录逻辑的实现
我们这里模拟实现一个登录的功能, 很多网站都会让你先登录, 才能使用其中的一些功能, 我们这里实现登录完成之后, 就跳到另一个主页, 不进行登录的话, 这个主页是不能被访问的.
所以, 我们这里主要涉及到两个页面, 第一个是登录页面, 第二个是登录成功后要跳转的主页面.
登录页面包含两个输入框 (用来输入用户名和密码) 和一个登录按钮, 点击登录按钮就会发起一个 HTTP 请求, 服务器处理这个请求的时候就会验证用户名和密码, 验证通过就会跳转到主页, 主页就简单的显示出当前用户的用户名就行了.
实现整这里的逻辑就涉及到两个 Servlet 类:
- 处理登录的
LoginServlet
, 用来判段用户名和密码, 登录成功和登录失败的情况. - 构造主页面的
IndexServlet
, 用来构造登录成功的主页.
1️⃣第一步, 约定前后端接口.
我们需要实现两套交互逻辑, 一是登录跳转, 二是获取主页.
登录跳转, 这里只是示范作用, 约定只有一个用户, 就不加数据库的逻辑了.
约定用户名就是 zhangsan
, 密码就是 123
, 使用 POST
请求, 响应采用 302
重定向.
获取主页, 采用 GET
请求, 响应返回一个页面.
2️⃣第二步, 编写前端交互页面
目标页面如下:
这里的场景很简单. 就直接使用 form
表单构造来 Post
请求了.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form action="login" method="post">
<span>用户名</span>
<input type="text" name="username">
<span>密码</span>
<input type="password" name="password">
<input type="submit" value="登录">
</form>
</body>
</html>
3️⃣第三步, 编写后端处理代码
对于登录跳转主页的 Post 请求处理思路如下:
- 从请求中获取用户名和密码.
- 验证用户名和密码是否正确, 正常来说是要查询数据库的, 这里就不添加数据库的逻辑了.
- 如果验证通过, 创建会话, 并将
username
和主页被访问的次数数据添加到会话中 (保存必要的用户信息), 创建好会话后, 重定向到主页index
. - 如果验证不通过, 重定向到登录页面.
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 {
req.setCharacterEncoding("utf8");
resp.setCharacterEncoding("utf8");
// 获取用户名和密码
String username = req.getParameter("username");
String password = req.getParameter("password");
// 验证用户名密码是否正确
// 这里约定合法的用户名为zhangsan,密码是123
if (username.equals("zhangsan") && password.equals("123")) {
// 登陆成功!!
// 创建一个会话
HttpSession session = req.getSession(true);
// 把当前的用户名保存到会话中
session.setAttribute("username", username);
// 设置初始情况下登录成功访问主页的次数为0
session.setAttribute("count", 0);
// 重定向到主页
resp.sendRedirect("index");
} else {
// 登陆失败!!
// 重定向到 登陆页面
System.out.println("用户名或者密码错误!");
resp.sendRedirect("login.html");
}
}
}
获取主页的 GET 请求处理思路:
- 获取会话 (请求中包含
sessionId
, 会话是根据sessionId
获取的) - 取出会话信息, 将主页返回次数加
1
并写回到会话信息中. - 返回一个简单的页面, 显示用户名和主页访问次数.
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 {
// 重定向, 浏览器发起的是GET请求
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 先判定用户的登陆状态.
// 如果用户还没登陆, 要求先登陆.
// 已经登陆了, 则根据会话中的用户名, 将相关信息显示到页面上.
HttpSession session = req.getSession(false);
if (session == null) {
// 未登录状态
System.out.println("用户未登录!");
// 重定向到登录页面
resp.sendRedirect("login.html");
return;
}
// 已经登陆, 取出会话信息
String username = (String)session.getAttribute("username");
Integer count = (Integer)session.getAttribute("count");
// 访问次数加 1 后再写回到会话中
count++;
session.setAttribute("count", count);
// 构造页面
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write("<h3>欢迎您!" + username + "</h3> <h4>这个主页已经被访问了" + count + "次</h4>");
}
}
抓包分析交互过程:
第一次交互, 浏览器从服务器获取登录页面.
第二次交互, 浏览器给服务器一个登录请求, 服务器返回响应, 重定向页面.
第三次交互, 浏览器收到 302
重定向响应后, 再次向服务器发起请求, 访问主页.
响应结果:
用户名或者密码错误的情况下:
未登录直接访问的情况下:
这里关于 IDEA
集成的 Tomcat
环境有一些需要注意的点, 正常来说, 上面说的 sessionId
并不会一直存在下去, 比如 Tomcat
服务器重新启动的时候, 原来服务器在内存中维护的会话 Hash
表就应该没有了, 此时再次访问, 就应该出现 sessionld
查不到, 就被识别成 “未登录” 状态了, 但是有些版本 Smart Tomcat
为了方便程序猿调试程序, 会在停止服务器的时候把会话持久化保存, 并且在下次启动的时候自动把会话恢复到内存中, 在这种情况下会话是不丢失的.
但如果是手动部署程序到 Tomcat
, 则会话默认还是在内存中, 重启服务器是会丢失的.