HTTP协议–cookie
1.我们为什么要使用cookie?
我们做一个实验:(这个例子比较简单,在局域网下实现,大家了解即可)
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 java.io.IOException;
import java.util.Enumeration;
@WebServlet("/status")
public class HttpStatusServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
String requestURI = req.getRequestURI();
String protocol = req.getProtocol();
String remoteAddr = req.getRemoteAddr();
int remotePort = req.getRemotePort();
System.out.println("位于" + remoteAddr + "的客户端使用了端口号" + remotePort + "访问了当前的服务器,对应的请求报文为");
System.out.println(method + " " + requestURI + " " + protocol);
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()){
String key = headerNames.nextElement();
String value = req.getHeader(key);
System.out.println(key + ":" + value);
}
}
}
我们通过抓包打印日志发现,不同客户端访问服务器发送的报文基本上没什么区别。那这样的话,按理说服务器应该是不能区分不同客户端发送过来的报文。但是实际上,我们在京东或者其他网站购买东西的时候,当我们将购物车页面的地址给你的小伙伴,让他访问,发现他看不到商品,京东会提示他需要进行登录。
所以我们得到结论:服务器是可以识别出不同的客户端的,根据不同的客户端返回不同的数据
由此我们就可以引出会话技术
2.cookie
会话技术就是为了解决HTTP协议的无状态性,会话技术一共可以分为两大类,一类是客户端技术,一类是服务请技术。这决定了数据存储在客户端还是服务器。我们今天先来介绍客户端技术的典型代表cookie
cookie的本质就是在HTTP请求报文和HTTP响应报文中分别引用了Cookie请求头和set-Cookie响应头。利用头信息来传递状态信息
cookie以字符串的形式保存在浏览器中。常见的几种属性:
key=value;
expires=失效时间;
path=路径;
domain= 域名;
8secure;(secure表安全级别)
raw: 默认值:false。 默认情况下,读取和写入 cookie 的时候自动进行编码和解码(使用encodeURIComponent 编码, decodeURIComponent 解码)
setMaxAge:设置存活时间
2.1设置cookie的存活时间
场景:用户登录商城,将商品加入购物车然后查看购物车。
用户登录时,客户端将用户的信息发送到服务器,服务器在数据库中对该用户的用户名和密码进行比对,比对通过后服务器使用能够唯一标识该用户的数据用户名(这里拿用户名举例),放进HTTP的set-Cookie响应头中返回给客户端。当客户端执行商品加入购物车操作之后然后去查看购物车,此时页面需要跳转访问,此时客户端携带cookie信息向服务器发送访问购物车的请求,服务器根据cooke信息来判断是哪个用户要访问购物车,以此来返回正确的购物车信息。
在此场景下,我们希望登录状态能够持续久一点,在一定时间内,我们再去查看我们的购物车的时候仍然可以看到,不希望看到反复的登录页面。这就可以设置cookie的存活时间来解决。如果cookie没有设置存活时间,表示是存活在浏览器对应的内存区域当中,关闭浏览器cooki就会失效,如果希望cookie能够持久化保存,则可以设置一个正数存活时间,表示在硬盘上存活多少秒。当然另外一个场景就不需要我们持久的保持登录状态比如银行app
setMaxAge:设置正数表示硬盘存活多少秒,设置负数表示存在于浏览器内存中,设置0表示删除cookie
2.2设置路径
如果没有设置路径,那么仅当访问当前路径以及当前路径的子路径时才会携带cookie
举个例子:
比如当前的servlet地址是 /a/b/c/servlet1
那么当访问 /a/b/c/servlet2时会携带cookie
/a/b/c/servlet1时会携带cookie
/a/b/c/e…同理
当访问 /a/b/servlet3时不会携带cookie
应用场景:设置访问html页面时携带cookie,访问js、css等资源文件时不携带cookie
2.3设置域名
浏览器不允许设置和当前域名无关的域名的cookie
比如当前的代码运行在localhost域名下,但是希望设置一个jd.com域名的cookie,浏览器会屏蔽该行为。如果希望设置某个域名的cookie,就必须在相关域名之下设置才可以。
在这里介绍的设置域名是指如果设置了一个父域名的cookie,那么当访问相关的子域名网页时,会自动携带cookie
举个例子:
设置了一个cookie的域名为jd.com,当访问search.jd.com、passport.jd.com时浏览器会默认携带cookie
jd.com ------父域名
search.jd.com
passport.jd.com-------子域名
2.4cookie的优缺点
优点:小巧、轻便、存储在客户端,减轻服务器的压力;父子域名之间可以共享
缺点:只可以存储字符类型;只可以存储一些非敏感数据;大小有限制
3.session
服务器技术的典型代表有session、HttpSession
我们今天来介绍一下session的原理:在某些场景下,服务器会给当前的客户端在服务器上开辟一块内存,也可以理解为对象,该对象和对应的客户端做到一一绑定。之后只要是该客户端发送过来的请求,如果希望去共享数据,那么就可以使用当前对象来进行共享。
session对象是如何和一个客户端对应起来的呢?
session对象会生成一个唯一的编号,对象产生之后,会把该唯一的编号利用cookie传输给客户端,客户端会将该cookie信息保存到本地,当客户端下次访问服务器的时候,就会在此将cookie再次携带回来,通过取出里面的session编号,那么就可以定位到响应的session对象
如何得知当前请求头有没有关联的session对象呢?getSession()执行逻辑是怎样的
HttpSession getSession()
Returns the current session associated with this request, or if the request does not have a session, creates one.
getSession()方法会去查看当前的请求中又没有有效的Cookie:JSESSIONID=xxxx,如果有携带一个有效的JSESSIONDI,那么就可以利用该编号获取到对应的session对象;如果没有携带有效的编号,就再创建一个新的session对象
3.1常见问题
1.关闭浏览器,session对象会不会销毁,数据会不会丢失?
session对象没有销毁。数据会丢失
浏览器关闭之后,你会发现session对象以及编号发生了改变,发生改变并不是因为session对象被销毁了。而是凭证丢了,关闭服务器之后再打开,Cookie:JSESSIONID=xxxx不会再携带了。所以request.getSession()会重新创建一个新的session对象。
2.关闭服务器,session对象会不会销毁,数据会不会丢失?
服务器关闭之后,session域里面的数据仍然可以访问到。但是session的地址已经发生了变化。
原因:应用在关闭之前,会把session域里面的数据序列化到本地磁盘上。应用重启的时候,会重新读取该序列化文件,并且创建新的session对象来接收原先session的编号以及session域里面的数据。
3.session常用方法
1、setAttribute(String name,String value) 设定指定名字的属性的值,并将它添加到session会话范围内,如果这个属性是会话范围内存在,则更改该属性的值。
2、getAttribute(String name) 在会话范围内获取指定名字的属性的值,返回值类型为object,如果该属性不存在,则返回null。
3、removeAttribute(String name),删除指定名字的session属性,若该属性不存在,则出现异常。
4、invalidate(),使session失效。可以立即使当前会话失效,原来会话中存储的所有对象都不能再被访问。
5、getId( ),获取当前的会话ID。每个会话在服务器端都存在一个唯一的标示sessionID,session对象发送到浏览器的唯一数据就是sessionID,它一般存储在cookie中。
6、setMaxInactiveInterval(int interval) 设置会话的最大持续时间,单位是秒,负数表明会话永不失效。
7、getMaxInActiveInterval(),获取会话的最大持续时间,使用时候需要一些处理
8、getSession() 返回与此请求关联的当前会话,如果请求没有会话,则创建一个。
3.2三个域的比较
context域、session域、request域
对象里都有一个类似与map的数据结构,只要获取到同一个对象,就可以共享该对象里的数据
contest域:每个应用里有且只有一个ServletContext对象,只要是同一个应用下面的资源,那么均可以获取到同一个servletContext对象。共享context域。一般用来存储全局性的数据、配置等。比如当前系统的主机、端口号;当前商城卖的商品分类。session域:正常情况下来说,只有请求中没有携带有效的Cookie:JSESSIONID=xxxx时,执行request.getSession()代码时会创建一个新的session对象。一般情况下来说,如果没有特殊设置,**正常情况下一个浏览器对应一个session对象。只要使用同一个浏览器来访问当前应用下的所有资源时,那么均可以共享session域。**可以用来存放用户相关的数据。登录状态、购物车、浏览记录
request域:只有转发的两个组件之间可以共享request域。一般情况下存储一些仅当前请求需要用到的数据。
4.JWT(JSON Web Token)
介绍JWT认证:在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证。我们不再使用Session认证机制,而使用Json Web Token(本质就是token)认证机制
4.1JWT构成
JWT就是一段字符串,由三部分信息组成,这三部分信息之间用 . 来连接,这样就构成了Jwt字符串。
第一部分信息:我们称之为头部(header)
第二部分信息:我们称之为载荷(payload)
第三部分信息:签证(signature)
jwt头部
头部承载两部分信息
声明类型
声明机密算法,通常使用HMAC SHA256将头部进行base64加密,构成了第一部分
jwt载荷
载荷是盛放有效信息的地方,包括三部分信息
标准中注册的声明
公共的声明:公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明:私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息
然后进行base64加密得到第二部分
jwt签证
签证信息包含三部分
header(base64加密之后)
payload(base64加密之后)
secret
这个部分需要base64加密后的header和base64加密后的payload使用
.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了
4.2JWT认证开发流程
(1)使用账号密码进行登录,登录接口中调用签发token的接口,得到token,返回给客户端,客户端自己将token信息存储在本地浏览器
public Account login(Account account) {
Account dbAdmin = adminMapper.selectByUsername(account.getUsername());
if (ObjectUtil.isNull(dbAdmin)) {
throw new CustomException(ResultCodeEnum.USER_NOT_EXIST_ERROR);
}
if (!account.getPassword().equals(dbAdmin.getPassword())) {
throw new CustomException(ResultCodeEnum.USER_ACCOUNT_ERROR);
}
// 生成token
String tokenData = dbAdmin.getId() + "-" + RoleEnum.ADMIN.name();
String token = TokenUtils.createToken(tokenData, dbAdmin.getPassword());
dbAdmin.setToken(token);
return dbAdmin;
}
/**
* 生成token
*/
public static String createToken(String data, String sign) {
return JWT.create().withAudience(data) // 将 userId-role 保存到 token 里面,作为载荷
.withExpiresAt(DateUtil.offsetHour(new Date(), 2)) // 2小时后token过期
.sign(Algorithm.HMAC256(sign)); // 以 password 作为 token 的密钥
}
前端拿到后端返回的token之后,将数据存储在本地浏览器
this.$request.post('/login', this.form).then(res => {
if (res.code === '200') {
let user = res.data;
localStorage.setItem("xm-user", JSON.stringify(user)) // 存储用户数据
之后每次发送请求时,将token信息放置在请求头
request.interceptors.request.use(config => {
config.headers['Content-Type'] = 'application/json;charset=utf-8'; // 设置请求头格式
let user = JSON.parse(localStorage.getItem("xm-user") || '{}') // 获取缓存的用户信息
config.headers['token'] = user.token // 设置请求头
return config
}, error => {
console.error('request error: ' + error) // for debug
return Promise.reject(error)
});
后端对前端发送的请求进行验证
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 1. 从http请求的header中获取token
String token = request.getHeader(Constants.TOKEN);
if (ObjectUtil.isEmpty(token)) {
// 如果没拿到,从参数里再拿一次
token = request.getParameter(Constants.TOKEN);
}
// 2. 开始执行认证
if (ObjectUtil.isEmpty(token)) {
throw new CustomException(ResultCodeEnum.TOKEN_INVALID_ERROR);
}
Account account = null;
try {
// 解析token获取存储的数据
String userRole = JWT.decode(token).getAudience().get(0);
String userId = userRole.split("-")[0];
String role = userRole.split("-")[1];
// 根据userId查询数据库
if (RoleEnum.ADMIN.name().equals(role)) {
account = adminService.selectById(Integer.valueOf(userId));
}
if (RoleEnum.MERCHANT.name().equals(role)) {
account = merchantService.selectById(Integer.valueOf(userId));
}
if (RoleEnum.USER.name().equals(role)) {
account = userService.selectById(Integer.valueOf(userId));
}
} catch (Exception e) {
throw new CustomException(ResultCodeEnum.TOKEN_CHECK_ERROR);
}
if (ObjectUtil.isNull(account)) {
throw new CustomException(ResultCodeEnum.USER_NOT_EXIST_ERROR);
}
try {
// 用户密码加签验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(account.getPassword())).build();
jwtVerifier.verify(token); // 验证token
} catch (JWTVerificationException e) {
throw new CustomException(ResultCodeEnum.TOKEN_CHECK_ERROR);
}
return true;
}
如果验证成功则返回请求资源,如果验证失败则抛出ResultCodeEnum.TOKEN_CHECK_ERROR异常
在这个过程中,前端拿到token信息之后,再次访问后端的时候,token的信息是保存在了header中,当然这个信息也可以保存在cookie中,然后携带cookie去访问后端,后端在cookie中取出token信息进行验证即可。