引言
CORS和CSRF是Web开发中最常见的安全问题,也是最容易配置错误的地方。错误的CORS配置会导致安全漏洞,而CSRF防护不当则可能被恶意利用。本文将深入讲解这两个安全机制的原理与实战方案。
CORS(跨域资源共享)
什么是CORS
同源策略(Same-Origin Policy):
┌─────────────────────────────────────────┐
│ 协议 + 域名 + 端口 必须完全相同 │
│ │
│ ✅ 同源: │
│ https://api.example.com:443 │
│ https://api.example.com:443 │
│ │
│ ❌ 跨域: │
│ https://api.example.com │
│ https://web.example.com │
│ │
│ https://api.example.com │
│ http://api.example.com │
│ │
│ https://api.example.com:8080 │
│ https://api.example.com:8081 │
└─────────────────────────────────────────┘
CORS作用:
允许服务器声明哪些源可以访问其资源,打破同源策略限制
CORS请求类型
简单请求(Simple Request):
┌─────────────────────────────────────────┐
│ 条件: │
│ 1. 方法:GET、POST、HEAD │
│ 2. Header仅限: │
│ - Accept │
│ - Accept-Language │
│ - Content-Language │
│ - Content-Type(仅限三种) │
│ │
│ 流程: │
│ 浏览器 ────── 请求 ──────▶ 服务器 │
│ 浏览器 ◀──── 响应 ────── 服务器 │
│ (包含CORS头) │
└─────────────────────────────────────────┘
预检请求(Preflight Request):
┌─────────────────────────────────────────┐
│ 条件:不满足简单请求条件 │
│ │
│ 流程: │
│ 1. 浏览器发送OPTIONS预检请求 │
│ ┌──────────────────────────────┐ │
│ │ OPTIONS /api/data HTTP/1.1 │ │
│ │ Origin: https://web.com │ │
│ │ Access-Control-Request- │ │
│ │ Method: PUT │ │
│ │ Access-Control-Request- │ │
│ │ Headers: X-Custom-Header │ │
│ └──────────────────────────────┘ │
│ │
│ 2. 服务器返回允许的源、方法、Header │
│ ┌──────────────────────────────┐ │
│ │ HTTP/1.1 204 No Content │ │
│ │ Access-Control-Allow-Origin: │ │
│ │ https://web.com │ │
│ │ Access-Control-Allow-Method: │ │
│ │ PUT, POST │ │
│ │ Access-Control-Allow- │ │
│ │ Headers: X-Custom-Header │ │
│ │ Access-Control-Max-Age: 86400│ │
│ └──────────────────────────────┘ │
│ │
│ 3. 浏览器发送实际请求 │
└─────────────────────────────────────────┘
CORS响应头详解
服务器响应头:
┌─────────────────────────────────────────┐
│ Access-Control-Allow-Origin │
│ 值:* 或 具体域名 │
│ 作用:允许访问的源 │
│ 注意:* 不能与 credentials 同时使用 │
│ │
│ Access-Control-Allow-Methods │
│ 值:GET, POST, PUT, DELETE... │
│ 作用:允许的HTTP方法 │
│ │
│ Access-Control-Allow-Headers │
│ 值:Content-Type, Authorization... │
│ 作用:允许的请求头 │
│ │
│ Access-Control-Allow-Credentials │
│ 值:true │
│ 作用:允许携带Cookie │
│ 注意:与 * 互斥 │
│ │
│ Access-Control-Expose-Headers │
│ 值:X-Custom-Header... │
│ 作用:允许前端访问的响应头 │
│ │
│ Access-Control-Max-Age │
│ 值:秒数(如86400) │
│ 作用:预检请求缓存时间 │
└─────────────────────────────────────────┘
Nginx CORS配置
基础配置
# nginx.conf
server {
listen 80;
server_name api.example.com;
# CORS配置
location /api/ {
# 允许的源(生产环境不要用 *)
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://web.example.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain';
return 204;
}
add_header 'Access-Control-Allow-Origin' 'https://web.example.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With';
# 代理到后端
proxy_pass http://backend;
}
}
动态多源配置
# 支持多个允许的源
map $http_origin $cors_origin {
default "";
"https://web.example.com" "https://web.example.com";
"https://admin.example.com" "https://admin.example.com";
"https://mobile.example.com" "https://mobile.example.com";
"~^https://.*\.example\.com$" "$http_origin"; # 正则匹配子域名
}
server {
listen 80;
server_name api.example.com;
location /api/ {
# 预检请求
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With, X-Custom-Header';
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
# 实际请求
if ($cors_origin != "") {
add_header 'Access-Control-Allow-Origin' $cors_origin;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With, X-Custom-Header';
}
proxy_pass http://backend;
}
}
携带Cookie的配置
server {
listen 80;
server_name api.example.com;
location /api/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://web.example.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Allow-Credentials' 'true'; # 允许Cookie
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
add_header 'Access-Control-Allow-Origin' 'https://web.example.com';
add_header 'Access-Control-Allow-Credentials' 'true'; # 允许Cookie
proxy_pass http://backend;
}
}
Spring Boot CORS配置
全局配置
@Configuration
@EnableWebMvc
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(
"https://web.example.com",
"https://admin.example.com"
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders(
"Content-Type",
"Authorization",
"X-Requested-With"
)
.exposedHeaders("X-Custom-Header")
.allowCredentials(true)
.maxAge(86400); // 预检请求缓存24小时
}
}
基于配置的动态源
@Configuration
public class DynamicCorsConfig {
@Value("${cors.allowed-origins}")
private List<String> allowedOrigins;
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(allowedOrigins);
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(Arrays.asList(
"Content-Type",
"Authorization",
"X-Requested-With"
));
config.setAllowCredentials(true);
config.setMaxAge(86400L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return new CorsFilter(source);
}
}
# application.yml
cors:
allowed-origins:
- https://web.example.com
- https://admin.example.com
- https://mobile.example.com
控制器级别配置
@RestController
@RequestMapping("/api/users")
@CrossOrigin(
origins = "https://web.example.com",
methods = {RequestMethod.GET, RequestMethod.POST},
allowedHeaders = {"Content-Type", "Authorization"},
maxAge = 3600
)
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUser(id);
}
}
Go CORS中间件
基础实现
package middleware
import (
"net/http"
"strings"
)
type CORSConfig struct {
AllowedOrigins []string
AllowedMethods []string
AllowedHeaders []string
ExposedHeaders []string
AllowCredentials bool
MaxAge int
}
var DefaultCORSConfig = CORSConfig{
AllowedOrigins: []string{"https://web.example.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization", "X-Requested-With"},
ExposedHeaders: []string{},
AllowCredentials: true,
MaxAge: 86400,
}
func CORSMiddleware(config CORSConfig) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// 检查Origin是否在允许列表中
allowed := false
for _, allowedOrigin := range config.AllowedOrigins {
if allowedOrigin == origin || allowedOrigin == "*" {
allowed = true
break
}
}
if !allowed {
next.ServeHTTP(w, r)
return
}
// 设置CORS响应头
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", strings.Join(config.AllowedMethods, ", "))
w.Header().Set("Access-Control-Allow-Headers", strings.Join(config.AllowedHeaders, ", "))
if len(config.ExposedHeaders) > 0 {
w.Header().Set("Access-Control-Expose-Headers", strings.Join(config.ExposedHeaders, ", "))
}
if config.AllowCredentials {
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
if config.MaxAge > 0 {
w.Header().Set("Access-Control-Max-Age", fmt.Sprintf("%d", config.MaxAge))
}
// 预检请求
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
// 使用示例
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/users", handleUsers)
handler := CORSMiddleware(DefaultCORSConfig)(mux)
http.ListenAndServe(":8080", handler)
}
CSRF(跨站请求伪造)
CSRF攻击原理
CSRF攻击流程:
┌─────────────────────────────────────────┐
│ 1. 用户登录银行网站 │
│ https://bank.com/login │
│ Cookie: session=abc123 │
│ │
│ 2. 用户访问恶意网站 │
│ https://evil.com │
│ 页面包含: │
│ <img src="https://bank.com/transfer │
│ ?to=attacker&amount=1000"> │
│ │
│ 3. 浏览器自动发送请求 │
│ GET https://bank.com/transfer │
│ ?to=attacker&amount=1000 │
│ Cookie: session=abc123 ◀─ 自动携带 │
│ │
│ 4. 银行服务器验证Cookie有效 │
│ 执行转账操作 │
│ │
│ 结果:用户损失1000元 │
└─────────────────────────────────────────┘
CSRF防护方案
防护方案:
┌─────────────────────────────────────────┐
│ 1. CSRF Token(推荐) │
│ └─ 服务器生成随机Token │
│ └─ 表单中隐藏字段或Header传递 │
│ └─ 服务器验证Token有效性 │
│ │
│ 2. SameSite Cookie属性 │
│ └─ Strict:完全禁止跨站发送 │
│ └─ Lax:允许部分GET跨站 │
│ └─ None:允许跨站(需Secure) │
│ │
│ 3. 验证Origin/Referer │
│ └─ 检查请求来源是否合法 │
│ │
│ 4. 双重Cookie验证 │
│ └─ Token存入Cookie和表单 │
│ └─ 服务器对比两者是否一致 │
│ │
│ 5. 关键操作二次验证 │
│ └─ 转账、修改密码等需要短信验证 │
└─────────────────────────────────────────┘
CSRF Token实现
Spring Boot CSRF配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers("/api/public/**") // 忽略公开接口
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
// 前端获取CSRF Token
// 从Cookie中读取
function getCsrfToken() {
const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/);
return match ? match[1] : '';
}
// 发送请求时携带Token
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': getCsrfToken() // 添加到Header
},
body: JSON.stringify({
to: 'user123',
amount: 100
})
});
Go CSRF中间件
package csrf
import (
"crypto/rand"
"encoding/base64"
"net/http"
"time"
)
type CSRFMiddleware struct {
cookieName string
headerName string
secure bool
}
func NewCSRFMiddleware() *CSRFMiddleware {
return &CSRFMiddleware{
cookieName: "csrf_token",
headerName: "X-CSRF-Token",
secure: true,
}
}
func (m *CSRFMiddleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 安全方法不需要验证
if r.Method == "GET" || r.Method == "HEAD" || r.Method == "OPTIONS" {
// 生成Token并设置Cookie
token := m.generateToken()
http.SetCookie(w, &http.Cookie{
Name: m.cookieName,
Value: token,
Path: "/",
HttpOnly: false, // 前端需要读取
Secure: m.secure,
SameSite: http.SameSiteStrictMode,
})
// 将Token存入上下文(供模板使用)
ctx := context.WithValue(r.Context(), "csrf_token", token)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
// 验证Token
cookieToken, err := r.Cookie(m.cookieName)
if err != nil {
http.Error(w, "CSRF token missing", http.StatusForbidden)
return
}
headerToken := r.Header.Get(m.headerName)
if headerToken == "" {
headerToken = r.FormValue("_csrf") // 表单字段
}
if cookieToken.Value != headerToken {
http.Error(w, "CSRF token invalid", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
func (m *CSRFMiddleware) generateToken() string {
bytes := make([]byte, 32)
rand.Read(bytes)
return base64.URLEncoding.EncodeToString(bytes)
}
// 使用示例
func main() {
mux := http.NewServeMux()
// 表单页面
mux.HandleFunc("/transfer", func(w http.ResponseWriter, r *http.Request) {
token := r.Context().Value("csrf_token").(string)
html := `
<form method="POST" action="/api/transfer">
<input type="hidden" name="_csrf" value="` + token + `">
<input type="text" name="to" placeholder="Recipient">
<input type="number" name="amount" placeholder="Amount">
<button type="submit">Transfer</button>
</form>
`
w.Write([]byte(html))
})
// API接口
mux.HandleFunc("/api/transfer", func(w http.ResponseWriter, r *http.Request) {
// 处理转账逻辑
w.Write([]byte("Transfer successful"))
})
csrfMiddleware := NewCSRFMiddleware()
handler := csrfMiddleware.Handler(mux)
http.ListenAndServe(":8080", handler)
}
SameSite Cookie配置
// 设置SameSite属性
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: "abc123",
Path: "/",
HttpOnly: true,
Secure: true, // 仅HTTPS
SameSite: http.SameSiteLaxMode, // Lax模式
})
/*
SameSite模式对比:
┌─────────────────────────────────────────┐
│ Strict: │
│ - 完全禁止跨站发送Cookie │
│ - 从其他网站点击链接也不会携带 │
│ - 最安全,但用户体验差 │
│ │
│ Lax(默认): │
│ - 允许顶级导航GET请求携带 │
│ - 禁止POST、iframe、AJAX跨站 │
│ - 平衡安全与体验 │
│ │
│ None: │
│ - 允许所有跨站请求携带 │
│ - 必须配合Secure属性 │
│ - 最不安全,仅用于特殊场景 │
└─────────────────────────────────────────┘
*/
常见陷阱与调试
CORS常见错误
// 错误1:预检请求失败
/*
Access to fetch at 'https://api.example.com/data'
from origin 'https://web.example.com'
has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
*/
// 原因:服务器没有正确处理OPTIONS请求
// 解决:确保OPTIONS请求返回正确的CORS头
// 错误2:携带Cookie时Origin不能为 *
/*
Access to fetch at 'https://api.example.com/data'
from origin 'https://web.example.com'
has been blocked by CORS policy:
The value of the 'Access-Control-Allow-Origin' header in the response
must not be the wildcard '*' when the request's credentials mode is 'include'.
*/
// 原因:allowCredentials=true时,Origin不能是 *
// 解决:指定具体的Origin
// 错误3:自定义Header未在允许列表中
/*
Access to fetch at 'https://api.example.com/data'
from origin 'https://web.example.com'
has been blocked by CORS policy:
Request header field X-Custom-Header is not allowed by
Access-Control-Allow-Headers in preflight response.
*/
// 原因:自定义Header需要在Access-Control-Allow-Headers中声明
// 解决:将X-Custom-Header加入允许列表
CORS调试工具
# 使用curl测试CORS
curl -I -X OPTIONS \
-H "Origin: https://web.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
https://api.example.com/data
# 预期响应:
# HTTP/1.1 204 No Content
# Access-Control-Allow-Origin: https://web.example.com
# Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
# Access-Control-Allow-Headers: Content-Type
# Access-Control-Max-Age: 86400
总结
CORS配置最佳实践
| 场景 | 配置建议 |
|---|---|
| 生产环境 | 明确指定允许的Origin列表,不用 * |
| 需要Cookie | AllowCredentials=true,Origin必须具体 |
| 预检缓存 | Max-Age设置为86400(24小时) |
| 自定义Header | 在Allow-Headers中明确声明 |
CSRF防护最佳实践
| 防护方式 | 适用场景 | 安全性 |
|---|---|---|
| CSRF Token | 表单提交、API请求 | ⭐⭐⭐⭐⭐ |
| SameSite=Lax | 现代浏览器 | ⭐⭐⭐⭐ |
| 验证Origin | 辅助验证 | ⭐⭐⭐ |
| 双重Cookie | 简单场景 | ⭐⭐⭐ |
| 二次验证 | 敏感操作 | ⭐⭐⭐⭐⭐ |
关键原则
- CORS不是安全机制:只是浏览器的限制,服务器仍需验证权限
- CSRF Token必须随机:使用加密安全的随机数生成器
- SameSite配合Secure:SameSite=None必须设置Secure
- 分层防护:多种防护方式组合使用
- 定期审计:检查CORS配置是否过于宽松
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。