HTTP 协议请求头 If-Match、If-None-Match 和 ETag

概述

在 HTTP 协议中,请求头 If-MatchIf-None-MatchIf-Modified-SinceIf-Unmodified-SinceIf-Range 主要是为了解决浏览器缓存数据而定义的请求头标准,按照协议规范正确的判断和使用这几个请求头,可以更精准的处理浏览器缓存,从而达到提高系统性能和减少系统带宽的占用的目的。

更精准的处理 Web 缓存效果是可以很明显的:

  • 1、 减少了网络交互,加快页面响应速度,增强用户体验;
  • 2、 减少了网络带宽消耗,因为没有更新的资源就不需要重复返回了,特别是图片、视频、下载文件这类大响应体请求;

当请求中存在上述 If-xxx 时,服务器对附加的条件进行判断,当判定条件为真,才会执行标准的数据处理和数据返回,否则直接返回对应的HTTP错误码。

针对服务端原始资源是否变更目前有两类处理规则:基于修改时间的(If-Modified-SinceIf-Unmodified-Since)和基于自定义标识的(If-MatchIf-None-Match),还有一个是处理文件断电续传使用到的 If-Range

经常做服务端开发的会发现,基于时间的并不能很精准的进行缓存判断,有些场景下后端资源可能在1秒钟以内进行了变更,时间请求头只精确到秒,是不足以覆盖这种场景的。还有一些场景是我们没有定义修改时间的,可能是基于其他标志记录是否被修改的。这种情况下,我们使用 If-MatchIf-None-Match 来进行资源是否变更的更精准判断,这两个头基于一个自定义字符串传送,这个字符串你可以自己定义,例如用 md5,时间戳都可以,需要注意它俩需要结合 ETag 请求头一起使用(ETag 指代一个独一无二的版本号字符串,称为“实体标签”)。

下文针对 If-Match、If-None-Match 和 ETag 的交互原理及使用方法进行说明。

详解

服务端对资源记录一个 ETag(实体标记)的字段,当资源更新后ETag也会随之更新。
所以当客户端 If-Match 的值若与服务端的ETag一致,才会执行请求,否则拒绝处理返回412状态码。

交互图:

HTTP 协议请求头 If-Match、If-None-Match 和 ETag_第1张图片
HTTP 协议请求头 If-Match、If-None-Match 和 ETag_第2张图片

示例代码:

package com.example.webfluxreactivedemo1.controller;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * HTTP请求头IfMatch和ETag处理
 *
 * @author 单红宇
 * @date 2023/11/2 10:09
 */

@RestController
@RequestMapping
public class IfMatchController {

    /**
     * 获取资源接口
     *
     * @param id         资源ID
     * @param clientETag 请求头中的 If-None-Match
     * @return ResponseEntity
     */
    @GetMapping("/resource/{id}")
    public ResponseEntity<String> getResource(@PathVariable String id,
                                              @RequestHeader("If-None-Match") String clientETag) {
        // 检查资源是否存在以及资源最新的ETag是否与请求头中的If-None-Match匹配
        boolean resourceExists = checkResourceExists(id);
        boolean etagMatch = checkETagMatch(id, clientETag);

        if (!resourceExists) {
            // 如果资源不存在,返回404 Not Found
            return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
        } else if (!etagMatch) {
            // 如果资源存在且ETag不匹配(即资源已经发生了变更),则返回资源内容
            return ResponseEntity.ok().header(HttpHeaders.ETAG, this.generateETag(id)).body("Resource content");
        } else {
            // 如果资源存在且ETag匹配(即资源没有发生变更),返回304和空响应体
            return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
        }
    }

    /**
     * 修改文章内容
     *
     * @param id          文章ID
     * @param clientETag 请求头中的 If-Match
     * @return ResponseEntity
     */
    @GetMapping("/updateArticle/{id}")
    public ResponseEntity<String> updateArticle(@PathVariable String id,
                                              @RequestHeader("If-Match") String clientETag) {
        // 检查资源是否存在以及资源最新的ETag是否与请求头中的If-Match匹配
        boolean etagMatch = checkETagMatch(id, clientETag);

        if (etagMatch) {
            // 如果资源存在且ETag匹配,即文章没有被其他人修改过,执行更新操作
            String newETag = "返回文章最新的ETag";
            // articleService.update(id);
            return ResponseEntity.ok().header(HttpHeaders.ETAG, newETag).body("修改成功");
        } else {
            // 如果ETag不匹配,说明文章被其他人修改过,用户需要获取最新内容后再基于最新内容修改提交,防止多人同时修改文章内容出现覆盖问题
            // 返回412 Precondition Failed
            return ResponseEntity.status(HttpStatus.PRECONDITION_FAILED).build();
        }
    }


    // 如果资源存在但ETag不匹配,返回412 Precondition Failed

    /**
     * 检查资源是否存在
     *
     * @param id 资源ID
     * @return true=存在
     */
    private boolean checkResourceExists(String id) {
        // 在这里实现检查资源是否存在的逻辑
        return true;
    }

    /**
     * 检查资源ETag是否匹配,即判定资源的ETag是否发生了变动
     *
     * @param id         资源ID
     * @param clientETag 浏览器客户端传过来的ETag
     * @return 当资源已经被更新时返回false,资源未更新返回true
     */
    private boolean checkETagMatch(String id, String clientETag) {
        // 在这里实现检查资源ETag是否与请求头中的If-Match/If-None-Match匹配的逻辑
        return true;
    }

    /**
     * 生成一个ETag
     *
     * @param resourceId 资源ID
     * @return ETag
     */
    public String generateETag(String resourceId) {
        // 在这里实现检查资源是否存在的逻辑
        String eTag = "根据resourceId按照自己的逻辑生成etag,比如你可以使用md5";

        // 注意ETag必须使用双引号包起来返回,这是HTTP协议规范要求
        return "\"" + eTag + "\"";
    }
}

常见误区

以下是关于这两个字段的一些常见误区:

  • 错误的使用方式:有些开发者可能会错误地将If-None-Match和If-Match混淆或颠倒使用。例如,本应使用If-None-Match来检查缓存有效性的情况下使用了If-Match,这可能导致不必要的请求失败。

  • 不了解Etag的工作机制:Etag是一个与特定资源关联的确定值,通常由服务器生成并存储。当资源发生变化时,Etag也会相应地更新。而有些开发者可能误认为Etag是由客户端生成和管理的,这可能导致无法正确使用If-Match或If-None-Match。

  • 不正确的Etag格式:Etag的格式应该是ASCII字符串,可能包含一个"W/"前缀来表示弱比较算法。有些开发者可能会忽略这一点,导致Etag格式不正确,从而影响缓存控制的效果。

为了避免这些错误,建议开发者仔细阅读HTTP规范,确保正确理解和使用If-Match和If-None-Match字段。同时,也需要了解和掌握Etag的工作机制和正确的使用方法。


(END)

你可能感兴趣的:(Java开发,http)