直播弹幕:前台注册登录与 JWT;Nginx 缓解间歇 502;site_users 集合

Made-with: Cursor
This commit is contained in:
whm
2026-03-26 14:52:28 +08:00
parent f28b80354f
commit 2e675bda51
15 changed files with 730 additions and 6 deletions

View File

@@ -0,0 +1,178 @@
package handlers
import (
"context"
"errors"
"net/http"
"os"
"strings"
"time"
"unicode/utf8"
"yh_web/server/config"
"yh_web/server/models"
"yh_web/server/utils"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
)
const siteJWTExpire = 30 * 24 * time.Hour
// SiteClaims 前台 JWT仅弹幕等轻量场景勿与后台 Claims 混用)
type SiteClaims struct {
UserID string `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
func siteJWTSigningKey() []byte {
s := strings.TrimSpace(os.Getenv("SITE_JWT_SECRET"))
if s == "" {
s = "yh_web_site_dm_jwt_change_in_production"
}
return []byte(s)
}
// ParseSiteClaims 解析前台 JWT失败返回 nil,false
func ParseSiteClaims(tokenStr string) (*SiteClaims, bool) {
tokenStr = strings.TrimSpace(tokenStr)
if len(tokenStr) > 7 && strings.EqualFold(tokenStr[:7], "bearer ") {
tokenStr = strings.TrimSpace(tokenStr[7:])
}
if tokenStr == "" {
return nil, false
}
var claims SiteClaims
t, err := jwt.ParseWithClaims(tokenStr, &claims, func(t *jwt.Token) (interface{}, error) {
return siteJWTSigningKey(), nil
})
if err != nil || !t.Valid {
return nil, false
}
return &claims, true
}
// SiteDanmakuTokenValid 弹幕发送权限:有效的前台 JWT
func SiteDanmakuTokenValid(tokenStr string) bool {
_, ok := ParseSiteClaims(tokenStr)
return ok
}
type siteRegisterInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type siteLoginInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func siteUsersColl() *mongo.Collection {
return config.GetDB(config.DBName).Collection("site_users")
}
// WebSiteRegister POST /api/web/site/register — 仅用于前台直播弹幕账号
func WebSiteRegister(c *gin.Context) {
if config.MongoClient == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "服务暂不可用"})
return
}
var input siteRegisterInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数无效"})
return
}
u := strings.TrimSpace(input.Username)
if utf8.RuneCountInString(u) < 2 || utf8.RuneCountInString(u) > 32 {
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名为 232 个字符"})
return
}
if len(input.Password) < 6 {
c.JSON(http.StatusBadRequest, gin.H{"error": "密码至少 6 位"})
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second)
defer cancel()
coll := siteUsersColl()
var existing models.SiteUser
err := coll.FindOne(ctx, bson.M{"username": u}).Decode(&existing)
if err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "该用户名已被注册"})
return
}
if !errors.Is(err, mongo.ErrNoDocuments) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "注册失败"})
return
}
doc := models.SiteUser{
Username: u,
PasswordHash: utils.HashPassword(input.Password),
CreatedAt: time.Now().UTC(),
}
res, err := coll.InsertOne(ctx, doc)
if err != nil {
if mongo.IsDuplicateKeyError(err) {
c.JSON(http.StatusConflict, gin.H{"error": "该用户名已被注册"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "注册失败"})
return
}
id, _ := res.InsertedID.(bson.ObjectID)
token, err := issueSiteToken(id.Hex(), u)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "签发登录凭证失败"})
return
}
c.JSON(http.StatusOK, gin.H{"token": token, "username": u})
}
// WebSiteLogin POST /api/web/site/login
func WebSiteLogin(c *gin.Context) {
if config.MongoClient == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "服务暂不可用"})
return
}
var input siteLoginInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数无效"})
return
}
u := strings.TrimSpace(input.Username)
ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second)
defer cancel()
var user models.SiteUser
err := siteUsersColl().FindOne(ctx, bson.M{"username": u}).Decode(&user)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
return
}
if utils.HashPassword(input.Password) != user.PasswordHash {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
return
}
token, err := issueSiteToken(user.ID.Hex(), user.Username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "签发登录凭证失败"})
return
}
c.JSON(http.StatusOK, gin.H{"token": token, "username": user.Username})
}
func issueSiteToken(userID, username string) (string, error) {
claims := SiteClaims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(siteJWTExpire)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Subject: userID,
},
}
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return t.SignedString(siteJWTSigningKey())
}