开发者

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

开发者 https://www.devze.com 2025-08-19 10:29 出处:网络 作者: 墨瑾轩
目录什么是图片防盗链?实现方式对比:5种方法深度解析方法1:过滤器(Filter)实现防盗链1. 创建Spring Boot项目2. 配置防盗链参数(application.yml)3. 编写过滤器方法2:拦截器(Interceptor)实现1. 创建拦截器
目录
  • 什么是图片防盗链?
  • 实现方式对比:5种方法深度解析
  • 方法1:过滤器(Filter)实现防盗链
    • 1. 创建Spring Boot项目
    • 2. 配置防盗链参数(application.yml)
    • 3. 编写过滤器
  • 方法2:拦截器(Interceptor)实现
    • 1. 创建拦截器类
  • 方法3:Nginx配置防盗链
    • 方法4:签名URL(Token验证)
      • 1. 生成带Token的URL
    • 方法5:混合策略(Filter + Token)
      • 1. 修改过滤器逻辑
    • 注意事项与优化建议
      • 1. Referer不可靠
      • 2. 缓存优化
      • 3. 白名单陷阱

    什么是图片防盗链?

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

    防盗链的核心思想

    • 检查请求来源(Referer):只允许指定域名的请求访问资源。
    • 白名单机制:某些资源可以完全开放访问。
    • 默认图片兜底:拒绝请求时返回一张“禁止盗链”的提示图。

    场景重现

    你运行的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 booleanandroid isWhitelisted(String requestURI) {
            for (String path : whitelistPaths) {
                if (pathMatcher.yOQTgdXkmatch(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 pattjsern = 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");
            }
        }
    }
    

    代码解析

    • doFilter:核心逻辑入口,检查请求是否合法。
    • isWhitelisted:判断路径是否在白名单中。
    • isValidReferer:支持正则、通配符和精确匹配。
    • handleInvalidRequest:根据配置返回不同响应。

    方法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;
        }
    }
    

    性能对比

    • Nginx:毫秒级响应,无需Java处理
    • Java过滤器:延迟约10ms

    方法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 validateTokenhttp://www.devze.com(String filePath, String token, long timestamp) {
            return generateToken(filePath, timestamp).equals(token);
    php    }
    }
    

    方法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不可靠

    • 伪造问题:恶意客户端可伪造Referer头。
    • 解决方案:结合Token验证或Nginx配置。

    2. 缓存优化

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

    3. 白名单陷阱

    • 路径匹配漏洞/api/public/** 可能被绕过。
    • 解决方案:使用严格路径匹配规则。

    以上就是SpringBoot实现图片防盗链的五种方式详解的详细内容,更多关于SpringBoot图片防盗链的资料请关注编程客栈(www.devze.com)其它相关文章!

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号