java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot图片防盗链

SpringBoot实现图片防盗链的五种方式详解

作者:墨瑾轩

出于安全考虑,我们需要后端返回的图片只允许在某个网站内展示,不想被爬虫拿到图片地址后被下载,或者,不想浏览器直接访问图片链接,出于性能考虑,不想要别人的网站,拿着我们的图片链接去展示,所以本文给大家介绍了SpringBoot实现图片防盗链的五种方式

什么是图片防盗链?

想象一下,你的网站有一张超可爱的猫咪图片(/images/cute_cat.jpg),但某天发现别的网站直接用 <img src="http://yourdomain.com/images/cute_cat.jpg"> 把你的图偷走了!这就是盗链——别人不劳而获,占用你服务器的流量和资源。

防盗链的核心思想

场景重现

你运行的Spring Boot服务突然流量暴增,但发现90%请求来自第三方网站。这时候你会不会想:“难道我的猫咪图成了别人的广告位?”

实现方式对比:5种方法深度解析

方法优点缺点
过滤器(Filter)全局拦截,适合静态资源无法处理复杂逻辑
拦截器(Interceptor)可访问Spring上下文仅限MVC请求
Nginx配置性能高,无需代码不灵活
签名URL安全性强增加复杂度
混合策略多层防护配置复杂

方法1:过滤器(Filter)实现防盗链

1. 创建Spring Boot项目

添加必要依赖(pom.xml):

<dependencies>
    <!-- Web支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- 缓存支持(可选) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>

    <!-- Lombok(简化代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

2. 配置防盗链参数(application.yml)

# 防盗链配置
anti-hotlink:
  # 是否启用防盗链
  enabled: true
  # 允许的域名列表(支持子域名和正则)
  allowed-domains:
    - localhost
    - 127.0.0.1
    - "*.example.com"
    - "^test\\d+\\.domain\\.com$"  # 匹配test1.domain.com等
  # 需要保护的资源格式(结尾匹配)
  protected-formats:
    - .jpg
    - .jpeg
    - .png
    - .gif
  # 是否允许直接访问(无Referer)
  allow-direct-access: true
  # 拒绝访问时的动作(REDIRECT/FORBIDDEN/DEFAULT_IMAGE)
  deny-action: DEFAULT_IMAGE
  # 默认图片路径
  default-image: /images/no-hotlinking.png
  # 白名单路径(无需检查)
  whitelist-paths:
    - /api/public/**
    - /images/public/**

3. 编写过滤器

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
import java.util.regex.Pattern;

/**
 * 图片防盗链过滤器
 */
@Component
@Slf4j
public class AntiHotlinkFilter implements Filter {

    // 从配置中读取参数
    @Value("${anti-hotlink.enabled}")
    private boolean enabled;

    @Value("${anti-hotlink.allowed-domains}")
    private List<String> allowedDomains;

    @Value("${anti-hotlink.protected-formats}")
    private List<String> protectedFormats;

    @Value("${anti-hotlink.allow-direct-access}")
    private boolean allowDirectAccess;

    @Value("${anti-hotlink.deny-action}")
    private String denyAction;

    @Value("${anti-hotlink.default-image}")
    private String defaultImage;

    @Value("${anti-hotlink.whitelist-paths}")
    private List<String> whitelistPaths;

    // 路径匹配工具
    private final AntPathMatcher pathMatcher = new AntPathMatcher();

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        if (!enabled) {
            chain.doFilter(request, response);
            return;
        }

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        String requestURI = httpRequest.getRequestURI();
        String referer = httpRequest.getHeader("Referer");

        // 检查是否在白名单中
        if (isWhitelisted(requestURI)) {
            chain.doFilter(request, response);
            return;
        }

        // 检查是否是受保护的资源格式
        if (!isProtectedResource(requestURI)) {
            chain.doFilter(request, response);
            return;
        }

        // 直接访问(无Referer)且允许
        if (allowDirectAccess && referer == null) {
            chain.doFilter(request, response);
            return;
        }

        // 检查Referer是否合法
        if (isValidReferer(referer)) {
            chain.doFilter(request, response);
        } else {
            handleInvalidRequest(httpResponse);
        }
    }

    /**
     * 检查路径是否在白名单中
     */
    private boolean isWhitelisted(String requestURI) {
        for (String path : whitelistPaths) {
            if (pathMatcher.match(path, requestURI)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 检查是否是受保护的资源格式
     */
    private boolean isProtectedResource(String requestURI) {
        for (String format : protectedFormats) {
            if (requestURI.endsWith(format)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 检查Referer是否合法
     */
    private boolean isValidReferer(String referer) {
        if (referer == null) {
            return false;
        }

        for (String domain : allowedDomains) {
            if (domain.startsWith("^")) {
                // 正则匹配
                Pattern pattern = Pattern.compile(domain.substring(1));
                if (pattern.matcher(referer).matches()) {
                    return true;
                }
            } else if (domain.equals("*")) {
                // 通配符匹配
                return true;
            } else if (referer.contains(domain)) {
                // 精确匹配
                return true;
            }
        }
        return false;
    }

    /**
     * 处理非法请求
     */
    private void handleInvalidRequest(HttpServletResponse response) throws IOException {
        switch (denyAction) {
            case "REDIRECT":
                response.sendRedirect("https://example.com/forbidden");
                break;
            case "FORBIDDEN":
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
                break;
            case "DEFAULT_IMAGE":
                response.sendRedirect(defaultImage);
                break;
            default:
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
        }
    }
}

代码解析

方法2:拦截器(Interceptor)实现

1. 创建拦截器类

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 图片防盗链拦截器
 */
@Component
public class AntiHotlinkInterceptor implements HandlerInterceptor {

    // 配置参数(需从配置文件注入)
    private final List<String> allowedDomains;
    private final List<String> protectedFormats;
    private final boolean allowDirectAccess;
    private final String denyAction;
    private final String defaultImage;

    public AntiHotlinkInterceptor(
            List<String> allowedDomains,
            List<String> protectedFormats,
            boolean allowDirectAccess,
            String denyAction,
            String defaultImage) {
        this.allowedDomains = allowedDomains;
        this.protectedFormats = protectedFormats;
        this.allowDirectAccess = allowDirectAccess;
        this.denyAction = denyAction;
        this.defaultImage = defaultImage;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        String referer = request.getHeader("Referer");

        // 检查是否是受保护的资源格式
        if (!isProtectedResource(requestURI)) {
            return true;
        }

        // 直接访问(无Referer)且允许
        if (allowDirectAccess && referer == null) {
            return true;
        }

        // 检查Referer是否合法
        if (isValidReferer(referer)) {
            return true;
        } else {
            handleInvalidRequest(response);
            return false;
        }
    }

    private boolean isProtectedResource(String requestURI) {
        for (String format : protectedFormats) {
            if (requestURI.endsWith(format)) {
                return true;
            }
        }
        return false;
    }

    private boolean isValidReferer(String referer) {
        if (referer == null) {
            return false;
        }

        for (String domain : allowedDomains) {
            if (domain.startsWith("^")) {
                // 正则匹配
                Pattern pattern = Pattern.compile(domain.substring(1));
                if (pattern.matcher(referer).matches()) {
                    return true;
                }
            } else if (domain.equals("*")) {
                // 通配符匹配
                return true;
            } else if (referer.contains(domain)) {
                // 精确匹配
                return true;
            }
        }
        return false;
    }

    private void handleInvalidRequest(HttpServletResponse response) throws IOException {
        switch (denyAction) {
            case "REDIRECT":
                response.sendRedirect("https://example.com/forbidden");
                break;
            case "FORBIDDEN":
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
                break;
            case "DEFAULT_IMAGE":
                response.sendRedirect(defaultImage);
                break;
            default:
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
        }
    }
}

方法3:Nginx配置防盗链

如果你使用Nginx作为反向代理,可以更高效地实现防盗链:

location ~* \.(jpg|jpeg|png|gif)$ {
    # 限制Referer
    valid_referers none blocked example.com *.example.com ~\.example\.com$;
    if ($invalid_referer) {
        rewrite ^/images/(.*)$ /images/no-hotlinking.png last;
    }
}

性能对比

方法4:签名URL(Token验证)

1. 生成带Token的URL

import java.security.MessageDigest;
import java.time.Instant;
import java.util.Base64;

public class TokenGenerator {

    private static final String SECRET_KEY = "your-secret-key";
    private static final int TTL_SECONDS = 300; // 5分钟过期

    public static String generateSignedUrl(String filePath) {
        long timestamp = Instant.now().getEpochSecond();
        String token = generateToken(filePath, timestamp);
        return "https://yourdomain.com" + filePath + "?token=" + token + "&ts=" + timestamp;
    }

    private static String generateToken(String filePath, long timestamp) {
        try {
            String input = filePath + SECRET_KEY + timestamp;
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] digest = md.digest(input.getBytes());
            return Base64.getEncoder().encodeToString(digest);
        } catch (Exception e) {
            throw new RuntimeException("Token generation failed", e);
        }
    }

    public static boolean validateToken(String filePath, String token, long timestamp) {
        return generateToken(filePath, timestamp).equals(token);
    }
}

方法5:混合策略(Filter + Token)

1. 修改过滤器逻辑

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    HttpServletRequest httpRequest = (HttpServletRequest) request;
    HttpServletResponse httpResponse = (HttpServletResponse) response;

    String requestURI = httpRequest.getRequestURI();
    String referer = httpRequest.getHeader("Referer");

    // 检查是否是签名请求
    if (isSignedRequest(httpRequest)) {
        chain.doFilter(request, response);
        return;
    }

    // 其余逻辑同方法1
}

注意事项与优化建议

1. Referer不可靠

2. 缓存优化

@Cacheable("hotlink_domains")
private boolean isAllowedDomain(String domain) {
    // 缓存域名检查结果
    return allowedDomains.contains(domain);
}

3. 白名单陷阱

以上就是SpringBoot实现图片防盗链的五种方式详解的详细内容,更多关于SpringBoot图片防盗链的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文