0

0

如何在Java中构建图片防盗链接系统 Java判断来源并控制资源访问

星夢妙者

星夢妙者

发布时间:2025-07-23 19:13:02

|

1193人浏览过

|

来源于php中文网

原创

图片防盗链系统的核心实现方案有两种:基于http referer的校验和基于token的动态链接验证。1. 基于http referer的校验通过检查请求头中的referer字段判断来源是否合法,但该方式易被伪造或因隐私设置失效;2. 基于token的动态链接方案在生成图片链接时附加带签名和时间戳的token,并在服务器端验证其有效性,安全性更高。具体实现中需完成token生成、传递、验证流程,并结合spring boot拦截器统一处理验证逻辑,同时面临性能开销、cdn兼容性、浏览器缓存等挑战。

如何在Java中构建图片防盗链接系统 Java判断来源并控制资源访问

在Java中构建图片防盗链系统,核心思路是通过服务器端对请求的来源进行判断。这通常意味着我们会检查HTTP请求头中的Referer信息,或者更安全地,为图片资源生成带有时间戳或签名的动态链接(token),在图片被请求时,后端再验证这个token的有效性,从而决定是否返回图片数据。这种做法能有效防止图片被未经授权的网站直接引用,保护你的带宽和内容版权。

如何在Java中构建图片防盗链接系统 Java判断来源并控制资源访问

解决方案

构建一个图片防盗链系统,可以从两个主要方向入手:基于HTTP Referer的简单校验,以及更健壮的基于Token的动态链接方案。

方案一:基于HTTP Referer的校验

立即学习Java免费学习笔记(深入)”;

如何在Java中构建图片防盗链接系统 Java判断来源并控制资源访问

这是最直接也最容易实现的方式。当浏览器请求一个资源时,通常会在HTTP请求头中带上Referer字段,指明这个请求是从哪个页面发起的。我们可以在服务器端检查这个字段。

假设你有一个Spring Boot应用,可以这样做:

如何在Java中构建图片防盗链接系统 Java判断来源并控制资源访问
import org.springframework.stereotype.Controller;
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.ResponseBody;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@Controller
public class ImageController {

    private final String ALLOWED_DOMAIN = "http://yourdomain.com"; // 替换为你的域名

    @GetMapping("/images/{imageName:.+}") // .+: 匹配文件名,包括点
    @ResponseBody
    public void getImage(@PathVariable String imageName,
                         @RequestHeader(value = "Referer", required = false) String referer,
                         HttpServletResponse response) throws IOException {

        if (referer == null || !referer.startsWith(ALLOWED_DOMAIN)) {
            // 如果Referer为空或者不是来自允许的域名,则拒绝访问
            response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403 Forbidden
            // 可以返回一个默认的“禁止盗链”图片,或者直接结束
            return;
        }

        // 假设图片存储在某个目录下
        Path imagePath = Paths.get("/path/to/your/images/", imageName); // 替换为你的图片存储路径

        if (Files.exists(imagePath) && Files.isReadable(imagePath)) {
            response.setContentType(Files.probeContentType(imagePath));
            try (InputStream is = Files.newInputStream(imagePath)) {
                is.transferTo(response.getOutputStream());
            }
        } else {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND); // 404 Not Found
        }
    }
}

这段代码很简单,但需要注意的是,Referer头很容易被伪造,或者在某些隐私设置下可能不会发送。所以,这更多是一种初级的、象征性的防护。

方案二:基于Token的动态链接防盗链

这是更推荐、更健壮的方案。核心思想是,当你的页面(比如yourdomain.com/article/123)加载时,其中包含的图片链接不再是静态的/images/pic.jpg,而是动态生成的,例如/images/pic.jpg?token=XYZ×tamp=123。服务器端在处理/images/pic.jpg的请求时,会验证这个tokentimestamp是否有效。

实现步骤概览:

  1. Token生成: 在生成包含图片的HTML页面时,为每个图片生成一个唯一的、有时效性的Token。这个Token可以包含图片路径、过期时间,并用一个密钥进行签名(例如HMAC-SHA256),防止篡改。
  2. Token传递: 将生成的Token作为查询参数附加到图片URL上。
  3. Token验证: 当用户浏览器请求图片时,服务器端截取URL中的Token,验证其签名和过期时间。如果验证通过,则返回图片;否则,返回403 Forbidden或一个默认的错误图片。

Token生成示例(一个简化的HMAC签名):

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

public class ImageTokenUtil {

    private static final String SECRET_KEY = "your_very_secret_key_here"; // 生产环境请使用更复杂的密钥
    private static final String HMAC_ALGORITHM = "HmacSHA256";
    private static final long TOKEN_EXPIRATION_MILLIS = 5 * 60 * 1000; // Token有效期:5分钟

    public static String generateSignedToken(String imagePath) {
        long expires = System.currentTimeMillis() + TOKEN_EXPIRATION_MILLIS;
        String dataToSign = imagePath + ":" + expires; // 待签名数据:图片路径 + 过期时间

        try {
            Mac mac = Mac.getInstance(HMAC_ALGORITHM);
            SecretKeySpec secretKeySpec = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
            mac.init(secretKeySpec);
            byte[] hmacSha256 = mac.doFinal(dataToSign.getBytes(StandardCharsets.UTF_8));
            String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(hmacSha256);

            return Base64.getUrlEncoder().withoutPadding().encodeToString(dataToSign.getBytes(StandardCharsets.UTF_8)) + "." + signature;
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            // 实际应用中需要更好的异常处理
            e.printStackTrace();
            return null;
        }
    }

    public static boolean validateSignedToken(String fullToken) {
        if (fullToken == null || !fullToken.contains(".")) {
            return false;
        }
        String[] parts = fullToken.split("\\.");
        if (parts.length != 2) {
            return false;
        }

        String encodedData = parts[0];
        String receivedSignature = parts[1];

        try {
            String dataToSign = new String(Base64.getUrlDecoder().decode(encodedData), StandardCharsets.UTF_8);
            String[] dataParts = dataToSign.split(":");
            if (dataParts.length != 2) {
                return false;
            }
            String imagePath = dataParts[0];
            long expires = Long.parseLong(dataParts[1]);

            if (System.currentTimeMillis() > expires) {
                // Token已过期
                return false;
            }

            // 重新计算签名并比对
            Mac mac = Mac.getInstance(HMAC_ALGORITHM);
            SecretKeySpec secretKeySpec = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
            mac.init(secretKeySpec);
            byte[] hmacSha256 = mac.doFinal(dataToSign.getBytes(StandardCharsets.UTF_8));
            String expectedSignature = Base64.getUrlEncoder().withoutPadding().encodeToString(hmacSha256);

            return expectedSignature.equals(receivedSignature);

        } catch (Exception e) {
            // 签名解析或验证失败
            e.printStackTrace();
            return false;
        }
    }
}

图片服务Controller结合Token验证:

// 假设ImageController中引入了ImageTokenUtil
// ... 其他导入和类定义 ...

@GetMapping("/secure-images/{encodedToken}/{imageName:.+}")
@ResponseBody
public void getSecureImage(@PathVariable String encodedToken,
                           @PathVariable String imageName,
                           HttpServletResponse response) throws IOException {

    // 实际的图片路径需要从token中解析出来,或者这里只作为辅助验证
    // 假设token中包含完整的图片路径信息,这里简化处理
    String fullToken = encodedToken + "." + imageName; // 这里需要根据实际token生成方式调整

    if (!ImageTokenUtil.validateSignedToken(fullToken)) {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403 Forbidden
        return;
    }

    // 从token中解析出实际的图片路径,或者直接使用imageName(如果token只负责验证权限)
    // 为了简化,这里仍然假设imageName是实际的文件名,但实际应用中,token应包含完整路径或校验路径
    Path imagePath = Paths.get("/path/to/your/images/", imageName);

    if (Files.exists(imagePath) && Files.isReadable(imagePath)) {
        response.setContentType(Files.probeContentType(imagePath));
        try (InputStream is = Files.newInputStream(imagePath)) {
            is.transferTo(response.getOutputStream());
        }
    } else {
        response.setStatus(HttpServletResponse.SC_NOT_FOUND); // 404 Not Found
    }
}

在HTML页面中,图片链接会变成类似这样: 如何在Java中构建图片防盗链接系统 Java判断来源并控制资源访问 这只是一个示意,实际的URL结构和Token的生成与解析需要严格对应。

为什么传统的防盗链方法效果不佳?

谈到传统的防盗链方法,我们通常会想到基于HTTP Referer的校验,或者一些前端JavaScript的判断。这些方法之所以效果不佳,甚至可以说“防君子不防小人”,主要有几个原因:

首先,Referer头信息极易伪造。攻击者或者一些爬虫工具,可以非常轻松地在请求中伪造Referer头,将其设置为你的合法域名。这就好比你家门上贴了个“本小区住户请走正门”的牌子,但小偷换身衣服就混进来了,你根本无法辨别。很多浏览器出于用户隐私考虑,也可能默认不发送Referer,或者只发送部分信息,这导致即使是合法用户也可能被误判。

其次,前端JavaScript防盗链形同虚设。所有在浏览器端执行的逻辑,都可以在开发者工具中被轻易查看、修改甚至禁用。你用JavaScript判断当前页面URL是否是你的域名,如果不是就替换图片链接或者阻止加载,这对于稍微懂点前端知识的人来说,绕过简直不费吹灰之力。就好比你把家里的钥匙藏在门口地毯下,并希望小偷找不到——这显然不现实。

sematic
sematic

一个开源的机器学习平台

下载

再者,CDN缓存可能带来复杂性。如果你的图片通过CDN加速,CDN节点会缓存你的图片。当CDN收到请求时,它可能不会携带原始的Referer信息到你的源站,或者CDN自身的配置会影响Referer的传递。这使得基于Referer的校验在CDN环境下变得更加复杂,甚至失效。你得考虑CDN如何与你的防盗链逻辑配合,这本身就是个挑战。

最后,这些方法都无法阻止直接下载。如果有人直接知道了你的图片URL,并且你的服务器没有做任何校验,他可以直接通过下载工具、脚本等方式批量下载你的图片,完全绕过你的网页访问。

正是因为这些局限性,我们才需要更底层的、服务器端强制执行的、基于加密和时效性的验证机制,也就是Token防盗链。

实现图片防盗链,有哪些关键技术点和挑战?

实现一个健壮的图片防盗链系统,远不止写几行代码那么简单。这里面涉及到不少关键的技术点和需要面对的挑战:

关键技术点:

  1. 安全Token生成与验证:

    • 加密算法选择: HMAC(Hash-based Message Authentication Code)是一个非常好的选择,它能确保Token的完整性和真实性,防止篡改。使用如HMAC-SHA256这样的算法,配合一个只有服务器知道的密钥,可以有效生成和验证签名。
    • Token内容设计: Token里应该包含什么信息?通常包括图片资源的唯一标识(路径或ID)、过期时间戳。有时为了更严格,还可以加入请求用户的IP地址,实现IP绑定,但这也增加了复杂性。
    • 时间戳管理: 确保Token包含过期时间,并且服务器端能正确解析和判断是否过期。这是一个防止Token被无限期重用的关键。
    • 一次性Token(可选): 对于特别敏感的资源,可以考虑生成一次性Token,即Token使用一次后就立即失效。这需要服务器维护Token的使用状态,增加了存储和同步的复杂性。
  2. URL重写与路由:

    • 你需要设计一个清晰的URL结构,能方便地嵌入Token,同时又不显得过于冗长。例如/images/secure/{token}/{filename}
    • 服务器端需要配置路由规则,将这类请求正确地导向你的防盗链处理逻辑。
  3. 图片流式传输:

    • 当验证通过后,你需要将图片文件以字节流的形式写入HTTP响应体。正确设置Content-Type(如image/jpeg, image/png等)和Content-Length头,确保浏览器能正确解析和显示图片。
    • 利用Java NIO的Files.newInputStreamtransferTo方法可以高效地传输文件。
  4. 错误处理与用户体验:

    • 当防盗链验证失败时,应该返回什么?通常是HTTP 403 Forbidden状态码。
    • 为了更好的用户体验,可以配置一个默认的“禁止盗链”图片,而不是直接显示一个破碎的图片图标。这可以通过返回一个预设的图片流来实现。

面临的挑战:

  1. 性能开销: 每次图片请求都需要进行Token的生成(在页面渲染时)和验证(在图片请求时),这会增加服务器的CPU开销。对于高并发、图片数量巨大的网站,这可能是一个显著的性能瓶颈。
    • 优化策略: 可以考虑Token的缓存,或者将Token的生成逻辑下放到前端(通过JS在页面加载后动态生成,但安全性稍弱),或者使用CDN的Token机制。
  2. Token管理与同步: 如果你的应用是集群部署的,那么每个服务器实例都需要能够生成和验证Token。这意味着密钥必须在所有实例间保持一致。如果使用一次性Token,还需要一个共享的、高性能的存储(如Redis)来记录Token的使用状态,并解决分布式环境下的并发问题。
  3. CDN兼容性: 多数大型网站都会使用CDN来加速图片分发。如何让防盗链系统与CDN协同工作是一个复杂的问题。
    • CDN的Token支持: 许多CDN服务商本身就提供了Token防盗链功能(如阿里云OSS、腾讯云COS的签名URL),直接利用CDN的能力往往是更优的选择,它将验证逻辑下沉到CDN边缘节点,减轻源站压力。
    • 回源验证: 如果CDN不支持Token,或者你选择在源站验证,那么CDN每次缓存失效或首次请求时,都需要回源到你的Java应用进行验证,这可能会增加回源带宽和源站压力。
  4. 浏览器缓存: 动态URL(带Token的URL)会导致浏览器每次都认为是一个新资源,从而可能无法有效利用浏览器缓存。这会增加服务器的请求量。
    • 解决方案: 可以将Token的有效期设置得相对长一些,或者在Token中只包含少量动态信息,让图片本身的URL保持相对稳定,或者利用Etag/Last-Modified头进行协商缓存。
  5. 合法外部链接的处理: 有时你可能希望图片能被社交媒体平台(如微博、微信)正常分享和显示。这些平台在抓取图片时,其Referer可能不是你的域名。你需要为这些特定的场景提供白名单机制,或者生成特殊的、长期有效的分享Token。
  6. 密钥管理: 用于签名Token的密钥至关重要,一旦泄露,防盗链机制将彻底失效。密钥应该妥善保管,定期轮换,并避免硬编码在代码中。

这些挑战要求我们在设计系统时,不仅要考虑功能实现,更要从性能、可扩展性、安全性和运维角度进行全面权衡。

如何在Spring Boot项目中优雅地实现图片资源保护?

在Spring Boot项目中实现图片资源保护,我们可以利用Spring框架的特性,比如拦截器(Interceptor)或者过滤器(Filter),来集中处理防盗链逻辑,而不是在每个图片处理方法中重复代码。这使得代码更清晰,维护起来也方便。

这里我们以基于Token的防盗链为例,展示如何通过Spring Interceptor来优雅地实现。

1. Token生成工具类(同上,或更完善):ImageTokenUtil.java (保持不变,或根据实际需求调整,例如generateSignedToken方法可能需要传入图片资源的唯一ID而不是路径,然后通过ID查找实际路径)

2. 图片资源控制器: 这个控制器会变得非常简洁,因为它不再直接处理防盗链逻辑,而是专注于读取和返回图片文件。

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@Controller
public class SecureImageController {

    private final String IMAGE_BASE_PATH = "/path/to/your/images/"; // 你的图片存储根目录

    @GetMapping("/secure-images/{imageName:.+}") // 这里的URL不再包含token,token会在拦截器中处理
    @ResponseBody
    public void getSecureImage(@PathVariable String imageName,
                               HttpServletResponse response) throws IOException {
        // 假设拦截器已经验证了权限,这里直接提供图片
        Path imagePath = Paths.get(IMAGE_BASE_PATH, imageName);

        if (Files.exists(imagePath) && Files.isReadable(imagePath)) {
            response.setContentType(Files.probeContentType(imagePath));
            try (InputStream is = Files.newInputStream(imagePath)) {
                is.transferTo(response.getOutputStream());
            }
        } else {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND); // 图片不存在
        }
    }
}

3. 防盗链拦截器: 这是核心部分,它会在请求到达SecureImageController之前进行拦截和验证。

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;

public class AntiHotlinkInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 从请求参数中获取token,例如:/secure-images/pic.jpg?token=XYZ
        String token = request.getParameter("token");
        String requestUri = request.getRequestURI(); // /secure-images/pic.jpg
        String imageName = requestUri.substring(requestUri.lastIndexOf('/') + 1); // pic.jpg

        // 假设token中需要包含imageName作为验证的一部分
        // 实际场景中,token可能只包含一个资源ID,然后通过ID去查找资源路径
        String dataToValidate = imageName; // 或者更复杂的,从token中解析出原始数据

        if (token == null || !ImageTokenUtil.validateSignedToken(token)) {
            // Token无效或缺失
            response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403 Forbidden
            // 可以选择返回一个默认的“禁止盗链”图片,或者一个错误页面
            // response.sendRedirect("/error-images/hotlink-forbidden.png");
            return false; // 阻止请求继续向下执行
        }

        // 如果token验证通过,则放行
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 在Controller方法执行后,视图渲染前执行
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 在整个请求完成后执行,用于资源清理等
    }
}

4. 配置拦截器:

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

804

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

723

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

727

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

395

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

398

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

445

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

428

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16861

2023.08.03

小游戏4399大全
小游戏4399大全

4399小游戏免费秒玩大全来了!无需下载、即点即玩,涵盖动作、冒险、益智、射击、体育、双人等全品类热门小游戏。经典如《黄金矿工》《森林冰火人》《狂扁小朋友》一应俱全,每日更新最新H5游戏,支持电脑与手机跨端畅玩。访问4399小游戏中心,重温童年回忆,畅享轻松娱乐时光!官方入口安全绿色,无插件、无广告干扰,打开即玩,快乐秒达!

30

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.1万人学习

C# 教程
C# 教程

共94课时 | 5.7万人学习

Java 教程
Java 教程

共578课时 | 39.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号