ARTICLE · 1136850
源码CSR?
企业的软件研发过程管控相当严格,每个阶段生成相应的文件并经过审批,连测试过程都有人工排查后门程序……那么又开始丧心病狂的审核模式,查安全编码一般是最容易找到问题的。可是我没做过java开发,没吃过这猪肉,就看看猪跑呗。
在源代码中防止CSRF攻击,主要有两种现代且可靠的方式:一是使用CSRF Token,二是设置Cookie的SameSite属性。它们的工作原理不同,但可以互相补充,构建起有效的防御体系。
核心方法一:CSRF Token(最主流的方式)
这是目前最通用、最可靠的防护手段。其核心思想是,服务器生成一个无法被攻击者猜到的随机令牌(Token),并将其与当前用户的会话(Session)绑定。之后,任何修改状态的请求都必须携带这个合法的Token,否则将被服务器拒绝。
如何工作?
1. 生成:用户登录后,服务器生成一个随机的CSRF Token,并存储在Session中。
2. 传递:服务器将这个Token嵌入到需要提交的表单(作为隐藏字段)或AJAX请求的响应头中,发送给前端。
3. 验证:当用户提交请求时,服务器会提取请求中的Token,并与Session中存储的Token进行比较。如果两者不一致或缺失,请求将被视为非法并拒绝。
代码实现参考:
前端 (在表单中嵌入Token):将Token作为一个隐藏的`<input>`元素放在表单中,随表单一起提交。
```html
<form action="/user/password/update" method="post">
<!-- 使用后端渲染的session中的csrfToken值 -->
<input type="hidden" name="csrfToken" value="${sessionScope.csrfToken}">
<div class="form-item">
<label>新密码:</label>
<input type="password" name="newPassword">
</div>
<button type="submit">提交修改</button>
</form>
前端 (在AJAX请求中携带Token):可以从Cookie或DOM中读取Token,并将其放入请求头(如`X-CSRF-Token`)中。
```javascript
// 以Axios为例,通过请求拦截器统一添加
axios.interceptors.request.use(config => {
const csrfToken = localStorage.getItem('csrfToken'); // 或者从Cookie/页面meta中获取
if (csrfToken) {
config.headers['X-CSRF-Token'] = csrfToken;
}
return config;
});
后端 (Spring Boot拦截器验证示例):这是后端验证的核心逻辑,校验请求中的Token是否与会话中的一致。
```java
@Component
public class CsrfInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 从请求中提取Token(支持表单参数或请求头)
String requestToken = request.getParameter("csrfToken");
if (requestToken == null) {
requestToken = request.getHeader("X-CSRF-Token");
}
// 2. 从Session中获取服务器存储的Token
HttpSession session = request.getSession();
String serverToken = (String) session.getAttribute("csrfToken");
// 3. 比较Token
if (requestToken == null || serverToken == null || !requestToken.equals(serverToken)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("CSRF Token验证失败,请求被拒绝");
return false;
}
// 验证成功,放行
return true;
}
}
```
安全要点:Token必须是高强度的随机字符串(如使用`java.util.UUID`或加密安全的随机数生成器)。在服务端比较Token时,应使用恒定时间比较算法(例如Java中的`MessageDigest.isEqual()`或Go/Python中的`hmac.compare_digest`),以防止计时攻击(Timing Attack)。
核心方法二:利用Cookie的SameSite属性(浏览器侧防护)
这是一种较新的、由浏览器提供支持的防御方式。通过在设置会话Cookie时添加`SameSite`属性,可以控制浏览器是否在跨站请求时发送该Cookie,从而直接阻止CSRF攻击发起的条件。
```http
Set-Cookie: sessionId=abcd1234; Secure; HttpOnly; SameSite=Lax; Path=/
```
`SameSite=Lax` (推荐):大部分现代浏览器的默认值。它允许Cookie在用户通过点击链接进行顶级导航(如从A站点点击链接跳转到B站点)时发送,但会阻止来自其他站点`<img>`、`<script>`或`<form>`提交等子请求携带Cookie。这在保证安全的同时,对用户体验影响最小。
`SameSite=Strict` (最严格):浏览器将完全禁止任何跨站请求携带此Cookie。安全性最高,但可能会影响用户从外部站点点击链接正常登录的体验。
`SameSite=None`:允许在所有跨站请求中携带Cookie,必须与`Secure`(即HTTPS)属性一同使用。仅在你明确需要跨站请求携带Cookie的场景下使用。
注意啊,这是java开发框架内置功能,避免重复造轮子。
绝大多数现代Web框架都已内置或通过扩展库提供了完善的CSRF防护机制。直接使用它们是最简单、最安全的选择。
.NET Core:
自动CSRF中间件:从.NET 11开始,`WebApplication.CreateBuilder`会自动启用一个基于请求头(`Sec-Fetch-Site`, `Origin`)的CSRF防护中间件。对于现代浏览器,它无需额外的Token即可生效,保护了大部分表单提交场景。
基于Token的防伪系统:可以使用`app.UseAntiforgery()`和`[AutoValidateAntiforgeryToken]`属性,这是.NET中的传统也是更显式的Token校验方式。两者可以共存,形成纵深防御。
Java (Spring Security):从Spring Security 4.0起,CSRF保护默认是开启的。它会自动为所有非`GET`请求(如POST, PUT, DELETE)进行Token验证。你可以在前端通过`${_csrf.token}`获取Token,并在表单或AJAX请求头中传递。
PHP:虽然没有自动机制,但手动实现非常简单,核心就是使用`session_start()`创建会话,并用`bin2hex(random_bytes(32))`生成安全的随机Token,存放在`$_SESSION`变量中,并在表单提交时进行比对。
Go:
GoFrame框架:社区提供了`github.com/gogf/csrf`中间件,可以通过配置文件快速集成,实现Token的自动生成和校验。
标准库/Gin等:可以使用像`github.com/gorilla/csrf`或`github.com/utrack/gin-csrf`这样的流行库,它们实现了`SameSite`和基于Token的防护。
Python (FastAPI等):可以通过自定义中间件或依赖项实现。IBM提供的示例代码展示了如何生成和验证基于HMAC-SHA256的CSRF Token,其中包含用户ID和会话ID绑定,并实现了时间窗口验证。
Bun (JavaScript运行时):提供了内置的`Bun.CSRF` API,可以方便地生成和验证签名Token,并且强烈建议在生成和验证时都传递`sessionId`,将Token与当前用户绑定,防止Token被其他用户复用。
关键安全原则总结:
1. 优先使用框架内置方案:避免重复实现安全逻辑,出错的概率极高。
2. 双重保险:对于关键应用,可以同时开启Token验证和设置Cookie的`SameSite=Lax`,形成纵深防御。
3. Token绑定会话:CSRF Token必须与当前用户会话(Session)或用户ID强绑定,否则一个Token可能被所有用户共用,导致防护失效。
4. 区分API场景:对于仅使用Bearer Token(如JWT)或API Key进行认证的API,由于攻击者无法获取这个自定义头信息,通常可以安全地禁用CSRF防护。但若API同时依赖Cookie,则必须启用防护。
猪跑看了一半,最后落点还是数据库连接密码明码、数据库地址Ip地址、加密用md5……终于拔了一根猪毛下来。