别着急,坐和放宽
使用社交账号登录
HSTS参数说明:
max-age: HTTPS有效期(秒)includeSubDomains: 包括所有子域名preload: 加入浏览器HSTS预加载列表安全审查要点:
监控指标:
本系列文章系统地覆盖了Web应用安全的方方面面:
立即执行:
短期目标:
长期规划:
学习资源:
func main() {
r := gin.Default()
// ... routes ...
// Force HTTPS in production
if gin.Mode() == gin.ReleaseMode {
// Redirect HTTP to HTTPS
go func() {
httpRouter := gin.Default()
httpRouter.GET("/*path", func(c *gin.Context) {
c.Redirect(301, "https://"+c.Request.Host+c.Request.RequestURI)
})
httpRouter.Run(":80")
}()
// Start HTTPS server
r.RunTLS(":443", "/path/to/cert.pem", "/path/to/key.pem")
} else {
r.Run(":8080")
}
}
func SecurityHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
// Force HTTPS for 2 years
c.Header("Strict-Transport-Security",
"max-age=63072000; includeSubDomains; preload")
c.Next()
}
}
import (
"crypto/tls"
"net/http"
)
func CreateSecureServer() *http.Server {
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
},
}
return &http.Server{
Addr: ":443",
TLSConfig: tlsConfig,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
}
import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
)
func SetupSessions(r *gin.Engine) {
// Use secure session store
store := cookie.NewStore([]byte("secret-key-change-this-32-bytes!!"))
// Configure session options
store.Options(sessions.Options{
Path: "/",
Domain: "",
MaxAge: 3600, // 1 hour
Secure: true, // HTTPS only
HttpOnly: true, // Not accessible via JavaScript
SameSite: http.SameSiteStrictMode,
})
r.Use(sessions.Sessions("mysession", store))
}
func RequireAuth() gin.HandlerFunc {
return func(c *gin.Context) {
session := sessions.Default(c)
userID := session.Get("user_id")
if userID == nil {
c.JSON(401, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
// Check session age
createdAt := session.Get("created_at")
if createdAt != nil {
if time.Now().Unix()-createdAt.(int64) > 3600 {
session.Clear()
session.Save()
c.JSON(401, gin.H{"error": "Session expired"})
c.Abort()
return
}
}
// Regenerate session ID periodically
lastRotation := session.Get("last_rotation")
if lastRotation == nil || time.Now().Unix()-lastRotation.(int64) > 300 {
// Rotate session every 5 minutes
oldSession := session.Get("user_id")
session.Clear()
session.Set("user_id", oldSession)
session.Set("last_rotation", time.Now().Unix())
session.Save()
}
c.Set("userID", userID)
c.Next()
}
}
func Login(c *gin.Context) {
var credentials struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&credentials); err != nil {
c.JSON(400, gin.H{"error": "Invalid input"})
return
}
// Verify credentials
user, err := authenticateUser(credentials.Username, credentials.Password)
if err != nil {
c.JSON(401, gin.H{"error": "Invalid credentials"})
return
}
// Create session
session := sessions.Default(c)
session.Set("user_id", user.ID)
session.Set("created_at", time.Now().Unix())
session.Set("last_rotation", time.Now().Unix())
session.Set("ip_address", c.ClientIP())
session.Set("user_agent", c.Request.UserAgent())
session.Save()
c.JSON(200, gin.H{"message": "Login successful"})
}
import "golang.org/x/crypto/bcrypt"
// HashPassword creates a bcrypt hash of the password
func HashPassword(password string) (string, error) {
// Cost factor: higher = more secure but slower (10-12 recommended)
hash, err := bcrypt.GenerateFromPassword([]byte(password), 12)
if err != nil {
return "", err
}
return string(hash), nil
}
// VerifyPassword checks if password matches hash
func VerifyPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
import "regexp"
// ValidatePasswordStrength ensures password meets security requirements
func ValidatePasswordStrength(password string) error {
if len(password) < 8 {
return errors.New("password must be at least 8 characters")
}
if len(password) > 128 {
return errors.New("password must be less than 128 characters")
}
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
hasSpecial := regexp.MustCompile(`[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]`).MatchString(password)
requirementsMet := 0
if hasUpper { requirementsMet++ }
if hasLower { requirementsMet++ }
if hasNumber { requirementsMet++ }
if hasSpecial { requirementsMet++ }
if requirementsMet < 3 {
return errors.New("password must contain at least 3 of: uppercase, lowercase, number, special character")
}
return nil
}
func Register(c *gin.Context) {
var req struct {
Username string `json:"username" binding:"required,alphanum,min=3,max=20"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Validate password strength
if err := ValidatePasswordStrength(req.Password); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Check if user already exists
var existingUser User
if err := db.Where("username = ? OR email = ?", req.Username, req.Email).First(&existingUser).Error; err == nil {
c.JSON(400, gin.H{"error": "Username or email already exists"})
return
}
// Hash password
hashedPassword, err := HashPassword(req.Password)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to hash password"})
return
}
// Create user
user := User{
Username: req.Username,
Email: req.Email,
Password: hashedPassword,
}
if err := db.Create(&user).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to create user"})
return
}
c.JSON(200, gin.H{"message": "User created successfully"})
}
import (
"crypto/rand"
"encoding/base64"
)
// GenerateResetToken creates a secure random token
func GenerateResetToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
func RequestPasswordReset(c *gin.Context) {
var req struct {
Email string `json:"email" binding:"required,email"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "Invalid email"})
return
}
// Find user
var user User
if err := db.Where("email = ?", req.Email).First(&user).Error; err != nil {
// Don't reveal if email exists
c.JSON(200, gin.H{"message": "If email exists, reset link has been sent"})
return
}
// Generate reset token
token, err := GenerateResetToken()
if err != nil {
c.JSON(500, gin.H{"error": "Failed to generate token"})
return
}
// Store token with expiration
resetToken := PasswordResetToken{
UserID: user.ID,
Token: token,
ExpiresAt: time.Now().Add(1 * time.Hour),
}
db.Create(&resetToken)
// Send email with reset link
sendPasswordResetEmail(user.Email, token)
c.JSON(200, gin.H{"message": "If email exists, reset link has been sent"})
}
import (
"sync"
"time"
)
type RateLimiter struct {
requests map[string][]time.Time
mu sync.RWMutex
limit int
window time.Duration
}
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
rl := &RateLimiter{
requests: make(map[string][]time.Time),
limit: limit,
window: window,
}
// Cleanup old entries
go func() {
ticker := time.NewTicker(1 * time.Minute)
for range ticker.C {
rl.cleanup()
}
}()
return rl
}
func (rl *RateLimiter) Allow(key string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
cutoff := now.Add(-rl.window)
// Get existing requests
requests := rl.requests[key]
// Remove old requests
var validRequests []time.Time
for _, t := range requests {
if t.After(cutoff) {
validRequests = append(validRequests, t)
}
}
// Check if limit exceeded
if len(validRequests) >= rl.limit {
return false
}
// Add current request
validRequests = append(validRequests, now)
rl.requests[key] = validRequests
return true
}
func (rl *RateLimiter) cleanup() {
rl.mu.Lock()
defer rl.mu.Unlock()
cutoff := time.Now().Add(-rl.window)
for key, requests := range rl.requests {
var validRequests []time.Time
for _, t := range requests {
if t.After(cutoff) {
validRequests = append(validRequests, t)
}
}
if len(validRequests) == 0 {
delete(rl.requests, key)
} else {
rl.requests[key] = validRequests
}
}
}
func RateLimitMiddleware(limiter *RateLimiter) gin.HandlerFunc {
return func(c *gin.Context) {
// Use IP address as key
key := c.ClientIP()
if !limiter.Allow(key) {
c.JSON(429, gin.H{
"error": "Too many requests",
"retry_after": limiter.window.Seconds(),
})
c.Abort()
return
}
c.Next()
}
}
// Usage
func main() {
r := gin.Default()
// Global rate limit: 100 requests per minute per IP
globalLimiter := NewRateLimiter(100, 1*time.Minute)
r.Use(RateLimitMiddleware(globalLimiter))
// Stricter limit for login endpoint: 5 attempts per minute
loginLimiter := NewRateLimiter(5, 1*time.Minute)
r.POST("/login", RateLimitMiddleware(loginLimiter), Login)
// Stricter limit for registration: 3 attempts per hour
registerLimiter := NewRateLimiter(3, 1*time.Hour)
r.POST("/register", RateLimitMiddleware(registerLimiter), Register)
r.Run(":8080")
}
func UserRateLimitMiddleware(limiter *RateLimiter) gin.HandlerFunc {
return func(c *gin.Context) {
// Try to get user ID from session
session := sessions.Default(c)
userID := session.Get("user_id")
var key string
if userID != nil {
// Use user ID for authenticated users
key = fmt.Sprintf("user:%v", userID)
} else {
// Use IP for unauthenticated users
key = fmt.Sprintf("ip:%s", c.ClientIP())
}
if !limiter.Allow(key) {
c.JSON(429, gin.H{"error": "Too many requests"})
c.Abort()
return
}
c.Next()
}
}
import (
"github.com/sirupsen/logrus"
"os"
)
var log = logrus.New()
func SetupLogging() {
// Configure logging format
log.SetFormatter(&logrus.JSONFormatter{})
// Set log level
if gin.Mode() == gin.ReleaseMode {
log.SetLevel(logrus.InfoLevel)
} else {
log.SetLevel(logrus.DebugLevel)
}
// Log to file in production
if gin.Mode() == gin.ReleaseMode {
file, err := os.OpenFile("/var/log/app/security.log",
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err == nil {
log.SetOutput(file)
}
}
}
// LogSecurityEvent logs security-related events
func LogSecurityEvent(eventType, message string, c *gin.Context) {
log.WithFields(logrus.Fields{
"type": eventType,
"ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
"path": c.Request.URL.Path,
"method": c.Request.Method,
"timestamp": time.Now().Unix(),
}).Warn(message)
}
// Examples of security events to log
const (
EventLoginFailure = "LOGIN_FAILURE"
EventLoginSuccess = "LOGIN_SUCCESS"
EventPasswordReset = "PASSWORD_RESET"
EventUnauthorizedAccess = "UNAUTHORIZED_ACCESS"
EventSuspiciousActivity = "SUSPICIOUS_ACTIVITY"
EventRateLimitExceeded = "RATE_LIMIT_EXCEEDED"
)
func RequestLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
latency := time.Since(start)
statusCode := c.Writer.Status()
clientIP := c.ClientIP()
method := c.Request.Method
entry := log.WithFields(logrus.Fields{
"status": statusCode,
"method": method,
"path": path,
"query": query,
"ip": clientIP,
"latency": latency.Milliseconds(),
"user_agent": c.Request.UserAgent(),
})
if statusCode >= 500 {
entry.Error("Server error")
} else if statusCode >= 400 {
entry.Warn("Client error")
// Log security events
if statusCode == 401 || statusCode == 403 {
LogSecurityEvent(EventUnauthorizedAccess,
fmt.Sprintf("Access denied: %s %s", method, path), c)
}
} else {
entry.Info("Request processed")
}
}
}
type SecurityMonitor struct {
failedAttempts map[string]int
mu sync.RWMutex
threshold int
window time.Duration
}
func NewSecurityMonitor(threshold int, window time.Duration) *SecurityMonitor {
return &SecurityMonitor{
failedAttempts: make(map[string]int),
threshold: threshold,
window: window,
}
}
func (sm *SecurityMonitor) RecordFailure(key string) bool {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.failedAttempts[key]++
// Check if threshold exceeded
if sm.failedAttempts[key] >= sm.threshold {
log.WithFields(logrus.Fields{
"key": key,
"attempts": sm.failedAttempts[key],
}).Error("Security threshold exceeded")
// Trigger alert (email, SMS, webhook, etc.)
triggerSecurityAlert(key, sm.failedAttempts[key])
return true
}
return false
}
func (sm *SecurityMonitor) Reset(key string) {
sm.mu.Lock()
defer sm.mu.Unlock()
delete(sm.failedAttempts, key)
}
import (
"github.com/joho/godotenv"
"os"
)
type Config struct {
// Database
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
// Security
JWTSecret string
SessionSecret string
EncryptionKey string
// Server
ServerPort string
Environment string
// External Services
SMTPHost string
SMTPPort string
SMTPUser string
SMTPPassword string
// API Keys
StripeAPIKey string
AWSAccessKey string
AWSSecretKey string
}
func LoadConfig() *Config {
// Load .env file in development
if os.Getenv("ENVIRONMENT") != "production" {
if err := godotenv.Load(); err != nil {
log.Warn("No .env file found")
}
}
return &Config{
DBHost: getEnv("DB_HOST", "localhost"),
DBPort: getEnv("DB_PORT", "5432"),
DBUser: getEnv("DB_USER", ""),
DBPassword: getEnv("DB_PASSWORD", ""),
DBName: getEnv("DB_NAME", ""),
JWTSecret: getEnv("JWT_SECRET", ""),
SessionSecret: getEnv("SESSION_SECRET", ""),
EncryptionKey: getEnv("ENCRYPTION_KEY", ""),
ServerPort: getEnv("SERVER_PORT", "8080"),
Environment: getEnv("ENVIRONMENT", "development"),
SMTPHost: getEnv("SMTP_HOST", ""),
SMTPPort: getEnv("SMTP_PORT", "587"),
SMTPUser: getEnv("SMTP_USER", ""),
SMTPPassword: getEnv("SMTP_PASSWORD", ""),
StripeAPIKey: getEnv("STRIPE_API_KEY", ""),
AWSAccessKey: getEnv("AWS_ACCESS_KEY", ""),
AWSSecretKey: getEnv("AWS_SECRET_KEY", ""),
}
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func (c *Config) Validate() error {
required := map[string]string{
"DB_PASSWORD": c.DBPassword,
"JWT_SECRET": c.JWTSecret,
"SESSION_SECRET": c.SessionSecret,
"ENCRYPTION_KEY": c.EncryptionKey,
}
for key, value := range required {
if value == "" {
return fmt.Errorf("%s is required", key)
}
}
return nil
}
# .env.example - Copy to .env and fill in actual values
# Environment
ENVIRONMENT=development
# Database
DB_HOST=localhost
DB_PORT=5432
DB_USER=myapp_user
DB_PASSWORD=change_this_password
DB_NAME=myapp_db
# Security (Generate strong random values)
JWT_SECRET=your-256-bit-secret-change-this
SESSION_SECRET=your-session-secret-32-bytes-min
ENCRYPTION_KEY=your-encryption-key-32-bytes
# Server
SERVER_PORT=8080
# Email
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
[email protected]
SMTP_PASSWORD=your-app-password
# External APIs
STRIPE_API_KEY=sk_test_...
AWS_ACCESS_KEY=AKIA...
AWS_SECRET_KEY=...
# Static analysis
go install github.com/securego/gosec/v2/cmd/gosec@latest
gosec ./...
# Dependency vulnerability scanning
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
# Run tests with race detector
go test -race ./...
# OWASP ZAP for dynamic testing
docker run -t owasp/zap2docker-stable zap-baseline.py \
-t https://your-app.com \
-r zap-report.html