直播弹幕:前台注册登录与 JWT;Nginx 缓解间歇 502;site_users 集合
Made-with: Cursor
This commit is contained in:
178
server/handlers/site_auth.go
Normal file
178
server/handlers/site_auth.go
Normal 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": "用户名为 2~32 个字符"})
|
||||
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())
|
||||
}
|
||||
Reference in New Issue
Block a user