在 Spring Boot 项目中实现防盗链可以通过多种方式,下面为你介绍两种常见的实现方法,分别是基于请求头 Referer 和基于令牌(Token)的防盗链。
基于请求头 Referer 的防盗链
这种方法通过检查请求头中的 Referer 字段,判断请求是否来自合法的来源。如果不是,则拒绝该请求。
以下是实现步骤和示例代码:
- 创建一个过滤器:用于拦截请求并检查 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;@Overridepublic void init(FilterConfig filterConfig) throws ServletException {String refererConfig = filterConfig.getInitParameter("allowedReferers");if (Objects.nonNull(refererConfig)) {allowedReferers = Arrays.asList(refererConfig.split(","));}}@Overridepublic 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");}}@Overridepublic void destroy() {Filter.super.destroy();}
}
- 配置过滤器:在 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 {@Beanpublic FilterRegistrationBean<AntiLeachingFilter> filterRegistrationBean() {FilterRegistrationBean<AntiLeachingFilter> registration = new FilterRegistrationBean<>();registration.setFilter(new AntiLeachingFilter());registration.addInitParameter("allowedReferers", "127.0.0.1");registration.addUrlPatterns("/*");return registration;}}
基于令牌(Token)的防盗链
此方法为每个请求生成一个唯一的令牌,并在请求时验证令牌的有效性。
以下是实现步骤和示例代码:
- 创建令牌生成和验证工具类:
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);}}
- 创建控制器处理请求并验证
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)的防盗链:安全性较高,但实现相对复杂,需要处理令牌的生成和验证逻辑。