一、概述

1.1 HTTP无状态问题

目前主流的服务采用的是B\S架构,即浏览器\服务端架构。一般采用的协议是HTTP,HTTP有个特点是无状态,即在一次连接,两次成功请求之间没有任何的关系。这个特性既带来了一定的优点,在某些场景下也有不足。

优点:因为服务器不会去记录HTTP的状态,所以不需要额外的资源来记录状态信息,减轻了服务器的负担
缺点:因为没有记忆能力,如果后续操作需要前面的信息,必须重传,效率低。例如,用户在进行购物时,用户将某一个商品加入购物车,切换页面后,再次添加另一个商品,者两次添加商品的请求之间没有任何关系。因此,浏览器就无法知道用户最终选择了哪些商品。可使用HTTP的头部进行扩展来解决此问题。

1.2 解决方案

目前主要有三种主流的解决方案:Cookie机制、Session机制以及Token机制

二、三种机制的原理

2.1 Cookie机制原理

2.1.1 特点

  1. 服务器发送到浏览器,并保存到浏览器的一小块数据;
  2. 浏览器下次访问该服务器时,会自动携带该块数据,将其发送给服务器。
  3. 主要用于服务器判断请求是否来自同一个浏览器。

2.1.2 原理

核心机制:通过在请求和响应报文中写入Cookie信息来控制客户端的状态

  1. 当客户端第一次向服务器请求信息时,后端服务器会将需要验证的信息以Key-Value的形式放入Cookie中,并将Cookie放入到响应报文中,客户端会解析响应报文取出Cookie并保存Cookie;
  2. 当该客户端再次向服务器发送请求报文时,会在请求报文中添加Cookie信息,服务器在收到请求报文时,会解析Cookie中的信息,判断是否和预期设置的验证信息一致,进而达到不同请求之间的数据互通。

2.1.3 优缺点

优点:
	1. 可配置到期时间;
	2. 简单性:Cookie是一种基于文本的轻量结构,包含简单的键值对;
	3. 数据持久性:Cookie在过期之前一直存储在客户端浏览器上。
缺点:
	1. 大小受限制:大多数浏览器对Cookie的大小有4K、8K的限制;
	2. 用户配置Cookie禁用:有些用户会在浏览器客户端禁用Cookie的能力;
	3. 安全性差:Cookie由于存在在浏览器,有可能会被篡改

2.1.4 代码实现

/*
* 服务器生成Cookie
* */
@RequestMapping(path = "/cookie/set", method = RequestMethod.GET)
@ResponseBody
public String setCookie(HttpServletResponse response) {
    Cookie cookie = new Cookie("code", CommunityUtil.generateUUID()); // 存在验证信息
    cookie.setMaxAge(60 * 10); // 设置过期时间
    cookie.setPath("/community/alpha"); // 设置生效路径
    response.addCookie(cookie); // 将Cookie放入到响应报文中

    return "set cookie";
}

/*
 * 浏览器发送Cookie
 * */
@RequestMapping(path = "/cookie/get", method = RequestMethod.GET)
@ResponseBody
public String getCookie(@CookieValue("code") String code) {
    System.out.println(code);

    return "get cookie";
}

2.2 Session机制原理

2.2.1 特点

  1. 在服务端保存客户端的信息;
  2. Session指的是同一个浏览器发起的多次请求,一旦会话关闭,Session也会被销毁;
  3. 服务器会为每一次会话分配一个Session对象。

2.2.2 原理

核心机制:服务器端生成一个Session,存储浏览器的各种信息,同时有唯一的一个sessionId,服务器通过识别这个sessionId来实现业务的连续

  1. 打开第一次会话时,服务器会默认生成一个Session,服务器将各种信息存放在Session中,同时服务器端隐式的生成一个Cookie用于存放SessionId,会放在响应报文中返回给浏览器,浏览器会保存Cookie;
  2. 同一个会话下,每次请求都会将SessionId放入到Cookie中,发送给服务器,服务器就可以解析Session获取需要的信息。

2.2.3 优缺点

优点:解决了Cookie存放数据大小的限制, 同时数据保存在服务器, 安全性更高
缺点:大量的Session数据存放在服务器, 占用很大的内存

扩展:实际生产环境中如何使用Session?

背景:现实生活中多采用分布式集群架构,中间利用NGINX保证负载均衡;若服务器A为此请求生成了session,下一次请求时,因NGINX可能会请求到另一台服务器上,重新生成Session,无法获取到上一次请求的信息,同时占用大量内存。
解决方案:设置分配策略
1. 黏性Session:记住上次访问主机的IP地址,下一次同一请求,还来请求这台服务器;【缺点:无法保证负载均衡】
2. 同步Session: 每生成一个session,在服务器集群中同步该session;【缺点:1. 同步对服务器产生性能影响;2.服务器之间产生耦合,对部署不利。】
3. 共享Session: 专门设置一台服务器,用于存放Session;【缺点:单机存在性能瓶颈,一旦宕机,则无法使用】
利用数据库[推荐使用]:大部分存在浏览器中,敏感数据直接放在Mysql数据库集群中;【缺点:数据存放在硬件中,性能低】==> 优化:利用Redis数据库存放

2.2.4 代码实现

/*
* 服务器端生成session
* */
@RequestMapping(path = "/session/set", method = RequestMethod.GET)
@ResponseBody
public String setSession(HttpSession session) {
    session.setAttribute("id", 1);
    session.setAttribute("name", "test");
    return "set session";
}

/*
 * 浏览器发送请求的时候session
 * */
@RequestMapping(path = "/session/get", method = RequestMethod.GET)
@ResponseBody
public String getSession(HttpSession session) {
    session.getAttribute("id");
    session.getAttribute("name");
    return "get session";
}

2.3 Token机制原理

2.3.1 特点

JWT(JSON Web Token)是一种开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。这些信息可以被验证和信任,因为它们是数字签名的,不需要存储在服务器端。

2.3.2 原理

JWT主要由三部分组成:Header、PayLoad和Signature,每一部分通过"."分割。例如:

xxxx.yyyyy.zzzzzz

Header由两部分组成:算法和类型。例如:

{
	”alg“: "HS256",
	"typ": "JWT"
}

上述JSON数据会被encode成Base64Url, 作为JWT第一部分设置。
PayLoad:载荷,即存放有效信息的地方,是一组claim值。claim包含claim name和claim value,前者是String类型,后者可以是任意的json对象。claims有三种类型:reserved claim,public claim和private claim,类似于Java中的权限控制符。例如:

{
	"sub": "27927692",
	"name": ”John“,
	"admin": true
}

上述JSON也会被encode成Base64Url, 作为第二部分设置。
Signature: 签名,用点号将header和payload关联起来,用header中指定的加密算法对字符串进行加密。例如:

signature = HAMCHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret密钥)

最佳实践:在客户端和服务端之间的传输将JWT设置在HTTP header里,一般名称设置为Authorization的header, 具体格式如下:

Authorization: Bearer <token>。Beaer之后有个空格

2.3.3 代码实现

// 生成JWT Token
	@Test
    public void testGengerateJWT() {
        String jwtToken = Jwts.builder()
                // set header
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                // set payload
                .claim("username", "tom")
                .claim("role", "admin")
                .setSubject("admin-role-test")
                // set expired time 有效期
                .setExpiration(new Date(System.currentTimeMillis() + time))
                .setId(UUID.randomUUID().toString())
                // set signature
                .signWith(SignatureAlgorithm.HS256, secert)
                .compact();
        System.out.println(jwtToken);
    }

	// 获取JWT Token信息以及验证Token的有效性
    @Test
    public void testCheckJwtToken() {
        String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InRvbSIsInJvbGUiOiJhZG1pbiIsInN1YiI6ImFkbWluLXJvbGUtdGVzdCIsImV4cCI6MTcxMzMxOTA1NSwianRpIjoiNzViNjlhYTUtNjBmYy00NGQwLWJmYjctZGIyMWRhZWE4MDQ1In0.pg7F_gN9r6gZuMie4Ibs6IYjfvVAbywMcSSQtdUA27s";
        JwtParser parser = Jwts.parser();
        Claims claims = parser.setSigningKey(secert).parseClaimsJws(token).getBody();
        System.out.println(claims.get("username"));
        System.out.println(claims.get("role"));
        System.out.println(claims.getExpiration());
    }

三、三种机制的区别和联系

  1. 是否支持跨域:JWT支持跨域,Cookie和Session不支持跨域,只能在当前域和子域中使用;
  2. 存储位置:JWT放在HTTP header中, Cookie放在客户端,Session放在服务端;
  3. 安全性:JWT 的安全性高于Session和Cookie
  4. 性能: JWT中放在Header中,若存放信息过多,会使得整个网络流量非常大,但不占用内存,不过需要耗费解析Token的时间;Session耗费服务器的内存;Cookie放在客户端,相对来说占用内存较小。
  5. 原理:JWT Token是使服务端无状态化的机制,不会存储会话信息,支持服务负载均衡;Session使服务端有状态化,会存储会话信息,在分布式情况下,无法负载均衡
  6. 应用场景:JWT比较适合移动端(不支持Cookie)和内部系统,跨域认证、API接口鉴权等场景,特别是在微服务架构和分布式系统中。