别着急,坐和放宽
使用社交账号登录
跨域(Cross-Origin) 是指在一个网站中请求另一个域名下的资源。现代Web应用通常采用前后端分离架构,前端和后端API往往部署在不同的域名下,这就产生了跨域场景。
同源策略(Same-Origin Policy): 浏览器的基础安全机制,限制一个源(Origin)的文档或脚本如何与另一个源的资源进行交互。
源(Origin)的定义: 协议 + 域名 + 端口,三者完全相同才算同源。
https://example.com:443/page1 ✅ 同源
https://example.com:443/page2
https://example.com ❌ 不同源 (协议不同)
http://example.com
https://example.com ❌ 不同源 (域名不同)
https://api.example.com
https://example.com:443 ❌ 不同源 (端口不同)
https://example.com:8080
假设你的应用架构如下:
https://example-frontend.comhttps://api.example-backend.com如果不进行适当的跨域配置和安全防护,可能面临以下风险:
CORS允许服务器声明哪些源可以访问其资源,是解决跨域问题的标准方案。
Gin框架实现:
package main
import (
"github.com/gin-gonic/gin"
"github.com/gin-contrib/cors"
"time"
)
func main() {
r := gin.Default()
// Method 1: Use cors middleware with strict configuration
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://example-frontend.com"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
r.GET("/api/data", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "Secure cross-origin data transfer"})
})
r.Run(":8080")
}
自定义CORS中间件:
func ValidateOrigin() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
referer := c.Request.Header.Get("Referer")
allowedOrigins := []string{
"https://example-frontend.com",
"https://mobile.example.com",
}
isValid := false
for _, allowed := range allowedOrigins {
if origin == allowed || strings.HasPrefix(referer, allowed) {
isValid = true
break
}
}
if !isValid && origin != "" {
c.JSON(403, gin.H{"error": "Forbidden: Untrusted origin"})
c.Abort()
return
}
c.Next()
}
}
SameSite属性说明:
Strict: Cookie仅在同站请求时发送(最安全)Lax: 大多数跨站请求不发送Cookie(推荐)None: 所有请求都发送Cookie(需配合Secure)CSRF (Cross-Site Request Forgery) 是一种攻击方式,攻击者诱导受害者在已登录的Web应用上执行非本意的操作。
CSRF防护清单:
本文详细介绍了跨域攻击和CSRF攻击的原理与防护方法。通过正确配置CORS、实施CSRF Token保护、设置安全的Cookie属性,可以有效防御这些攻击。
关键要点:
func CustomCORS() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
allowedOrigins := map[string]bool{
"https://example-frontend.com": true,
"https://mobile.example.com": true,
}
if allowedOrigins[origin] {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CSRF-Token")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Max-Age", "86400")
}
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
func SetSecureCookie(c *gin.Context, name, value string) {
c.SetCookie(
name,
value,
3600,
"/",
"example.com",
true, // secure (HTTPS only)
true, // httpOnly
)
c.SetSameSite(http.SameSiteStrictMode)
}
<!-- 攻击者网站 -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" style="display:none">
package csrf
import (
"crypto/rand"
"encoding/base64"
"errors"
"sync"
"time"
"github.com/gin-gonic/gin"
)
type TokenManager struct {
tokens map[string]*TokenInfo
mu sync.RWMutex
}
type TokenInfo struct {
ExpiresAt time.Time
UserID string
}
var manager = &TokenManager{
tokens: make(map[string]*TokenInfo),
}
func Generate(userID string) string {
b := make([]byte, 32)
rand.Read(b)
token := base64.URLEncoding.EncodeToString(b)
manager.mu.Lock()
manager.tokens[token] = &TokenInfo{
ExpiresAt: time.Now().Add(1 * time.Hour),
UserID: userID,
}
manager.mu.Unlock()
return token
}
func Validate(token, userID string) error {
manager.mu.RLock()
info, exists := manager.tokens[token]
manager.mu.RUnlock()
if !exists {
return errors.New("token not found")
}
if time.Now().After(info.ExpiresAt) {
return errors.New("token expired")
}
if info.UserID != userID {
return errors.New("token user mismatch")
}
manager.mu.Lock()
delete(manager.tokens, token)
manager.mu.Unlock()
return nil
}
func Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Method == "GET" || c.Request.Method == "HEAD" ||
c.Request.Method == "OPTIONS" {
c.Next()
return
}
userID, exists := c.Get("userID")
if !exists {
c.JSON(401, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
token := c.GetHeader("X-CSRF-Token")
if token == "" {
token = c.PostForm("csrf_token")
}
if err := Validate(token, userID.(string)); err != nil {
c.JSON(403, gin.H{"error": "Invalid CSRF token"})
c.Abort()
return
}
c.Next()
}
}
async function transferMoney(to, amount) {
const tokenResponse = await fetch('/api/csrf-token', {
credentials: 'include'
});
const { csrf_token } = await tokenResponse.json();
const response = await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrf_token
},
credentials: 'include',
body: JSON.stringify({ to, amount })
});
return response.json();
}
func SetSessionCookie(c *gin.Context, sessionID string) {
c.SetCookie(
"session_id",
sessionID,
3600,
"/",
"",
true,
true,
)
c.SetSameSite(http.SameSiteStrictMode)
}
func ValidateReferer() gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Method == "GET" {
c.Next()
return
}
referer := c.Request.Header.Get("Referer")
origin := c.Request.Header.Get("Origin")
allowedDomains := []string{
"https://example.com",
"https://www.example.com",
}
isValid := false
for _, domain := range allowedDomains {
if strings.HasPrefix(referer, domain) || origin == domain {
isValid = true
break
}
}
if !isValid {
c.JSON(403, gin.H{"error": "Invalid referer"})
c.Abort()
return
}
c.Next()
}
}