HTTP是一种无状态协议,每一个HTTP请求都是相互独立的,当前请求不会记录上一次请求,即每次服务端接收到客户端的请求都是一个全新的请求,因此服务器单从网络连接上无法知道客户端的身份。
在程序中,一个用户的所有请求操作都应该属于同一个会话,另一个用户的所有请求应该属于另一个会话,会话追踪很重要!!!例如: 用户A在网上购物时购买的所有商品都应该放在用户A的购物车中,不论是什么时候购买的,这都属于同一个会话,不能将商品放入用户B的购物车内。
而Web应用程序是使用HTTP协议传输数据的,HTTP协议是无状态的协议。一旦数据交换完毕,客户端与服务器端的连接就会关闭,再次交换数据需要建立新的连接, 这就意味着服务器无法从连接上跟踪会话。例如: 用户A将一件商品放入购物车后,用户再次打开购物网站进行购物,此时服务器无法判断该购买行为属于哪一个用户的会话,因此我们需要跟踪会话。
Session 和 Cookie 能够 弥补HTTP无状态的不足并且跟踪会话。
Cookie技术是客户端的解决方案, Cookie就是服务器发送给客户端的特殊信息,这些特殊信息以文本文件的形式存放在客户端,客户端向服务器发送请求时会带上这些特殊信息。相当于:服务器给客户端颁发了一个 “通行证”,每个客户端一个,无论哪一个客户端访问服务器都必须携带上自己的 “通行证”;这样服务器就能够通过这个 “通行证” 来确认客户端的身份了。
有了Cookie技术后,服务器接收到来自客户端的请求后,能够通过分析请求头中的Cookie得到客户端特有的信息,从而动态的生成与该客户端相对应的内容。
Cookie的不可跨域名性:Cookie可以实现同域名下的跨页面,但不能跨域名;同一个网站中的所有页面共享一套Cookie,可以设置有效期限。
Cookie的存储数据量有限:Cookie存储空间为4KB左右,因此Cookie只能存储一些小数据量的数据。
Cookie的生存时间:
- 整个会话期间:浏览器会将Cookie保存在内存中,浏览器关闭时就自动清除这个Cookie。
- 持久化到客户端硬盘:浏览器关闭Cookie也不会被清除,下次打开浏览器访问该网站时,Cookie会再次自动发送到服务器。
HTTP的两个头部负责 设置及发送Cookie,分别是 Set-Cookie 和 Cookie。
当服务器返回给客户端一个HTTP响应时,如果包含 Set-Cookie头部,就会指示客户端建立一个Cookie并且在后续的HTTP请求中自动发送这个Cookie头部到服务器端,直到这个Cookie过期。
① 客户端发送一个HTTP请求到服务器(没有Cookie信息);
② 服务器发送一个HTTP响应到客户端,包含 Set-Cookie: header(服务端生成Cookie信息);
③ 客户端发送一个HTTP请求到服务器,包含 Cookie: header(自动发送客户端保存的Cookie信息);
④ 服务器根据Cookie信息动态的发送一个HTTP响应到客户端。
在默认情况下,浏览器将Cookie对象保存在内存中,只要浏览器关闭,Cookie对象就会被销毁。
在手动设置的情况下,可以要求浏览器将接收到的Cookie保存在客户端计算机的硬盘上,同时需要指定Cookie在硬盘上的存活时间。
// 以秒为单位设置 cookie 的最长期限
// 正 - cookie经过多少秒后过期;负 - cookie不会被永久存储,退出浏览器时被删除;0 - 删除cookie
public void setMaxAge(int expiry) {
maxAge = expiry;
}
一个Cookie对象只能存放一个键值对,这个键值对中存储中的数据都是String类型,并且name属性不能为中文。Cookie类中定义了很多属性和很多操作属性的方法,我们通过一个案例来熟悉。
private String name; // 键
private String value; // 值
private String comment; // 注释,用于描述Cookie的用途
private String domain; // 可访问Cookie的域名
private int maxAge = -1; // Cookie的有效时间
private String path; // 可访问Cookie的路径;将path设为/,Cookie可以被网站下所有页面访问。
private boolean secure; // true - https;false - http;默认为false。
private int version = 0; // Cookie的版本
案例:使用Cookie实现 记录上次的访问时间
public class CookieUtil {
public static Cookie getCookieByName(String name, Cookie[] cookies){
if (cookies != null){
for(Cookie cookie : cookies){
if (name.equals(cookie.getName())){
return cookie;
}
}
}
return null;
}
}
@WebServlet("/cookieTest")
public class CookieController extends HttpServlet{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html; charset=UTF-8");
PrintWriter out = response.getWriter();
// 获取Cookie
Cookie[] cookies = request.getCookies();
// 通过CookieUtil获取指定名称的Cookie
Cookie cookie = CookieUtil.getCookieByName("record", cookies);
// 判断cookie是否为null
if (cookie == null){
out.print("第一次访问!");
}else {
long lastView = Long.parseLong(cookie.getValue());
out.print("上次访问时间为:" + new Date(lastView).toLocaleString());
}
// 记录当前访问时间并存入Cookie,注意:Cookie的键值必须为String类型
Cookie c = new Cookie("record", System.currentTimeMillis() + "");
// 设置Cookie的有效时间
c.setMaxAge(60);
// 将Cookie写入响应头,推回到客户端
response.addCookie(c);
}
}
Session技术是服务端的解决方案, Session又称会话,是指:客户端浏览器访问服务器的时候,服务器将客户端的信息以某种形式记录在服务器上;客户端浏览器再次访问时,服务器只需要从这些记录中就能查找到该客户端的状态了。
如果说 Cookie机制是通过检查客户身上的“通行证” 来确定客户身份的话,那么 Session机制就是通过检查服务器上的“客户明细表” 来确认客户身份。Session相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了。在Web容器中有一个session列表,其中维护了大量的session对象。
一方面,可以将 客户端浏览器与服务器之间一系列交互的动作 称为一个 Session(会话);另一方面,Session指的是 服务器端为客户端所开辟的存储空间,该空间用于保存客户端状态信息。
Session保存在服务端,为获得更高的存取速度,服务器一般会将Session存放在内存中,且每个用户都有一个独立的Session;如果Session内容过于复杂,当大量客户访问服务器的时候可能会导致内存溢出。因此,Session中保存的信息应该尽量精简。
getSession()
方法创建 HttpSession 对象,同时会为该Session生成一个唯一的session id,这个id在随后的请求中用于重新获得已创建的Session;getSession(true)
方法获取到在服务器中 jsessionid对应的Session对象;有就使用,没有就创建。注意:getSession(boolean create)
方法中create值的分析
getSession()
和 getSession(true)
:获取session对象,如果没有session对象,就新建一个session对象。
getSession(false)
:获取session对象,如果没有session对象,则返回null。
URL地址重写(顺带一提)
当客户端不支持Cookie时,我们还想使用Session机制的话,解决方案就是 URL地址重写。
URL地址重写的原理是:在访问资源路径后、请求参数前,添加用户的 Session id,即 “;jsessionid=XXX”,其中XXX为Session id的值;服务器能够解析重写后的URL获取 Session id,这样即使客户端不支持Cookie,也可以使用Session来记录用户状态。
假设访问:/web/urlServlet?name=mike&age=22
URL地址重写后的访问路径:/web/urlServlet;jsessionid=0CCD096E7F8D97B0BE608AFDC3E1931E?name=mike&age=22
为什么要有这个Session有效时间/失效时间/超时时间?
- 浏览器关闭后,Session不会消失;除非程序通知服务器删除Session,否则服务器会一直保留。浏览器不会主动在其关闭时通知服务器它即将关闭,因此服务器根本不会知道服务器已经关闭了。而浏览器关闭后,保存Session id的Cookie会消失,再次连接服务器后也无法找到原来的Session;这时我们需要通过某些手段将原来的Session id发送给服务器,才能找到原来的Session。(将Cookie持久化到硬盘上,或者改写HTTP请求头,将原来的Session id发送给服务器)
- Session生成后,用户访问一次,服务器就会更新一次Session的最后访问时间,并维护该Session。用户每访问一次Session,服务器都认为该用户的Session"活跃(active)"了一次。越来越多的用户访问服务器,随之Session也会越来越多。
- 正是因为浏览器关闭不会导致Session被删除 和 为防止内存溢出,服务器会把长时间内没有活跃的Session从内存删除。这个时间就是Session的有效时间。如果超过了有效时间没访问过服务器,Session就自动失效了。
Session对象默认30分钟销毁。
Session的有效时间为
maxInactiveInterval
属性,可以通过getMaxInactiveInterval()
方法获取,通过setMaxInactiveInterval(longinterval)
修改。
HttpSession session = request.getSession();
int maxInactiveInterval = session.getMaxInactiveInterval();
session.setMaxInactiveInterval(60*60);
// 单位是秒
Session的超时时间也可以在web.xml中修改。
<session-config>
<session-timeout>60session-timeout>
session-config>
另外,通过调用Session的invalidate()方法可以使Session失效。
session.invalidate();
四大共享数据方式:
三大域对象: ServletContext(应用作用域)、HttpSession(会话作用域)、HttpServletRequest(请求作用域)
数量: 一个用户可拥有多个request对象;一个用户只有1个session对象;所有用户共用一个 application对象。
范围排序:
使用原则: 由小到大尝试,优先使用范围小的。
public interface HttpSession {
public String getId(); // 获取SessionID
public void setMaxInactiveInterval(int interval); // 设置Session的有效时间,单位为秒
public int getMaxInactiveInterval(); // 获取Sessison的有效时间
public void invalidate(); // 设置Session立即失效
public void setAttribute(String name, Object value); // 以键值对的形式用session保存数据
public Object getAttribute(String name); // 通过name获取数据
public void removeAttribute(String name); // 通过name删除数据
}
案例:利用Session实现免登录
LoginServlet
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取请求参数
String username = request.getParameter("username");
String password = request.getParameter("password");
if ("Sun".equals(username) && "123".equals(password)){
// 如果账号密码正确,跳转到 success.jsp,并将提示消息存入session会话域
// 即使关闭浏览器再打开,直接访问 /web/success.jsp 也能访问到,实现了免登录
HttpSession session = request.getSession();
session.setAttribute("msg1", "登录成功!");
response.sendRedirect("/web/success.jsp");
}else {
request.setAttribute("msg2", "密码或账号不正确~");
request.getRequestDispatcher("index.jsp").forward(request, response);
}
}
}
login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>logintitle>head>
<body>
${msg2}<br>
<form action="/web/login">
username: <input type="text" name="username"><br>
password: <input type="password" name="password"><br>
<input type="submit" value="log in">
form>
body>
html>
success.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>logged intitle>head>
<body>
${msg2}
body>
html>
HttpSession | Cookie | |
---|---|---|
存储位置 | 存放在服务器的 Session列表(服务器的内存中) | 存放在客户端浏览器缓存中 或 客户端计算机的硬盘中 |
数据类型 | Session 对象可以存储任意类型的数据Object | Cookie对象存储的共享数据类型只能是String |
数据数量 | Session 对象使用Map集合存储共享数据,可存储任意数量的共享数据 | 一个Cookie对象只能存储一个共享数据 |
存储容量 | Session没有大小限制,和服务器的内存大小相关 | 单个Cookie保存的数据不能超过4K, 很多浏览器限制一个站点最多保存20个Cookie |
安全性 | Session更加安全(保存用户重要的信息) | Cookie的信息是可见的且易于本地编辑的,容易引起安全问题 (保存用户不重要的信息) |
销毁时机 | 浏览器关闭后Session不会被销毁;除非程序通知服务器删除Session 或者 Session的有效时间结束 | 存放在客户端浏览器缓存中:关闭浏览器时销毁 存放在客户端计算机硬盘中:不会被销毁直到Cookie的有效时间结束 |
还是一样的问题,我们为什么需要Token?不是已经有了 Cookie 和 Session吗?因为在真实的开发环境中有这样的两种情景:单机Tomcat应用 和 分布式应用;在单机Tomcat应用中Session自然是够用了,但是在分布式应用中存在一个 Session共享问题!
分布式应用中的Session共享
Token,即令牌,也可以理解为暗号;即在数据传输前需要对暗号,不同的暗号被授予不同的数据操作;使用Token的身份验证方法,服务器不再需要存储用户的登录记录。
1. 用户使用账号和密码请求登录;
2. 服务器收到请求,通过对比数据库中的信息验证用户名和密码;
3. 验证成功后签发一个Token,并将这个Token返回给客户端;
4. 客户端收到这个Token后将其存储起来,比如存到Cookie、LocalStorage或SessionStorage中;
5. 客户端每次向服务器请求资源时都带上这个Token(请求头中);
6. 服务器接收到请求,获取请求头中的Token,验证其有效性;
7. 验证通过后,解析 Token 获取用户信息,根据用户信息进行其他逻辑操作,例如:根据用户权限允许其访问相关资源;
8. 将客户端访问的目标资源响应给客户端。
- Token可以存在数据库中,但是可能查询Token的时间过长导致Token丢失;
- 为了避免查询实践过程,可以将Token放在内存中(用户量即使是百万级的也占不了多少内存)。
JWT 就是上述流程当中Token的一种具体实现方式,其全称是 Json Web Token;就是通过一定规范生成Token,然后通过解密算法逆向解密Token,获取用户信息。
通俗地说,JWT的本质就是一个字符串,它将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT Token,这个JWT Token带有签名信息,接收后可以校验是否被篡改,可以用于在各方之间安全地将信息作为Json对象传输。
首先,前端通过表单将用户名和密码发送到后端的接口;
后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token;
后端将JWT Token字符串作为登录成功的结果返回给前端;
前端在每次请求时将JWT Token放入HTTP请求头中的 Authorization属性中;
后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、Token的接收方是否是自己等;
验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果;