Spring Boot 实现图片防盗链:Referer 校验与 Token 签名校验完整指南

Spring Boot 实现图片防盗链教程(Referer 校验 + Token 签名校验)

本文将详细讲解两种防盗链实现方案,并提供完整代码示例。


方案一:Referer 校验

通过检查 HTTP 请求头中的 Referer 字段判断来源是否合法。

实现步骤
  1. 创建 Referer 拦截器

    java 复制代码
    @Component
    public class RefererInterceptor implements HandlerInterceptor {
    
        private final List<String> allowedDomains = Arrays.asList(
            "https://yourdomain.com", 
            "https://trusted-site.com"
        );
    
        @Override
        public boolean preHandle(HttpServletRequest request, 
                                 HttpServletResponse response, 
                                 Object handler) throws Exception {
            
            // 获取 Referer 头
            String referer = request.getHeader("Referer");
            
            // 允许直接访问(如浏览器地址栏输入)
            if (referer == null) return true;
            
            // 验证 Referer 是否在白名单
            boolean isValid = allowedDomains.stream()
                    .anyMatch(domain -> referer.startsWith(domain));
            
            if (!isValid) {
                response.sendError(403, "Forbidden: Invalid Referer");
                return false;
            }
            return true;
        }
    }
  2. 注册拦截器到 Spring MVC

    java 复制代码
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
        @Autowired
        private RefererInterceptor refererInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            // 拦截图片路径
            registry.addInterceptor(refererInterceptor)
                    .addPathPatterns("/images/**");
        }
    }
  3. 测试效果

  • 合法访问:<img src="https://yourdomain.com/images/cat.jpg">

  • 盗链访问:<img src="https://yourdomain.com/images/cat.jpg" 在其他网站使用时返回 403


方案二:Token 签名校验

通过动态生成的签名 token 验证请求合法性(更安全)。

实现原理
  1. 生成图片 URL 时添加参数:/images/cat.jpg?t=时间戳&sign=签名

  2. 服务器验证签名和时间戳有效性

实现步骤
  1. 生成签名工具类(用非对称加密RSA更安全)

    java 复制代码
    public class TokenUtil {
    
        private static final String SECRET_KEY = "your_secret_123!";
        private static final long EXPIRE_SECONDS = 300; // 5分钟有效期
    
        // 生成带签名的URL
        public static String generateSignedUrl(String imagePath) {
            long timestamp = System.currentTimeMillis() / 1000;
            String sign = generateSign(imagePath, timestamp);
            return imagePath + "?t=" + timestamp + "&sign=" + sign;
        }
    
        // 生成签名 (使用HMAC-SHA256)
        private static String generateSign(String path, long timestamp) {
            String data = path + "|" + timestamp;
            return HmacUtils.hmacSha256Hex(SECRET_KEY, data);
        }
    
        // 验证签名
        public static boolean verifySign(String path, long timestamp, String sign) {
            // 检查过期时间
            long current = System.currentTimeMillis() / 1000;
            if (current - timestamp > EXPIRE_SECONDS) return false;
            
            // 验证签名
            String expectedSign = generateSign(path, timestamp);
            return expectedSign.equals(sign);
        }
    }
  2. 创建 Token 校验拦截器

    java 复制代码
    @Component
    public class TokenInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request, 
                                 HttpServletResponse response, 
                                 Object handler) throws Exception {
            
            String imagePath = request.getRequestURI();
            String sign = request.getParameter("sign");
            String timestampStr = request.getParameter("t");
    
            // 参数缺失检查
            if (sign == null || timestampStr == null) {
                response.sendError(400, "Missing token parameters");
                return false;
            }
    
            try {
                long timestamp = Long.parseLong(timestampStr);
                if (!TokenUtil.verifySign(imagePath, timestamp, sign)) {
                    response.sendError(403, "Invalid token");
                    return false;
                }
            } catch (NumberFormatException e) {
                response.sendError(400, "Invalid timestamp format");
                return false;
            }
            return true;
        }
    }
  3. 注册拦截器

    java 复制代码
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
        @Autowired
        private TokenInterceptor tokenInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(tokenInterceptor)
                    .addPathPatterns("/images/**");
        }
    }
  4. 生成安全链接的 Controller

    java 复制代码
    @RestController
    public class ImageController {
    
        @GetMapping("/getImageUrl")
        public String getImageUrl(@RequestParam String imageName) {
            String imagePath = "/images/" + imageName;
            return TokenUtil.generateSignedUrl(imagePath);
        }
    }
  5. 前端使用示例

    html 复制代码
    <!-- 前端先请求获取合法链接 -->
    <script>
    fetch('/getImageUrl?imageName=cat.jpg')
      .then(res => res.text())
      .then(url => {
        const img = document.createElement('img');
        img.src = url;
        document.body.appendChild(img);
      });
    </script>

两种方案对比
特性 Referer 校验 Token 校验
安全性 中(Referer 可伪造) 高(需破解签名算法)
实现复杂度 简单 中等
链接有效期 永久 可控制时效
跨浏览器支持 部分浏览器禁用 Referer 无兼容问题
防盗链效果 可防普通盗链 可防高级盗链

增强方案:双验证结合
java 复制代码
// 在拦截器中组合验证
public boolean preHandle(...) {
    // 先验证 Referer
    if (!refererValid) {
        // 再验证 Token(给合法合作方提供Token访问方式)
        if (!tokenValid) {
            response.sendError(403);
            return false;
        }
    }
    return true;
}
注意事项
  1. Referer 校验的局限性

    • 浏览器可能不发送 Referer(如HTTPS->HTTP)

    • 可通过 <meta name="referrer" content="no-referrer"> 绕过

  2. Token 校验最佳实践

    • 使用 HTTPS 防止 Token 被截获

    • 定期轮换密钥

    • 对 IP 进行访问频率限制