目录
回顾Cookie
前面介绍HTTP的时候已经有写到, 围绕Cookie的几个问题:
- Cookie是什么
Cookie是浏览器提供的持久化存储数据的机制.
像表白墙就是把数据存在服务器上, 但是有些时候也需要浏览器能够持续化存储数据. 那么"持久化"我们想到的就是存到文件上, 但是为了保证安全, 浏览器不允许一个网页直接访问文件系统, 毕竟这件事情太危险, 万一有恶意网站把电脑上的重要资料删除便得不偿失, 所以肯定不能这么办. 但是为了持久化存储数据, 浏览器就对持久化存储操作进行了封装, 不能让文件能够访问文件系统, 但是可以有限制的进行访问, 比如键值对这样的存储机制, 可以存储一些简单的数据, 但是想直接访问重要资料是不可能的.
- Cookie从哪里来
Cookie是从服务器返回给浏览器的.
服务器代码中由程序员决定要把什么样的信息保存到客户端, 然后通过HTTP响应的Set-Cookie字段, 把键值对写回去.
- Cookie到哪里去
Cookie是在客户端进行存储, 但是存储的目的是 下次请求服务器的时候自动带上, 所以Cookie会在后续浏览器访问服务器的时候带到请求的header中发给服务器.
注意: 服务器不是只给一个客户端提供服务. 同一时刻要处理多个客户端.那么此时就可以通过Cookie中的值来识别当前客户端是谁, 以及当前客户端的访问提供到哪个环节了...(客户端借助Cookie自报家门)
- Cookie存储在哪里
Cookie存储在浏览器(客户端)所在主机的硬盘上.
浏览器会根据 域名 来分别存储. 浏览器,每次访问一个网站, 都会根据这个网站的域名专门腾出一块空间来 就存储这个网站对应的Cookie.
Cookie的典型应用
Cookie的用途非常多, 其中最典型的应用就是 标识用户的身份信息.
Cookie应用: 登录功能
举一个例子, 很多网站都有一个登录功能.
以登录淘宝为例,
- 首先用户从浏览器发起请求获取到淘宝主页, 淘宝收到请求就把页面返回给浏览器.
这里要注意此时淘宝的服务器不知道用户的身份信息, 只是知道有一个客户端访问了它的主页,
- 接下来在主页上有一个登录按钮, 用户就可以通过这里输入用户名和密码进行登录.
用户发起登录请求, 淘宝收到登录请求之后, 返回登录成功的结果. 注意这里面淘宝就会针对登录操作查询数据库, 验证用户名和密码是否正确.
如果正确则登录成功, 然后淘宝会把当前用户的登录信息在内存中也保存一份, 同时给这个用户分配一个表示身份的序号(可能是个整数/字符串, 保证唯一性). 此时服务器会使用像hash表这样的结构, 把这个序号作为key, 身份信息作为value存储起来. 然后返回登录成功这个响应里面就会通过Set-Cookie把身份序号返回到客户端, 此时浏览器就会把身份序号保存到Cookie里了.
- 在后续的请求中, 服务器收到Cookie中的身份序号, 就会查询上述hash表, 判定用户是谁.
如果查到了, 就知道用户是谁了(可以避免重复输入账号密码); 如果没查到, 就会要求用户重新登录.
Cookie和Session之间的关联和区别
关联: 在网站登录功能中, 需要配合使用
区别:
- Cookie是客户端的存储机制. Session是服务器的存储机制.
- Cookie里面可以存各种键值对(还可以存别的). Session则专门用来保存用户的身份信息.
Cookie完全可以单独使用, 不搭配Session(实现非登录场景下)
Session也可以不搭配Cookie使用.(手机APP登录服务器, 服务器也需要Session, 此时就没有Cookie的概念) Cookie是跟浏览器强相关的.
Cookie是属于HTTP协议中的一个部分. Session则可以和HTTP无关(TCP, websocket... 也可以用Session)
代码示例: 模拟登录
在这个登录页中, 涉及到两个页面: 登录页面, 主页面.
登录页:
输入框输入用户名;
密码框输入密码;
登录按钮:点击按钮触发 登录请求->需要一个LoginServlet处理登录请求, 验证用户名密码是否 正确->登录成功, 跳转到 主页(另外一个页面, 可以是静态的html, 也可以是通过Servlet动态构建的页面)->IndexServlet, 在这个页面中显示用户的名字.
这个例子中涉及到两个页面:
- 登录页面
- 主页面
涉及到两个Servlet
- 处理登录的LoginServlet判定用户名密码
- 构造主页面的IndexServlet
我们在hello_servlet2中创建一个包login, 并创建LoginServlet类与IndexServlet类, 在webapp目录下创建前端页面.
- 编写登录页面
<form action="login" method="post">
<input type="text" name="username">
<br>
<input type="password" name="password">
<br>
<input type="submit" value="提交">
</form>
启动服务器, 页面就可以通过浏览器访问了, 我们查看一下页面效果.
可以看到, 登录页面已经有了. 那么我们可以写一些数据, 就会触发登录请求.
但是由于这里的login还没有实现, 所以是404.
那么接下来我们继续编写后续代码.
登录页面我们已经处理好了, 那么这样一个登录 一点击提交, 就会触发一个POST请求, 我们可以查看这个请求是 什么样子的.
可以看到, 通过我们上述的form表单构造出了POST请求. 还看到Content-Type: application/x-www-form-urlencoded, 可见这个特定Content-Type是和form表单相关的. body部分为username=zhangsan&password=123, 就是通过键值对的方式进行表示的. 所以这一段请求是和前面的前端代码息息相关的.
注意下图的对应关系.
- 编写LoginServlet处理登录请求.
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 {
String username = req.getParameter("username");
String password = req.getParameter("password");
// 验证用户名密码是否正确.
// 正常情况下, 用户名和密码会使用数据库来保存.
// 此处直接约定, 用户名合法的是 zhangsan 和 lisi
// 密码合法的都是 123
if (!username.equals("zhangsan") && !username.equals("lisi")) {
// 登录失败
// 重定向到 登录页面
System.out.println("登录失败, 用户名错误!");
resp.sendRedirect("login.html");
return;
}
if (!password.equals("123")) {
// 登录失败
System.out.println("登录失败, 密码错误!");
resp.sendRedirect("login.html");
return;
}
// 登录成功
// 1. 创建一个会话
HttpSession session = req.getSession(true);
// 2. 把当前的用户名保存到会话中. 此处HttpSession又可以当成一个map使用
session.setAttribute("username", username);
// 3. 重定向到主页
resp.sendRedirect("index");
}
}
注意这里的代码也是要和上方请求形成对应关系.
理解创建会话的基本含义
HttpSession session = req.getSession(true);
会话实际上是一个键值对, key是sessionId, value是一个HttpSession对象.
每个客户端登录的时候都有一个这样的键值对.(会话)
服务器要管理多个这样的会话, 那么就可以搞一个哈希表, 把这些会话组织起来.
getSession(true)
判定当前请求是否已经有对应的会话: 拿着请求中Cookie里的sessionId查哈希表, 如果sessionId不存在, 或者没查到, 就 创建新会话, 并且插入到哈希表中. 如果查到了, 直接返回查到的结果.
创建过程:
1. 构造一个HttpSession对象
2. 构造唯一的sessionId
3. 把这个键值对插入哈希表
4. 把sessionId设置到响应报文Set-Cookie字段中.
注意HttpSession对象自己也是一个键值对. 可以通过setAttribute和getAttribute来存取键值对.
服务器是如何组织会话的
- 编写IndexServlet生成主页.
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 {
// 通过重定向, 浏览器发起的是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");
// HttpSession value的类型是Object, 需要手动强转成String(设定Object意为存各种类型的都可以)
// 构造页面
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("欢迎 " + username + " 回来!");
}
}
注意这里的对应关系,
在登录页面的代码中, 重定向指定了一个相对路径"index", 所以对应到IndexServlet中的WebServlet也要写index, 两者要匹配.
getSession(true): 会话存在就返回现成的, 不存在就创建.
getSession(false): 会话存在就返回现成的, 不存在就返回null(不创建).
String username = (String) session.getAttribute("username");
此处可以这样来取, 前提是前面的登录操作中已经存过.
即: session.setAttribute("username", username);
这两个session是根据同一个sessionId对应到的同一个HttpSession对象.
运行程序, 并使用fidder查看代码的基本效果.
输入用户名和密码, 点击提交, 这里就触发了Login请求和index请求.
可以看到, 此时已经提示: 欢迎 zhangsan 回来!
注意关注这里的请求交互过程.
第一次交互, 进行登录操作.
第一次交互的请求: 注意, 此时这次请求是没有Cookie的.
第一次交互的响应: 响应中的Set-Cookie的JSESSIONID就是前文所提到的sessionId, 注意它的值是一个唯一的数字.
Location: index 就是接下来要跳转到的index页面
第二次交互请求: 可以看到, 这是由浏览器的重定向功能所构造出的GET请求.
首先这个GET的路径 /index 和上面的Location: index 是相对应的.
然后这里要重点关注Cookie: 可以看到, 第二次请求就会带有Cookie, 可以知道Cookie中的JSESSIONID的值正好是上面通过Set-Cookie返回的值. 这个值就被交给服务器这边了.
这个请求到达服务器, Servlet就会在getSession方法中根据sessionId来查询HttpSession对象.也就是代码中的req.getSession(false);
第二次交互的响应:
只要完成登录之后, 后续请求多次服务器也都会带上刚才的Cookie值(sessionId)的.