在互联网应用中,安全性问题是开发者必须时刻关注的核心内容之一。跨站请求伪造(Cross-Site Request Forgery, CSRF),是一种常见的Web安全漏洞。通过CSRF攻击,黑客可以冒用受害者的身份,发送恶意请求,执行诸如转账、订单提交等操作,导致严重的安全后果。
本文将详细讲解CSRF攻击的原理及其防御方法,结合电商交易系统的场景给出错误和正确的示范代码,并分析常见的安全问题与解决方案,帮助开发者全面理解和防御CSRF攻击。
CSRF攻击是指黑客通过欺骗用户在不知情的情况下向受信任的服务器发送请求,从而执行用户并未授权的操作。由于浏览器的同源策略,浏览器会自动携带当前登录用户的身份凭证(如Cookie),导致服务器误以为请求是合法用户发出的。
常见的CSRF攻击流程如下:
为了更直观地理解CSRF攻击的危害,我们以电商交易系统为例,演示错误的代码实现以及如何修复它。
在一个简单的电商交易系统中,用户可以通过提交订单来购买商品。假设服务器端的订单提交接口是通过POST请求进行的,代码如下:
// 订单提交控制器
@PostMapping("/submitOrder")
public String submitOrder(@RequestParam("productId") String productId,
@RequestParam("quantity") int quantity,
HttpSession session) {
// 获取当前用户信息
User user = (User) session.getAttribute("currentUser");
// 创建订单
Order order = new Order();
order.setUserId(user.getId());
order.setProductId(productId);
order.setQuantity(quantity);
// 保存订单到数据库
orderService.saveOrder(order);
return "orderSuccess";
}
这种实现存在明显的安全问题:攻击者可以诱导用户访问恶意链接,从而提交伪造的订单。
例如,攻击者可以构造如下HTML页面,并诱导用户点击:
如果用户在登录状态下点击了该页面的提交按钮,电商系统将生成一个伪造的订单,而用户对此一无所知。
在实际的电商系统中,攻击者可能会诱导用户执行更为严重的操作,比如修改收货地址、提交高价商品订单等。这些操作可以通过隐藏的表单字段自动完成,用户根本不需要手动提交。
防御CSRF攻击的核心在于服务器能够验证每一个请求的合法性。一般来说,CSRF防御的主要手段有:
SameSite
属性,限制跨站点的请求携带Cookie。Token校验是防护CSRF攻击最常用且最有效的方法。它的工作原理是:
以下是如何通过Token防护CSRF攻击的示例。
首先,我们需要为每一个请求生成唯一的Token,并在提交时携带该Token。以下是Spring Boot中的CSRF防护机制的实现。
生成CSRF Token
Spring Security默认提供了CSRF防护机制。我们可以通过以下配置启用它:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf() // 开启CSRF防护
.and()
.authorizeRequests()
.antMatchers("/submitOrder").authenticated() // 需要登录
.and()
.formLogin().loginPage("/login").permitAll();
}
}
Spring Security会自动为每个页面生成一个CSRF Token,并将该Token嵌入到页面中的隐藏字段或HTTP头中。
在表单中添加CSRF Token
Spring Security会在每个表单中自动包含一个CSRF Token。表单代码如下:
服务器端验证CSRF Token
当用户提交表单时,Spring Security会自动验证CSRF Token。如果Token验证失败,将会抛出异常,阻止请求的执行。
在 CSRF 防护机制中,限制 Token 的有效期可以通过以下步骤实现:
在生成 CSRF Token 时,我们可以在 Token 中附加一个时间戳来记录生成时间。例如,可以通过 base64 编码将随机生成的 Token 和当前时间戳一起组合。
import java.util.Base64;
import java.util.Date;
public class CsrfTokenGenerator {
private static final long TOKEN_VALIDITY = 5 * 60 * 1000; // Token 有效期为 5 分钟
// 生成带时间戳的 CSRF Token
public static String generateCsrfToken() {
String token = generateRandomToken(); // 生成随机Token
long timestamp = System.currentTimeMillis(); // 获取当前时间戳
String tokenWithTimestamp = token + ":" + timestamp;
return Base64.getEncoder().encodeToString(tokenWithTimestamp.getBytes()); // Base64 编码
}
private static String generateRandomToken() {
// 此处生成随机 Token,简单示例为随机UUID
return java.util.UUID.randomUUID().toString();
}
}
在服务器端对 Token 进行验证时,除了常规的 Token 匹配,还需要校验时间戳,确保 Token 在有效期内。
import java.util.Base64;
public class CsrfTokenValidator {
private static final long TOKEN_VALIDITY = 5 * 60 * 1000; // 5 分钟有效期
public static boolean validateCsrfToken(String token) {
try {
// 解码 Token
String decodedToken = new String(Base64.getDecoder().decode(token));
String[] parts = decodedToken.split(":");
if (parts.length != 2) {
return false; // Token 格式错误
}
String csrfToken = parts[0]; // 获取CSRF Token
long timestamp = Long.parseLong(parts[1]); // 获取时间戳
long currentTime = System.currentTimeMillis();
if (currentTime - timestamp > TOKEN_VALIDITY) {
return false; // Token 已过期
}
// 验证Token本身的正确性(与Session中的Token对比)
return csrfToken.equals(getStoredToken()); // 假设getStoredToken()获取服务器端存储的Token
} catch (Exception e) {
return false; // 解码或校验失败
}
}
private static String getStoredToken() {
// 这里从服务器Session或者数据库中获取已存储的CSRF Token
return "stored-token-example";
}
}
在电商交易系统的具体示例中,假设用户进行购物车的结算操作。我们可以通过 CSRF Token 限制请求的有效期,防止攻击者在很久之前窃取的 Token 被再次利用。
前端发送请求:
function checkout() {
let csrfToken = getCookie('CSRF-TOKEN');
fetch('/checkout', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken, // 包含CSRF Token
'Content-Type': 'application/json'
},
body: JSON.stringify({
productId: 123,
amount: 1
})
}).then(response => {
if (response.status === 403) {
alert('CSRF Token 过期或无效,请刷新页面再试');
}
});
}
服务器端验证:
在服务器端,通过 CsrfTokenValidator
对 Token 进行验证,确保其没有过期。
@PostMapping("/checkout")
public ResponseEntity checkout(@RequestHeader("X-CSRF-TOKEN") String csrfToken) {
if (!CsrfTokenValidator.validateCsrfToken(csrfToken)) {
return new ResponseEntity<>("CSRF Token 无效或已过期", HttpStatus.FORBIDDEN);
}
// 处理购物车结算逻辑
return new ResponseEntity<>("结算成功", HttpStatus.OK);
}
如果用户的 CSRF Token 过期,前端可以通过一个特定的 API 进行 Token 刷新,重新获取有效的 CSRF Token。
@GetMapping("/refreshCsrfToken")
public ResponseEntity refreshCsrfToken(HttpServletResponse response) {
String newToken = CsrfTokenGenerator.generateCsrfToken();
Cookie csrfCookie = new Cookie("CSRF-TOKEN", newToken);
csrfCookie.setPath("/");
csrfCookie.setHttpOnly(true);
response.addCookie(csrfCookie);
return new ResponseEntity<>("CSRF Token 已刷新", HttpStatus.OK);
}
另一种防护CSRF攻击的方式是检查HTTP请求头中的Referer字段,验证请求是否来自同一个网站。虽然Referer验证并不是100%可靠(因为可能被用户代理修改),但可以作为一种补充手段。
我们可以通过Spring Security提供的Referer验证器实现这一机制:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.requireCsrfProtectionMatcher(new AntPathRequestMatcher("/submitOrder"))
.and()
.authorizeRequests()
.antMatchers("/submitOrder").authenticated();
}
通过这种方式,服务器会检查请求的来源,如果请求的来源不是本站域名,则拒绝执行该请求。
通过设置 cookie 的 SameSite
属性,可以防止跨站点请求时浏览器发送 cookie,从而减少 CSRF 攻击的风险。
SameSite=Lax
: 允许导航链接发送 cookie,但跨站点 POST 请求不会发送 cookie。SameSite=Strict
: 完全禁止跨站点的请求发送 cookie,最大限度地防御 CSRF。Cookie 设置示例:
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(new CsrfFilter(csrfTokenRepository), UsernamePasswordAuthenticationFilter.class)
.headers()
.frameOptions().sameOrigin()
.httpStrictTransportSecurity()
.includeSubDomains(true)
.and()
.cookie()
.sameSite(SameSiteCookieAttributeValue.LAX);
本文详细介绍了CSRF攻击的原理和危害,并通过错误与正确示范,演示了如何防护CSRF攻击。CSRF攻击是一种常见且危险的安全漏洞,开发者应时刻保持警惕,采用诸如Token验证、Referer检查、SameSite Cookie等多层防护手段,确保Web应用的安全性。
通过掌握这些防护技巧,开发者可以有效抵御CSRF攻击,保护用户的个人信息与财产安全。在实际开发中,安全问题应始终放在优先位置,只有不断优化和完善,才能打造出安全、可靠的应用系统。