Spring Boot 实现防盗链

发布于:2025-04-17 ⋅ 阅读:(73) ⋅ 点赞:(0)

在 Spring Boot 项目中实现防盗链可以通过多种方式,下面为你介绍两种常见的实现方法,分别是基于请求头 Referer 和基于令牌(Token)的防盗链。

基于请求头 Referer 的防盗链

这种方法通过检查请求头中的 Referer 字段,判断请求是否来自合法的来源。如果不是,则拒绝该请求。
以下是实现步骤和示例代码:

  1. 创建一个过滤器:用于拦截请求并检查 Referer 字段。
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

public class AntiLeachingFilter implements Filter {

    private List<String> allowedReferers;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        String refererConfig = filterConfig.getInitParameter("allowedReferers");
        if (Objects.nonNull(refererConfig)) {
            allowedReferers = Arrays.asList(refererConfig.split(","));
        }
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;

        String referer = httpServletRequest.getHeader("Referer");
        boolean isAllowed = false;

        if (Objects.isNull(referer)) {
            isAllowed = false;
        } else {
            for (String allowedReferer : allowedReferers) {
                if (referer.contains(allowedReferer)) {
                    isAllowed = true;
                    break;
                }
            }
        }

        if (isAllowed) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        } else {
            httpServletResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
        }
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}
  1. 配置过滤器:在 Spring Boot 中注册该过滤器。
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<AntiLeachingFilter> filterRegistrationBean() {
        FilterRegistrationBean<AntiLeachingFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new AntiLeachingFilter());
        registration.addInitParameter("allowedReferers", "127.0.0.1");
        registration.addUrlPatterns("/*");
        return registration;
    }

}

基于令牌(Token)的防盗链

此方法为每个请求生成一个唯一的令牌,并在请求时验证令牌的有效性。
以下是实现步骤和示例代码:

  1. 创建令牌生成和验证工具类:
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;

public class TokenGeneratorUtils {

    private static final String SECRET_KEY = "4t3t35y546yertdgsr3w4";

    /**
     * 生成用于防盗链的令牌(Token)。
     * 该方法通过将资源路径、过期时间和密钥拼接后进行 SHA-256 哈希计算,
     * 最终将计算得到的哈希值转换为十六进制字符串作为令牌返回。
     *
     * @param resourcePath  请求的资源路径,用于标识具体要访问的资源
     * @param expirationTime 令牌的过期时间,以毫秒为单位
     * @return 生成的令牌,是一个十六进制字符串
     */
    public static String generateToken(String resourcePath, long expirationTime) {
        String date = resourcePath + expirationTime + SECRET_KEY;
        try {
            // 借助 MessageDigest.getInstance("SHA-256") 方法获取一个 MessageDigest 实例,
            // 该实例使用的哈希算法为 SHA-256。SHA-256 属于安全哈希算法,能够将任意长度的输入数据转换为固定长度(256 位)的哈希值。
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            //调用 digest 方法对拼接后的字符串 data 进行哈希计算,返回一个字节数组 hash,此数组就是 data 的 SHA-256 哈希值。
            byte[] hash = digest.digest(date.getBytes());

            StringBuilder hexString = new StringBuilder();
            for (byte b : hash) {
                //字节转换为对应的十六进制字符串。
                String hex = Integer.toHexString(0xff & b);
                if (hex.length() == 1) {//转换后的十六进制字符串长度为 1,在前面补一个 0,以保证每个字节都用两位十六进制数表示。
                    hexString.append('0');
                }
                //转换后的十六进制字符串追加到 hexString 中。
                hexString.append(hex);
            }
            return hexString.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    public static boolean verifyToken(String resourcePath, String token, long expirationTime) {
        if (new Date().getTime() > expirationTime) {
            return false;
        }
        String generatedToken = generateToken(resourcePath, expirationTime);
        return generatedToken.equals(token);
    }

}
  1. 创建控制器处理请求并验证
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

@RestController
@Slf4j
public class TestController {

    /**
     * curl --location '127.0.0.1:2223/test' \
     * --header 'Referer: 127.0.0.1'
     *
     * header头不传合法的Referer 127.0.0.1则拒绝访问
     * @return
     */
    @RequestMapping("/test")
    public String test() {
        return "ok";
    }

    @GetMapping("/protectedResource")
    public ResponseEntity<String> getProtectedResource(@RequestParam String token, @RequestParam long expirationTime) {
        String resourcePath = "/protectedResource";
        if (TokenGeneratorUtils.verifyToken(resourcePath, token, expirationTime)) {
            return ResponseEntity.ok("Access granted");
        } else {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid token");
        }
    }

    public static void main(String[] args) {
        // 资源路径
        String resourcePath = "/protectedResource";
        // 设置令牌过期时间,这里设置为当前时间往后 1 小时
        long expirationTime = new Date().getTime() + 3600 * 1000;
        // 生成令牌
        String token = TokenGeneratorUtils.generateToken(resourcePath, expirationTime);
        System.out.println(token);
        System.out.println(expirationTime);

    }
}

总结

  • 基于请求头 Referer 的防盗链:实现简单,但 Referer 字段容易被伪造。
  • 基于令牌(Token)的防盗链:安全性较高,但实现相对复杂,需要处理令牌的生成和验证逻辑。