This article documents in detail the complete construction of a blog comment system with multi-layer security, smart content moderation, and multi-layer anti-spam rate limiting, covering the implementation of core technologies including setting up the Gin framework, JWT authentication, multiple rate limits by IP/email/content, human verification, and AI content moderation.
Table of Contents
Project Overview and Architecture Design
Tech Stack Selection and Project Structure
The Multi-Layer Security System
Database Design and Model Relationships
Core Comment Feature Implementation
Smart Content Moderation System
Rate Limiting and Anti-Spam Mechanism
Deployment and Operations Practice
Summary and Outlook
1. Project Overview and Architecture Design 1.1 Project Background As the blog’s content kept growing, I needed a safe, reliable, and feature-complete comment system to interact with readers. Traditional third-party comment systems (like Disqus and Gitment) have flaws such as uncontrollable data, slow loading, and privacy issues. So I decided to build my own comment system, which needed to meet the following core requirements:
Comment features : post comments, reply to comments, paginated display
Multi-layer security : Referer validation, human verification, IP rate limiting, email rate limiting, content deduplication
Content moderation : AI automatic moderation + manual moderation as a dual safeguard
Admin backend : admin login, comment moderation, deletion, replies
Data security : JWT authentication, soft delete, email masking
1.2 Overall Architecture ┌─────────────────────────────────────────────────────────────────┐ │ Client Layer │ │ Blog frontend (Hexo) │ Admin backend │ └──────────────────────────────┼──────────────────────────────────┘ │ HTTPS ┌──────────────────────────────▼──────────────────────────────────┐ │ Nginx reverse proxy │ │ SSL termination │ static assets │ rate limit (1 r/m) │ logging │ └──────────────────────────────┬──────────────────────────────────┘ │ ┌──────────────────────────────▼──────────────────────────────────┐ │ Go Gin Server (8083 ) │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ │ │ Comment │ │ Message │ │ Auth │ │ Admin │ │ │ │ (comment) │ │ (messages) │ │ (JWT auth) │ │ (admin) │ │ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └─────┬─────┘ │ │ │ │ │ │ │ │ ┌──────▼───────────────▼───────────────▼──────────────▼─────┐ │ │ │ Middleware Layer │ │ │ │ Referer │ Turnstile │ RateLimit │ JWTAuth │ CORS │ Audit │ │ │ └────────────────────────────────────────────────────────────┘ │ └──────────────────────────────┬──────────────────────────────────┘ │ ▼ ┌─────────────────────┐ │ MySQL 8 │ │ (comments/messages/ │ │ users) │ └─────────────────────┘
1.3 Core Features
Feature
Technical implementation
Description
Origin validation
Referer middleware
Only allow comments from designated domains
Human verification
Cloudflare Turnstile
Invisible CAPTCHA replacing traditional CAPTCHA
Multi-layer rate limiting
Token Bucket
Triple rate limiting by IP + email + content fingerprint
Content moderation
Alibaba Cloud Bailian
AI-powered detection of violating content
Authentication/authorization
JWT
Stateless authentication, 7-day validity
Data masking
Runtime processing
Mask sensitive info like email and IP when displaying
2. Tech Stack Selection and Project Structure 2.1 Core Tech Stack module blog-apigo 1.21 require ( github.com/gin-gonic/gin v1.9 .1 github.com/golang-jwt/jwt/v5 v5.2 .0 github.com/google/uuid v1.6 .0 golang.org/x/time v0.5 .0 gorm.io/driver/mysql v1.5 .4 gorm.io/gorm v1.25 .7 )
2.2 Project Structure blog_api/ ├── cmd/ │ └── main.go ├── internal/ │ ├── config/ │ │ └── config.go │ ├── handlers/ │ │ ├── auth.go │ │ ├── comment.go │ │ └── message.go │ ├── middleware/ │ │ ├── auth.go │ │ ├── cors.go │ │ ├── error.go │ │ └── referer.go │ ├── models/ │ │ ├── comment.go │ │ └── message.go │ ├── routes/ │ │ └── routes.go │ └── services/ │ ├── audit.go │ └── turnstile.go ├── pkg/ │ ├── jwt/ │ │ └── jwt.go │ └── ratelimit/ │ ├── ratelimit.go │ └── comment_limiter.go ├── scripts/ │ └── init_db.sql ├── blog-api.service ├── nginx.conf ├── config.yaml └── go.mod
3. The Multi-Layer Security System 3.1 Referer Origin Check Prevents malicious websites from submitting comments cross-site; only the designated domain is allowed:
package middlewareimport ( "net/http" "strings" "github.com/gin-gonic/gin" )func RefererCheck (allowedReferer string ) gin.HandlerFunc { return func (c *gin.Context) { referer := c.GetHeader("Referer" ) origin := c.GetHeader("Origin" ) if referer != "" && !strings.Contains(referer, allowedReferer) { c.JSON(http.StatusForbidden, gin.H{"error" : "Illegal origin" }) c.Abort() return } if origin != "" && !strings.Contains(origin, allowedReferer) { c.JSON(http.StatusForbidden, gin.H{"error" : "Illegal origin" }) c.Abort() return } c.Next() } }
3.2 Cloudflare Turnstile Human Verification Replaces traditional CAPTCHA, offering a better user experience:
package servicesimport ( "encoding/json" "fmt" "net/http" "net/url" "time" )const turnstileVerifyURL = "https://challenges.cloudflare.com/turnstile/v0/siteverify" type TurnstileService struct { secretKey string client *http.Client }type TurnstileVerifyResponse struct { Success bool `json:"success"` ChallengeTS string `json:"challenge_ts"` Hostname string `json:"hostname"` ErrorCodes []string `json:"error-codes"` }func NewTurnstileService (secretKey string ) *TurnstileService { return &TurnstileService{ secretKey: secretKey, client: &http.Client{Timeout: 10 * time.Second}, } }func (s *TurnstileService) VerifyToken(token string , remoteIP string ) (*TurnstileVerifyResponse, error ) { data := url.Values{} data.Set("secret" , s.secretKey) data.Set("response" , token) if remoteIP != "" { data.Set("remoteip" , remoteIP) } resp, err := s.client.PostForm(turnstileVerifyURL, data) if err != nil { return nil , fmt.Errorf("failed to verify turnstile: %w" , err) } defer resp.Body.Close() var result TurnstileVerifyResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil , err } return &result, nil }
3.3 JWT Authentication Middleware Admin operations require JWT authentication:
package middlewareimport ( "net/http" "strings" "blog-api/pkg/jwt" "github.com/gin-gonic/gin" )func Auth () gin.HandlerFunc { return func (c *gin.Context) { authHeader := c.GetHeader("Authorization" ) if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{"error" : "Missing authentication information" }) c.Abort() return } parts := strings.SplitN(authHeader, " " , 2 ) if len (parts) != 2 || strings.ToLower(parts[0 ]) != "bearer" { c.JSON(http.StatusUnauthorized, gin.H{"error" : "Invalid authentication format" }) c.Abort() return } claims, err := jwt.ParseToken(parts[1 ]) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error" : "Invalid or expired token" }) c.Abort() return } c.Set("userID" , claims.UserID) c.Set("username" , claims.Username) c.Next() } }
4. Database Design and Model Relationships 4.1 Data Model Design package modelsimport ( "time" "github.com/google/uuid" "gorm.io/gorm" )type Comment struct { ID string `json:"id" gorm:"type:char(36);primary_key"` PostID string `json:"post_id" gorm:"type:varchar(200);not null;index;comment:'post ID/path'"` ParentID *string `json:"parent_id,omitempty" gorm:"type:char(36);index;comment:'parent comment ID, for replies'"` Nickname string `json:"nickname" gorm:"type:varchar(50);not null"` Email string `json:"email" gorm:"type:varchar(100);not null"` Website string `json:"website" gorm:"type:varchar(200)"` Content string `json:"content" gorm:"type:text;not null"` IP string `json:"ip" gorm:"type:varchar(50);not null"` UserAgent string `json:"user_agent" gorm:"type:varchar(500)"` Status int `json:"status" gorm:"type:tinyint;default:0;comment:'0-pending 1-approved 2-rejected'"` IsAdmin bool `json:"is_admin" gorm:"default:false"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` Replies []Comment `json:"replies,omitempty" gorm:"foreignKey:ParentID"` }func (c *Comment) BeforeCreate(tx *gorm.DB) error { if c.ID == "" { c.ID = uuid.New().String() } return nil }func (Comment) TableName() string { return "comments" }
4.2 Data Masking func (c *Comment) ToResponse() CommentResponse { return CommentResponse{ ID: c.ID, PostID: c.PostID, ParentID: c.ParentID, Nickname: c.Nickname, Email: maskEmail(c.Email), Website: c.Website, Content: c.Content, Status: c.Status, IsAdmin: c.IsAdmin, CreatedAt: c.CreatedAt, Replies: convertReplies(c.Replies), } }func maskEmail (email string ) string { if len (email) <= 3 { return email } parts := make ([]rune , 0 , len (email)) for i, ch := range email { if i < 2 || ch == '@' { parts = append (parts, ch) } else { parts = append (parts, '*' ) } } return string (parts) }
package handlersfunc (h *CommentHandler) CreateComment(c *gin.Context) { var req models.CreateCommentRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "Invalid request parameters: " + err.Error()}) return } if h.turnstileService != nil { clientIP := c.ClientIP() result, err := h.turnstileService.VerifyToken(req.TurnstileToken, clientIP) if err != nil || !result.Success { c.JSON(http.StatusBadRequest, gin.H{ "error" : h.turnstileService.GetErrorMessage(result.ErrorCodes), }) return } } clientIP := c.ClientIP() userAgent := c.GetHeader("User-Agent" ) comment := &models.Comment{ PostID: req.PostID, ParentID: req.ParentID, Nickname: req.Nickname, Email: req.Email, Website: req.Website, Content: req.Content, IP: clientIP, UserAgent: userAgent, Status: 0 , IsAdmin: false , } if err := h.db.Create(comment).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "Failed to create comment" }) return } if h.auditService != nil { auditText := req.Nickname + ": " + req.Content go h.asyncAuditComment(comment.ID, auditText) } c.JSON(http.StatusCreated, gin.H{ "message" : "Comment submitted successfully; it will display automatically once approved" , "data" : comment.ToResponse(), }) }
func (h *CommentHandler) GetComments(c *gin.Context) { postID := c.Query("post_id" ) if postID == "" { c.JSON(http.StatusBadRequest, gin.H{"error" : "Missing post ID parameter" }) return } page, _ := strconv.Atoi(c.DefaultQuery("page" , "1" )) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size" , "20" )) var comments []models.Comment err := h.db.Where("post_id = ? AND status = ? AND parent_id IS NULL" , postID, 1 ). Order("created_at DESC" ). Offset((page - 1 ) * pageSize). Limit(pageSize). Preload("Replies" , func (db *gorm.DB) *gorm.DB { return db.Where("status = ?" , 1 ).Order("created_at ASC" ) }). Find(&comments).Error if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "Failed to fetch comments" }) return } var responses []models.CommentResponse for _, comment := range comments { responses = append (responses, comment.ToResponse()) } c.JSON(http.StatusOK, gin.H{ "data" : responses, "pagination" : gin.H{ "page" : page, "page_size" : pageSize, }, }) }
6. Smart Content Moderation System 6.1 Alibaba Cloud Bailian AI Moderation Using the Alibaba Cloud Bailian app for smart content moderation:
package servicesimport ( "bytes" "encoding/json" "fmt" "io" "net/http" "time" )type AliyunAuditService struct { apiKey string appID string client *http.Client }func NewAliyunAuditService (apiKey, appID string ) *AliyunAuditService { return &AliyunAuditService{ apiKey: apiKey, appID: appID, client: &http.Client{Timeout: 30 * time.Second}, } }func (s *AliyunAuditService) AuditComment(content string ) (bool , error ) { url := fmt.Sprintf("https://dashscope.aliyuncs.com/api/v1/apps/%s/completion" , s.appID) requestBody := map [string ]interface {}{ "input" : map [string ]interface {}{ "prompt" : content, "biz_params" : map [string ]interface {}{ "user_defined_params" : map [string ]interface {}{ "content" : content, }, }, }, "parameters" : map [string ]interface {}{}, "debug" : map [string ]interface {}{}, } jsonData, _ := json.Marshal(requestBody) req, _ := http.NewRequest("POST" , url, bytes.NewBuffer(jsonData)) req.Header.Set("Authorization" , "Bearer " +s.apiKey) req.Header.Set("Content-Type" , "application/json" ) resp, err := s.client.Do(req) if err != nil { return false , err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var apiResp struct { Output struct { Text string `json:"text"` } `json:"output"` } if err := json.Unmarshal(body, &apiResp); err != nil { return false , err } var auditResult struct { Status bool `json:"status"` } if err := json.Unmarshal([]byte (apiResp.Output.Text), &auditResult); err != nil { return false , err } return auditResult.Status, nil }
6.2 Asynchronous Moderation Flow func (h *CommentHandler) asyncAuditComment(commentID string , auditText string ) { passed, err := h.auditService.AuditComment(auditText) if err != nil { log.Printf("Audit failed for comment %s: %v" , commentID, err) return } var status int if passed { status = 1 log.Printf("Comment %s audit passed" , commentID) } else { status = 2 log.Printf("Comment %s audit rejected" , commentID) } if err := h.db.Model(&models.Comment{}).Where("id = ?" , commentID).Update("status" , status).Error; err != nil { log.Printf("Failed to update comment %s status: %v" , commentID, err) } }
7. Rate Limiting and Anti-Spam Mechanism package ratelimitimport ( "crypto/sha256" "encoding/hex" "fmt" "net/http" "strings" "sync" "time" "github.com/gin-gonic/gin" "golang.org/x/time/rate" )type CommentRateLimiter struct { ipLimiters map [string ]*Visitor ipMu sync.RWMutex ipRate rate.Limit ipBurst int emailLimiters map [string ]*Visitor emailMu sync.RWMutex emailRate rate.Limit emailBurst int contentCache map [string ]time.Time contentMu sync.RWMutex contentTTL time.Duration }func NewCommentRateLimiter (ipPerMinute, emailPerMinute int ) *CommentRateLimiter { c := &CommentRateLimiter{ ipLimiters: make (map [string ]*Visitor), ipRate: rate.Every(time.Minute / time.Duration(ipPerMinute)), ipBurst: 1 , emailLimiters: make (map [string ]*Visitor), emailRate: rate.Every(time.Minute / time.Duration(emailPerMinute)), emailBurst: 1 , contentCache: make (map [string ]time.Time), contentTTL: time.Minute * 10 , } go c.cleanup() return c }func (c *CommentRateLimiter) LimitMiddleware() gin.HandlerFunc { return func (ctx *gin.Context) { if ctx.Request.Method != http.MethodPost { ctx.Next() return } ip := ctx.ClientIP() ipLimiter := c.GetIPLimiter(ip) if !ipLimiter.Allow() { ctx.JSON(http.StatusTooManyRequests, gin.H{ "error" : "Submitting too frequently, please try again later (IP limit)" , }) ctx.Abort() return } email := c.extractEmail(ctx) if email != "" { emailLimiter := c.GetEmailLimiter(email) if !emailLimiter.Allow() { ctx.JSON(http.StatusTooManyRequests, gin.H{ "error" : "This email is submitting too frequently, please try again later" , }) ctx.Abort() return } } ctx.Next() } }
7.2 Content Duplicate Detection func (c *CommentRateLimiter) CheckDuplicateContent(content string ) bool { c.contentMu.Lock() defer c.contentMu.Unlock() fingerprint := c.generateFingerprint(content) if lastTime, exists := c.contentCache[fingerprint]; exists { if time.Since(lastTime) < c.contentTTL { return true } } c.contentCache[fingerprint] = time.Now() return false }func (c *CommentRateLimiter) generateFingerprint(content string ) string { cleaned := strings.ToLower(strings.TrimSpace(content)) if len (cleaned) > 100 { cleaned = cleaned[:100 ] } hash := sha256.Sum256([]byte (cleaned)) return hex.EncodeToString(hash[:]) + fmt.Sprintf("_%d" , len (content)) }
8. Deployment and Operations Practice 8.1 Nginx Multi-Layer Rate-Limit Configuration limit_req_zone $binary_remote_addr zone=comment:10m rate=1r/m;server { listen 443 ssl http2; server_name blogapi.awen.me; location /api/v1/comments { limit_req zone=comment burst=5 nodelay; limit_req_status 429 ; proxy_pass http://127.0.0.1:8083; proxy_set_header Host $host ; proxy_set_header X-Real-IP $remote_addr ; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ; } location / { proxy_pass http://127.0.0.1:8083; proxy_set_header Host $host ; proxy_set_header X-Real-IP $remote_addr ; } }
8.2 systemd Service Configuration [Unit] Description =Blog API ServerAfter =network.target mysqld.serviceRequires =mysqld.service[Service] Type =simpleUser =rootWorkingDirectory =/opt/blog-apiExecStart =/usr/bin/blog-apiEnvironment ="SERVER_PORT=8083" Environment ="SERVER_MODE=production" Environment ="ALLOWED_REFERER=www.awen.me" Environment ="DB_HOST=localhost" Environment ="DB_PORT=3306" Environment ="DB_USER=blog_api_user" Environment ="DB_PASSWORD=your_password" Environment ="DB_NAME=blog_api_db" Environment ="JWT_SECRET=your_jwt_secret_key" Environment ="JWT_EXPIRES_IN=168h" Environment ="COMMENT_RATE_LIMIT_IP=2" Environment ="COMMENT_RATE_LIMIT_EMAIL=2" Environment ="ALIYUN_API_KEY=your_api_key" Environment ="ALIYUN_APP_ID=your_app_id" Environment ="TURNSTILE_SECRET_KEY=your_secret_key" Restart =alwaysRestartSec =10 [Install] WantedBy =multi-user.target
8.3 Deployment Script #!/bin/bash set -e APP_NAME="blog-api" SERVER_HOST="your-server" SERVER_DIR="/opt/blog-api" echo "=== Building... ===" go build -o ${APP_NAME} cmd/main.goecho "=== Deploying to ${SERVER_HOST} ... ===" scp ${APP_NAME} root@${SERVER_HOST} :${SERVER_DIR} /echo "=== Restarting service... ===" ssh root@${SERVER_HOST} "systemctl restart blog-api" echo "=== Checking status... ===" ssh root@${SERVER_HOST} "systemctl status blog-api --no-pager" echo "=== Deploy complete! ==="
9. Summary and Outlook 9.1 Project Highlights
Multi-layer security system
Referer origin check preventing cross-site submission
Cloudflare Turnstile invisible human verification
Triple rate limiting by IP + email + content fingerprint
JWT stateless authentication
Smart content moderation
Alibaba Cloud Bailian AI automatic moderation
Asynchronous processing that doesn’t block users
Manual moderation as a fallback
Complete comment features
Nested reply support
Paginated display
Data masking protecting privacy
Multi-layer rate-limit protection
Nginx-layer rate limiting (1r/m)
Application-layer IP rate limiting
Application-layer email rate limiting
Content fingerprint deduplication
Metric
Value
Description
API response time
< 50ms
Local test
AI moderation time
500ms - 2s
Asynchronous processing
Database query
< 10ms
Indexed
Concurrency
1000+ QPS
Load-test data
9.3 Future Optimization Directions
Email notifications : email the user when a comment is replied to
Sensitive-word filtering : a local sensitive-word dictionary + AI moderation as a dual safeguard
Anti-spam enhancement : add device fingerprinting and behavior analysis
Admin backend : a web-visualized backend for managing comments
Data export : support exporting and backing up comment data
References
评论
0 条评论