二、Cookie 和 Session
1、cookie
HTTP 协议自身是属于 “无状态” 协议
“无状态” 的含义指的是:
- 默认情况下 HTTP 协议的客户端和服务器之间的这次通信,和下次通信之间没有直接的联系
但是实际开发中,我们很多时候是需要知道请求之间的关联关系的
- 例如登陆网站成功后,第二次访问的时候服务器就能知道该请求是否是已经登陆过了
举例:去医院~~
就诊卡,去医院的挂号处,就会先获得到一个就诊卡,就诊卡上就包含了当前患者的关键信息 (就相当于 cookie )
在各个科室都能刷就诊卡,刷就诊卡的时候,就可以通过医院的服务器,来获取到当前患者的一系列信息 (不只是身份信息,还有以往病例之类的)
这个机制最主要的用处就是帮助服务器来识别用的身份~~
就诊卡就是 cookie
医院的数据服务器上就保存着用户的信息,也就是通过 session 的方式来保存的~
此时在服务器这边就需要记录令牌信息,以及令牌对应的用户信息,这个就是 Session 机制所做的工作
理解会话机制 (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 传递
2、HttpServletRequest 类中的相关方法
在 Servlet 中,对于 Cookie 和 Session 都有很好的支持。此处就可以基于这个 API 来完成会话管理的操作~~
Servlet 提供的关键 APl:
方法 | 描述 |
---|---|
HttpSession getSession() | 在服务器中获取会话. 参数如果为 true, 则当不存在会话时新建会话; 参数如果 为 false, 则当不存在会话时返回 null |
Cookie[] getCookies() | 返回一个数组, 包含客户端发送该请求的所有的 Cookie 对象. 会自动把 Cookie 中的格式解析成键值对 |
2.1、HttpSession getSession()
getSession
方法,既能用于获取到服务器上的会话,也能用于创建会话,具体行为,取决于参数
如果参数为 true :会话不存在,则创建,会话存在,则获取
如果参数为 false :会话不存在,则返回 null ,会话存在,则获取
比如你来到了一家医院 (之前没来过这个地方)
先来到挂号处~~ 挂号操作,就要搞一个就诊卡,人家挂号处的医生就问你:
你有没有就诊卡?
- 如果你有,就不必再办了
- 如果你没有,就办一个新的卡 (工本费 20 块)
当你挂号了之后,你不光得到了一个就诊卡,同时在医院的服务器里也给你开了一个档案 (这个就是你的会话)
在调用 getSession 的时候具体要做的事情:
-
创建会话
-
首先先获取到请求中 cookie 里面的
sessionld
字段~~ (相当于会话的身份标识) -
判定这个
sessionld
是否在当前服务器上存在~~ -
如果不存在,则进入创建会话逻辑
创建会话,会创建一个HttpSession
对象,并且生成一个sessionld
(是一个很长的数字,通常是用十六进制来表示,能够保证唯一性~) -
接下来就会把这个 sessionld 作为 key,把这个 HttpSession 对象,作为 value,把这个键值对给保存到服务器内存的一个 “哈希表” 这样的结构中 (实际的实现不一定真是 Hash 表,但是一定是类似的能够存储键值对的结构,并且这个数据是在内存中的!!)
-
再然后,服务器就会返回一个 HTTP 响应,把
sessionld
通过Set-Cookie
字段返回给浏览器
浏览器就可以保存这个 sessionld 到 Cookie 中了
-
-
获取会话
- 先获取到请求中的 cookie 里面的
sessionld
字段 (也就是会话的身份标识),判定这个 sessionld 是否在当前服务器上存在 (也就是在这个哈希表中是否有) - 如果有,就直接查询出这个 HttpSession 对象,并且通过返回值返回回去~
- 先获取到请求中的 cookie 里面的
HttpSession:
- 这个对象本质上也是一个 “键值对” 的结构
- 允许程序猿往
HttpSession
对象中,存储任意的键值对数据 (key 必须是 String,value 是一个 Object)
服务器上有很多键值对,每个键值对是一个 sessionId 和一个 HttpSession 对象。
每个 HttpSession 对象里面又包含了若干的键值对,里面的 key 和 value 都是程序猿自定义的,HttpSession 里面的每个键值对,称为属性 (Attribute)
HttpSession 类中的相关方法:
一个 HttpSession 对象里面包含多个键值对,我们可以往 HttpSession 中存任何我们需要的信息
HttpSession提供了两个方法:
getAttribute 取键值对
setAttribute 存键值对
方法 | 描述 |
---|---|
Object getAttribute(String name) | 该方法返回在该 session 会话中具有指定名称的对象,如果没 有指定名称的对象,则返回 null. |
void setAttribute(String name, Object value) | 该方法使用指定的名称绑定一个对象到该 session 会话 |
boolean isNew() | 判定当前是否是新创建出的会话 |
2.2、Cookie[] getCookies()
获取到请求中的 Cookie 数据
返回值是 Cookie 类型的数组,每个元素是一个 Cookie 对象
每个 Cookie 对象又包含了两个属性,name,value (还是键值对)
HTTР 请求中的 cookie 字段就是按照键值对的方式来组织的
这里的这些键值对,大概的格式,使用 ;
来分割多个键值对,使用 =
来分割键和值,这些键值对都会在请求中通过 cookie 字段传给服务器
服务器收到请求之后,就会进行解析~~ 解析成上述看到的 Cookie[] 这样的形式
方法 | 描述 |
---|---|
String getName() | 该方法返回 cookie 的名称。名称在创建后不能改变。(这个值是 Set Cooke 字段设置给浏览器的) |
String getValue() | 该方法获取与 cookie 关联的值 |
void setValue(String newValue) | 该方法设置与 cookie 关联的值 |
Cookie 这里是可以保存任意自定制的键值对,如果是一般的键值对,直接通过 getCookies 来获取
如果是特殊的键值对 (表示 sessionld 的键值对),不需要使用 getCookies,直接用 getsession 其实就自动帮我们从 Cookie 中取 sessionld 了
3、HttpServletResponse 类中的相关方法
方法 | 描述 |
---|---|
void addCookie(Cookie cookie) | 把指定的 cookie 添加到响应中 |
响应中就可以根据 addCookie 这个方法,来添加一个 Cookie 信息到响应报文中,
这里添加进来的键值对,就会作为 HTTP 响应中的 Set-Cookie 字段来表示~~
4、案例:实现用户登陆
4.1、约定前后端交互接口
两组交互:登录 + 获取主页
有很多种约定的方式,确定好一种即可
登录的交互:
请求:
POST/login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=zhangsan&password=123
响应:
HTTP/1.1302Location: index
获取主页的交互:
请求:
GET /index HTTP/1.1
响应:
HTTP/1.1 200 OK
Content-Type: text/html
[body 中是一个简单的 html 片段,直接由浏览器进行展示]
4.3、登录页面
先编写一个简单的登录页面
使用 form 表单来构造 post 请求
webapp 下创建页面 login.html
<!-- action 的值就是构造请求的路径.
如果值是 /login 表示这是一个绝对路径~~ 直接把 /login 作为请求中的路径了
正确的做法, 应该是在这里使用相对路径. 没有 /,光是一个 login -->
<form action="/login" method="post">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="登录">
</form>
4.4、编写一个 Servlet 处理登录请求
创建 LoginServlet 类
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
if ("zhangsan".equals(username) && "123".equals(password)) {
resp.sendRedirect("index");
} else {
resp.sendRedirect("login failed!");
}
}
初步完成的代码~~ 这个代码看起来好像已经实现了登录功能,但是实际上还差了点意思,在后续跳转到 index 的时候,服务器要能够获取到当前用户的身份信息
当下就需要创建好一个 Session 供后面来使用
并且在 Session 中填写上一些必要的身份信息,以供后面的逻辑来使用~~
@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", "123"
if ("zhangsan".equals(username) && "123".equals(password)) {
// 登陆成功!
// 创建会话,并保存必要的额身份信息
HttpSession httpSession = req.getSession(true);
// 往会话中存储键值对. 必要的身份信息
httpSession.setAttribute("username", username);
resp.sendRedirect("index"); // 重定向 跳转
} else {
// 登陆失败!
resp.sendRedirect("login failed!");
}
}
}
4.5、编写服务器端返回主页的逻辑
创建 IndexServlet 类
@WebServlet("/index")
public class IndexServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 返回一个主页. (主页就是一个简单的 html 片段)
// 此处需要得到用户名是啥, 从 HttpSession 中就能拿到.
// 此处 getSession 的参数必须是 false. 前面在登录过程中, 已经创建过会话了. 此处是要直接获取到之前的会话.
HttpSession session = req.getSession(false);
String username = (String) session.getAttribute("username"); // Object
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write("<h3>欢迎你! " + username + "</h3>");
}
}
4.6、访问登录页面
部署程序,通过 127.0.0.1:8080/hello102/login.html 访问登录页面
第一次交互: 浏览器从服务器上拿到登录页面
- 抓包:200 HTTP 127.0.0.1:8080 hello102login.html
第二次交互: 点击登录之后,就要给服务器发送一个登录请求,服务器会返回响应
- 抓包:302 HTTP 127.0.0.1:8080 hello102login
第三次交互: 浏览器收到 302 响应之后,再次向服务器发起请求,来访问主页
- 抓包:200 HTTP 127.0.0.1:8080 /hello102/index
4.7、记录访问次数
- 会话里,是可以保存任意指定的用户信息的
- 在此处,就可以实现一个简单的功能,记录一下当前用户访问主页的次数,当登录之后,首次访问主页,主页上就显示,当前访问了 1 次
后续再刷新页面,次数就会累加
setAttribute 的第二个参数,是 Object。而 int 不是 Object,但是 Integer 是!!
就会触发 Java 中的自动装箱机制,就自动的把 0 给转成 Integer~
既然存的是 Integer,后续取的时候也就是 Integer
@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", "123"
if ("zhangsan".equals(username) && "123".equals(password)) {
// 登陆成功!
// 创建会话,并保存必要的额身份信息
HttpSession httpSession = req.getSession(true);
// 往会话中存储键值对. 必要的身份信息
httpSession.setAttribute("username", username);
// 初始情况下,登录次数设为 0
httpSession.setAttribute("count", 0);
resp.sendRedirect("index"); // 重定向 跳转
} else {
// 登陆失败!
resp.sendRedirect("login failed!");
}
}
}
@WebServlet("/index")
public class IndexServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 返回一个主页. (主页就是一个简单的 html 片段)
// 此处需要得到用户名是啥, 从 HttpSession 中就能拿到.
// 此处 getSession 的参数必须是 false. 前面在登录过程中, 已经创建过会话了. 此处是要直接获取到之前的会话.
HttpSession session = req.getSession(false);
String username = (String) session.getAttribute("username"); // Object
// 再从会话中取出 count
Integer count = (Integer) session.getAttribute("count");
count += 1;
// 自增之后 写回会话中
session.setAttribute("count", count);
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write("<h3>欢迎你! " + username + " [这是第 " + count + " 次访问]" + "</h3>");
}
}
重新启动,结果:欢迎你! zhangsan [这是第 1 此访问]
三、上传文件
1、核心方法
上传文件也是日常开发中的一类常见需求,在 Servlet 中也进行了支持
HttpServletRequest 类方法:
方法 | 描述 |
---|---|
Part getPart(String name) | 获取请求中给定 name 的文件 |
Collection<Part> getParts() | 获取所有的文件 |
上传文件的时候,在前端需要用到 form 表单,form 表单中需要使用特殊的类型 form-data
此时提交文件的时候,浏览器就会把文件内容以 form-data 的格式,构造到 HTTP 请求中,服务器就可以通过 getPart 来获取了~~
一个 HTTP 请求,可以一次性的提交多个文件的
每个文件都称为一个 Part ~
每个 Part 都有一个 name (身份标识)
服务器代码中就可以根据 name 找到对应的 Part
基于这个 Part 就可以进一步的获取到文件信息,并进行下一阶段操作~~
Part 类方法:
方法 | 描述 |
---|---|
String getSubmittedFileName() | 获取提交的文件名 |
String getContentType() | 获取提交的文件类型 |
long getSize() | 获取文件的大小 |
void write(String path) | 把提交的文件数据写入磁盘文件 |
2、案例:通过网页提交图片到服务器
2.1、登录页面
创建 upload.html 到 webapp 目录下
<form action="upload" method="post" enctype="multipart/form-data">
<!-- 页面上会有一个 "选择按钮" 选择本地的文件 -->
<input type="file" name="MyImage">
<input type="submit" value="提交">
</form>
2.2、写一个 Servlet 处理上传请求
@MultipartConfig
要给这个类加上这个注解,来开启对于上传文件的支持
否则 getPart 调用的时候就会抛出异常!
@MultipartConfig
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Part part = req.getPart("MyImage");
System.out.println(part.getSubmittedFileName());
System.out.println(part.getContentType());
System.out.println(part.getSize());
part.write("f:/test/picture.jpg");
resp.setContentType("text/htm; charset=utf8");
resp.getWriter().write("上传成功!");
}
}
访问:127.0.0.1:8080/hello102/upload.html
抓包:
boundary
随机生成的字符串,作为文件的边界的
这个表示 body 部分,从哪里开始是文件内容到哪里文件内容结束~~
在这个 boundary 中间的部分
就是图片的二进制数据 (二进制的,看起来在记事本中是乱码)
实现一个网站有两种典型的风格:
-
服务器渲染
-
客户端渲染 (前后端分离)
所谓的 渲染(render) 就是把一个动态的页面给构造出来,页面的基本结构+里面的数据~
根据用户不同的输入,得到不同的数据,将这些数据和页面基本结构结合,得到完整的页面
上述所描述的 “渲染过程",可以在客户端来做 (浏览器中,通过 JS 来完成),也可以在服务器来做 (Java 代码中完成
在 2022 年的今天,其实这种客户端渲染这种风格是比较主流的风格了~~
20 年前:
- 当时咱们做网站 (网站都相对比较简单) 那个时候网页结构和一些业务逻辑,都是糅合在一起的~~
- 当时做网站,最主流的技术,PHP / JSP / ASP
随着网站变的越来越复杂,业务逻辑也复杂了,页面结构也复杂了,再揉在一起,就非常不利于维护
10 年前:
- 模板引擎这样的技术就兴起了,把界面和逻辑就分离开了~~
- 使用单独的模板文件,来表示界面,其中的一些需要产生变化的数据,通过一些特殊的符号进行占位,然后在服务器代码中,计算得到数据之后,再把数据替换到占位符上,从而得到完整的页面~~
Thymeleaf
这个就是 Java 中比较流行的一个模板引擎
现在:
- 随着时间再进一步发展,前端页面变的越来越复杂,越来越多的逻辑已经直接在前端上进行处理了~~
前端不再是简单的写写页面和样式了,而是要处理很多的业务逻辑了~~ 逐渐变成了一个复杂的 “工程” 问题~~
使用模板引擎这种方式开发,意味着后端开发工程师写代码的时候不得不去接触到很多前端的东西… (需要把自己后端生成的数据嵌入到页面中,势必要对这个前端的内容进行一定的理解) - 引入了前后端分离开发方式,就把前端工程师和后端工程师彻底分离开了~~
- 让后端只是给前端返回数据 (数据往往就是以 json 格式来组织了),后端这边彻底不必关心页面的结构和样式了~~
只要说开始的时候前端和后端各自约定好前后端交互接口,然后再分别开发就行了~~
- 让后端只是给前端返回数据 (数据往往就是以 json 格式来组织了),后端这边彻底不必关心页面的结构和样式了~~
- 前后端分离带来的好处,除了能够各自独立的进行开发,还能各自独立的进行测试
- 前端写完了代码,可以自己创建一个假的服务器 (mock server),来吐出一些构造好的假数据,来验证前端写的内容正确性
- 后端写完代码了,也可以自己构造一些 HTTP 请求,来验证服务器吐出的数据对不对 (Postman)