server: Go 服务器(REST+WSS 网关+JWT+Argon2+配对+素材直链+任务状态机);修复并发下线 send-on-closed-channel、下发查询 SQL 优先级、配对码原子占用、上传体积上限、JWT 默认密钥告警

This commit is contained in:
Qiufeng
2026-08-20 20:38:21 +08:00
parent bf25d8beac
commit cc8845dcec
44 changed files with 4995 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
package auth
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
func jwtRegisteredExpired() jwt.RegisteredClaims {
return jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(-time.Hour)),
Issuer: "everypublish",
}
}
func signExpired(secret string, claims AccessClaims) (string, error) {
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret))
}
+66
View File
@@ -0,0 +1,66 @@
package auth
import (
"strings"
"testing"
)
func TestHashVerifyPassword(t *testing.T) {
hash, err := HashPassword("everypublish-2026")
if err != nil {
t.Fatalf("hash: %v", err)
}
if !strings.HasPrefix(hash, "$argon2id$") {
t.Fatalf("bad hash format: %s", hash)
}
ok, err := VerifyPassword("everypublish-2026", hash)
if err != nil || !ok {
t.Fatalf("verify should pass: ok=%v err=%v", ok, err)
}
ok, err = VerifyPassword("wrong-pass", hash)
if err != nil || ok {
t.Fatalf("verify should fail: ok=%v err=%v", ok, err)
}
}
func TestJWTAccessRoundTrip(t *testing.T) {
secret := "test-secret"
token, err := IssueAccess(secret, 42, 7, "user", "owner")
if err != nil {
t.Fatalf("issue: %v", err)
}
claims, err := ParseAccess(secret, token)
if err != nil {
t.Fatalf("parse: %v", err)
}
if claims.UID != 42 || claims.WorkspaceID != 7 || claims.MemberRole != "owner" {
t.Fatalf("claims mismatch: %+v", claims)
}
}
func TestJWTAccessExpired(t *testing.T) {
secret := "test-secret"
claims := AccessClaims{UID: 1, RegisteredClaims: jwtRegisteredExpired()}
token, err := signExpired(secret, claims)
if err != nil {
t.Fatalf("sign: %v", err)
}
if _, err = ParseAccess(secret, token); err == nil {
t.Fatal("expired token should fail")
}
}
func TestJWTRefreshRoundTrip(t *testing.T) {
secret := "test-secret"
token, err := IssueRefresh(secret, 9, "jti-1")
if err != nil {
t.Fatalf("issue: %v", err)
}
claims, err := ParseRefresh(secret, token)
if err != nil {
t.Fatalf("parse: %v", err)
}
if claims.UID != 9 || claims.JTI != "jti-1" {
t.Fatalf("claims mismatch: %+v", claims)
}
}
+135
View File
@@ -0,0 +1,135 @@
package auth
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
// 过期策略:Access 15 分钟 / Refresh 7 天(旋转)/ Invite 24 小时
const (
AccessTTL = 15 * time.Minute
RefreshTTL = 7 * 24 * time.Hour
InviteTTL = 24 * time.Hour
)
// AccessClaims 访问令牌声明
type AccessClaims struct {
UID uint64 `json:"uid"`
WorkspaceID uint64 `json:"wsid"`
Role string `json:"role"`
MemberRole string `json:"mrole"`
jwt.RegisteredClaims
}
// RefreshClaims 刷新令牌声明
type RefreshClaims struct {
UID uint64 `json:"uid"`
JTI string `json:"jti"`
jwt.RegisteredClaims
}
// InviteClaims 邀请令牌声明
type InviteClaims struct {
WorkspaceID uint64 `json:"wsid"`
Email string `json:"email"`
Role string `json:"role"`
jwt.RegisteredClaims
}
// IssueAccess 签发访问令牌
func IssueAccess(secret string, uid, wsid uint64, role, memberRole string) (string, error) {
now := time.Now()
claims := AccessClaims{
UID: uid, WorkspaceID: wsid, Role: role, MemberRole: memberRole,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(AccessTTL)),
Issuer: "everypublish",
},
}
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret))
}
// IssueRefresh 签发刷新令牌
func IssueRefresh(secret string, uid uint64, jti string) (string, error) {
now := time.Now()
claims := RefreshClaims{
UID: uid, JTI: jti,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(RefreshTTL)),
Issuer: "everypublish",
},
}
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret))
}
// IssueInvite 签发邀请令牌
func IssueInvite(secret string, wsid uint64, email, role string) (string, error) {
now := time.Now()
claims := InviteClaims{
WorkspaceID: wsid, Email: email, Role: role,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(InviteTTL)),
Issuer: "everypublish",
},
}
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret))
}
// ParseAccess 解析访问令牌
func ParseAccess(secret, token string) (*AccessClaims, error) {
claims := &AccessClaims{}
parsed, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return []byte(secret), nil
})
if err != nil {
return nil, err
}
if !parsed.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
// ParseRefresh 解析刷新令牌
func ParseRefresh(secret, token string) (*RefreshClaims, error) {
claims := &RefreshClaims{}
parsed, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return []byte(secret), nil
})
if err != nil {
return nil, err
}
if !parsed.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
// ParseInvite 解析邀请令牌
func ParseInvite(secret, token string) (*InviteClaims, error) {
claims := &InviteClaims{}
parsed, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return []byte(secret), nil
})
if err != nil {
return nil, err
}
if !parsed.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
+56
View File
@@ -0,0 +1,56 @@
package auth
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
// Argon2id 参数(OWASP 推荐基线)
const (
argonTime = 1
argonMemory = 64 * 1024
argonThreads = 4
argonKeyLen = 32
saltLen = 16
)
// HashPassword 生成 $argon2id$v=19$m=65536,t=1,p=4$salt$hash
func HashPassword(password string) (string, error) {
salt := make([]byte, saltLen)
if _, err := rand.Read(salt); err != nil {
return "", err
}
hash := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen)
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s", argonMemory, argonTime, argonThreads, b64Salt, b64Hash), nil
}
// VerifyPassword 校验密码(常数时间比较)
func VerifyPassword(password, encoded string) (bool, error) {
parts := strings.Split(encoded, "$")
if len(parts) != 6 {
return false, fmt.Errorf("invalid hash format")
}
var memory uint32
var timeCost uint32
var threads uint8
if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &timeCost, &threads); err != nil {
return false, err
}
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
return false, err
}
want, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil {
return false, err
}
got := argon2.IDKey([]byte(password), salt, timeCost, memory, threads, uint32(len(want)))
return subtle.ConstantTimeCompare(got, want) == 1, nil
}