详细介绍了Java Web中的Cookie技术的原理以及常见用法。
会话可以简单的理解为客户端与服务器之间的一次会晤,在一次会晤中可能会包含多次请求和响应。例如,用户在某个浏览器中访问某一个Web应用,只要不关闭该浏览器,不管该用户点击该Web应用的多少个超链接,访问多少资源,直到关闭浏览器之前,整个的这个访问过程我们称为一次会话。
在Java Web中,默认下,客户向某一服务器(Web应用)发出第一个请求开始,会话就开始了,直到客户关闭了浏览器,会话结束。
在一个会话的多个请求中共享数据,这就是会话跟踪技术。会话跟踪技术可以解决我们很多很多问题。比如最常见的就是一个用户在登陆一个网站之后,请求该网站的其他页面和资源时免登录的功能,又比如购物网站中常见的显示曾经浏览过的商品的功能,又比如某些购物网站允许未登录用户可以添加商品到临时购物车中的功能。
常见的会话跟踪技术有Cookie和Session,Cookie是先出现的。
HTTP 是一种不保存状态的协议,即无状态(stateless)协议。HTTP 协议自身不对请求和响应之间的通信状态进行保存。也就是说在 HTTP 这个级别,协议对于发送过的请求或响应都不做持久化处理。
使用 HTTP 协议,每当有新的请求发送时,就会有对应的新响应产生。协议本身并不保留之前一切的请求或响应报文的信息。这是为了更快地处理大量事务,确保协议的可伸缩性,而特意把 HTTP 协议设计成如此简单的。
可是,随着 Web 的不断发展,因无状态而导致业务处理变得棘手的情况增多了。比如,用户登录到一家购物网站,即使他跳转到该站的其他页面后,也需要能继续保持登录状态。针对这个实例,网站为了能够掌握是谁送出的请求,需要保存用户的状态,也就是会话跟踪。
HTTP/1.1 虽然同样是无状态协议,但为了实现期望的保持状态功能,于是引入了 Cookie 技术。Cookie 技术通过在请求和响应报文中写入 Cookie 信息来控制客户端的状态。
简单的说:Cookie是由服务器创建,然后通过响应(Set-Cookie 的首部字段)发送给客户端的一个键值对,这个键值对中包含了要该会话想要记住的状态信息,客户端会保存这个Cookie,并会标注出Cookie的来源(哪个网站的Cookie)。当客户端再一次向该网站发出请求时会把所有这个网站的Cookie包含在请求报文头中发送给服务器,这样服务器就可以根据该请求携带的Cookie信息识别客户端,并且得到之前的状态信息了!
有了 Cookie 再用 HTTP 协议通信,就可以在一定程度上管理会话状态了。
第一次,没有 Cookie 信息状态下的请求:
第 2 次开始,存有 Cookie 信息状态的请求:
Cookie是通过HTTP请求和响应首部字段在客户端和服务器端传递的:
在Java EE的Servlet规范中,可以非常简单的设置和获取Cookie,Cookie被抽象成为一个Cookie类。
当要设置Cookie的时候,需要创建Cookie对象并且设置key和value,随后通过response响应对象的addCookie方法添加这个Cookie即可,Web服务器会自动将response中的Cookie发送给客户端。
客户端在随后对该Web应用的其他资源进行访问的时候,将会自动带上服务器发送的Cookie(同一个服务器的其他Web应用也不会发送)。
如下案例,一个Web应用“cookie”中有两个Servlet,我们尝试在访问“/cookie-servlet”资源时向浏览器发送一个“id”的Cookie,值为一个随机的UUID字符串:
@WebServlet("/cookie-servlet")
public class CookieServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
//生成一个随机字符串
String id = UUID.randomUUID().toString();
//创建Cookie对象,指定名字和值。Cookie类只有这一个构造器
Cookie cookie = new Cookie("id", id);
//在响应中添加Cookie对象
resp.addCookie(cookie);
resp.getWriter().print("已经给你发送了ID");
}
}
在访问“/hello-servlet”资源时,用于测试Cookie,该Servlet中通过request请求对象的getCookies方法获取Cookie,并且尝试输出此前颁发的id(如果存在):
@WebServlet("/hello-servlet")
public class HelloServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
Cookie[] cookies = request.getCookies();
//如果请求中存在Cookie
if (cookies != null) {
//遍历所有Cookie
for (Cookie c : cookies) {
//获取Cookie名字,如果Cookie名字是id
if ("id".equals(c.getName())) {
//打印Cookie值
response.getWriter().print("您的ID是:" + c.getValue());
}
}
}
}
}
我们首先访问/hello-servlet
,结果并没有数据id信息,也没有Cookie相关的字段(这里我们使用的Google浏览器):
当我们再次访问/cookie-servlet
时,服务器会返回一个Set-Cookie响应头:
随后再次访问/hello-servlet
,结果输出了id信息,并且在请求头中具有了Cookie字段,并且就是刚才服务器发送的key和value:
如果服务器端多次发送重复key的Cookie,那么后发送的Cookie会覆盖原有的Cookie,例如客户端的第一个请求服务器端发送的Cookie是:Set-Cookie: a=A;第二请求服务器端发送的是:Set-Cookie: a=AA,那么客户端只留下一个Cookie,即:a=AA。
如果服务器端一次发送多个重复key的cookie,那么当客户端再次请求的时候,只携带最后一个设置的Cookie的值。
我们再一次请求/cookie-servlet
,可以看到颁发了不同的id,为同一个key设置为不同的值:
我们再一次请求/hello-servlet
,可以看到这一次请求头中的Cookie也跟着变化了,这就是Cookie的覆盖:
所谓Cookie的有效期就是Cookie在客户端的最大有效时间,我们可以通过setMaxAge(int)方法来设置Cookie的有效时间,单位秒。
cookie.setMaxAge(-1)
:cookie的maxAge属性的默认值就是-1,表示只在浏览器内存中存活。一旦关闭浏览器窗口,那么cookie就会消失。生命值为负数的Cookie也被称为内存Cookie。cookie.setMaxAge(60*60)
:表示cookie对象可存活1小时。当生命大于0时,浏览器会把Cookie保存到硬盘上,就算关闭浏览器,就算重启客户端电脑,cookie也会存活1小时,除非将Cookie从硬盘上手动清理了。生命值为正数的Cookie也被称为硬盘Cookie。cookie.setMaxAge(0)
:cookie生命等于0是一个特殊的值,它表示cookie被作废!也就是说,如果原来浏览器已经保存了这个Cookie,那么可以通过Cookie的setMaxAge(0)来要求浏览器删除这个Cookie。无论是在浏览器内存中,还是在客户端硬盘上都会删除这个Cookie。Cookie并没有提供直接的修改和删除功能,但是根据前面Cookie的覆盖和有效期属性,可以间接的修改和删除Cookie。
当想要修改Cookie时,可以发送同名(key)的Cookie,这样以前的旧Cookie就被覆盖了。当想要删除Cookie时,同样设置同名(key)的Cookie的maxAge = 0即可。
现在有Web应用A,向客户端发送了10个Cookie,那么客户端无论访问应用A的哪个资源都会把这10个Cookie包含在请求中!但是也许只有AServlet需要读取请求中的Cookie,而其他Servlet根本就不会获取请求中的Cookie。这说明客户端浏览器有时发送这些Cookie是多余的!
可以通过设置Cookie的path(路径)来指定浏览器在访问什么样的路径时包含什么样的Cookie。
下面是客户端浏览器保存的3个Cookie以及它们的路径:
下面是三个请求以及它们的URL:
那么:
也就是说,请求路径如果包含了Cookie路径,那么会在请求中包含这个Cookie,否则不会请求中不会包含这个Cookie。
设置Cookie的路径需要使用setPath()方法,例如:cookie.setPath("/cookietest/servlet")。如果setPath("/"),那么表示所有URL都会携带。
如果没有主动设置Cookie的路径,那么该Cookie路径的默认值当前访问资源所在路径,例如:
我们将上面案例的cookie-servlet
路径设置为/a/cookie-servlet
。请求/a/cookie-servlet
,可以看到颁发了cookie:
根据上面的路径规则,我们知道,这里设置的Cookie路径为“/cookie/a”,它兼容同样包含该路径的请求URL。
我们再次访问/hello-servlet
的,发现在请求头中并没有刚才颁发的Cookie这就是Cookie路径和URL不兼容的情况,该Cookie不会被携带:
默认情况下,不同的域名中设置的Cookie不可以共享,即跨域问题!
cookie有一个setDomain方法,可以设置Cookie的domain属性(域名),表示Cookie在域名级别中的作用范围。如果不设置,那么默认范文就是当前域名。
如何让具有相同domain后缀的多个不同子域名都共享主域名的Cookie的呢?比如有如下域名,http://www.baidu.com、http://zhidao.baidu.com、http://news.baidu.com、http://tieba.baidu.com,现在希望在访问它们的不同域名的资源时都加上某个Cookie!
在tomcat 8.x之前,我们可以通过在setDomain方法中设置的域名的第一个字符为“.”来设置多级域名共享。
很简单,需要下面两步:
在tomcat 8.x及其之后,如果domain方法参数前面带有“.”,那么将抛出异常:
因为tomcat8.x之后默认采用Rfc6265CookieProcessor作为默认Cookie处理器,它有如下域属性规定:
怎么解决呢?也很简单!修改配置文件context.xml,指定CookieProcessor为 org.apache.tomcat.util.http.LegacyCookieProcessor,这是此前老版本tomcat的默认Cookie处理器:
<CookieProcessor className="org.apache.tomcat.util.http.LegacyCookieProcessor" />
即:
Cookie的name和value都不能使用中文,因为中文属于Unicode字符,英文数据ASCII字符,中文占4个字符或者3个字符,英文占2个字符。
如果希望在Cookie中使用中文,那么需要先对中文进行URL编码,然后把编码后的字符串放到Cookie中。在获取Cookie时,先将编码的数据进行URL解码。
向客户端响应中添加Cookie:
String name = URLEncoder.encode("姓名", "UTF-8");
String value = URLEncoder.encode("张三", "UTF-8");
Cookie c = new Cookie(name, value);
c.setMaxAge(3600);
response.addCookie(c);
从客户端请求中获取Cookie
response.setContentType("text/html;charset=utf-8");
Cookie[] cs = request.getCookies();
if(cs != null) {
for(Cookie c : cs) {
String name = URLDecoder.decode(c.getName(), "UTF-8");
String value = URLDecoder.decode(c.getValue(), "UTF-8");
String s = name + ": " + value + "
";
response.getWriter().print(s);
}
}
简单的使用Cookie在内存中实现显示上次访问时间的功能,如果需要长期保存,并且需要在各个地方都能展示,那么还是需要借助数据库来保存。
@WebServlet("/cookie-time")
public class CookieTime extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html;charset=utf-8");
//创建Cookie,名为lastTime,值为当前时间,添加到response中
Cookie cookie = new Cookie("lastTime", Long.toString(System.currentTimeMillis()));
cookie.setMaxAge(60 * 60);
response.addCookie(cookie);
/*
* 获取lastTime的Cookie
* 如果不存在则输出“您是第一次访问本站”,如果存在输出“您上一次访问本站的时间是xxx”;
*/
Cookie[] cs = request.getCookies();
String s = "您是首次访问本站!";
if (cs != null) {
for (Cookie c : cs) {
if ("lastTime".equals(c.getName())) {
//格式化时间戳毫秒
s = "您上次的访问时间是:" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss SSS")
.format(Instant.ofEpochMilli(Long.parseLong(c.getValue())).atZone(ZoneId.systemDefault()));
}
}
}
response.getWriter().print(s);
}
}
第一次运行:
后续运行:
简单的使用Cookie在内存中实现显示曾经浏览过的商品的功能,如果需要长期保存,并且需要在各个地方都能展示,那么还是需要借助数据库来保存。
@WebServlet("/products-servlet")
public class ProductsServlet extends HttpServlet {
/**
* 商品名和链接的简单映射
*/
static HashMap<String, String> productsMap;
static String productsUrl;
static {
productsMap = new HashMap<>();
productsMap.put("ThinkPad", "ThinkPad
");
productsMap.put("Lenovo", "Lenovo
");
productsMap.put("Apple", "Apple
");
productsMap.put("HP", "HP
");
productsMap.put("SONY", "SONY
");
productsMap.put("ACER", "ACER
");
productsMap.put("DELL", "DELL
");
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html;charset=utf-8");
String productName = request.getParameter("name");
String products = getCookValue(request, "products");
String[] oldProducts = null;
if (products != null) {
oldProducts = products.split("_");
}
if (productName != null) {
if (products != null) {
LinkedList<String> strings = new LinkedList<>(Arrays.asList(oldProducts));
//更新Cookie
if (!strings.contains(productName)) {
strings.addFirst(productName);
//最多展示最近浏览的三个商品
if (strings.size() == 4) {
strings.removeLast();
}
} else {
//更新Cookie
strings.remove(productName);
strings.addFirst(productName);
}
products = String.join("_", strings);
} else {
products = productName;
}
}
//更新Cookie
if (products != null) {
Cookie cookie = new Cookie("products", products);
cookie.setMaxAge(60 * 60 * 24);
response.addCookie(cookie);
}
/*
* 输出信息
*/
PrintWriter writer = response.getWriter();
writer.println("商品列表:
");
//输出商品列表
writer.println(getproductsUrl());
writer.println("
您此前浏览过的商品:
");
if (oldProducts != null) {
for (String oldProduct : oldProducts) {
//输出Cooke中的最近浏览的商品
writer.println(productsMap.get(oldProduct));
}
}
}
/**
* 商品列表
*/
private String getproductsUrl() {
if (productsUrl != null) {
return productsUrl;
}
StringBuilder stringBuilder = new StringBuilder();
for (String value : productsMap.values()) {
stringBuilder.append(value);
}
productsUrl = stringBuilder.toString();
return productsUrl;
}
/**
* 获取Cookie中指定key的值
*/
private String getCookValue(HttpServletRequest request, String name) {
Cookie[] cs = request.getCookies();
if (cs == null) {
return null;
}
for (Cookie c : cs) {
if (name.equals(c.getName())) {
return c.getValue();
}
}
return null;
}
}
参考资料:
如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!