在 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;
@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();
}
}
- 配置过滤器:在 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)的防盗链
此方法为每个请求生成一个唯一的令牌,并在请求时验证令牌的有效性。
以下是实现步骤和示例代码:
- 创建令牌生成和验证工具类:
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)的防盗链:安全性较高,但实现相对复杂,需要处理令牌的生成和验证逻辑。