Files
nofx/auth/auth.go
T
WquGuru befc887a89 fix(ci): add test encryption key for CI environment (#826)
* fix(ci): add test encryption key for CI environment
- Add DATA_ENCRYPTION_KEY environment variable to PR test workflow
- Add test RSA public key for encryption tests in CI
- Ensures unit tests pass in CI without production credentials
Co-authored-by: tinkle-community <tinklefund@gmail.com>
* fix(ci): install Go cover tool to eliminate covdata warnings
- Add step to install golang.org/x/tools/cmd/cover in CI workflow
- Use || true to prevent installation failure from breaking CI
- Eliminates "no such tool covdata" warnings during test execution
- Apply go fmt to multiple files for consistency
Co-authored-by: tinkle-community <tinklefund@gmail.com>
* fix(ci): install covdata tool for Go 1.23 coverage
The CI was failing with "go: no such tool 'covdata'" error.
This is because Go 1.23 requires the covdata tool to be installed
for coverage reporting.
Changes:
- Install golang.org/x/tools/cmd/covdata in CI workflow
- Update step name to reflect both coverage tools being installed
Fixes the unit test failures in CI pipeline.
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
* fix(ci): remove unnecessary covdata installation and use builtin go tool cover
The previous attempt to install golang.org/x/tools/cmd/covdata was failing
because the package structure changed in Go 1.23 and tools v0.38.0.
The covdata tool is not needed for this project since we only use simple
coverage reporting with go test -coverprofile. The go tool cover command
is built into the Go toolchain and requires no additional installation.
Changes:
- Remove failed covdata and cover installation attempts
- Add verification step for go tool cover availability
- Simplify CI pipeline by eliminating unnecessary dependencies
Co-authored-by: tinkle-community <tinklefund@gmail.com>
* fix(ci): upgrade Go version to 1.25 to match go.mod declaration
The CI was using Go 1.23 while go.mod declares go 1.25.0, causing
"no such tool covdata" errors during coverage test compilation.
Go 1.25's coverage infrastructure requires toolchain features not
available in Go 1.23.
This change aligns the CI Go version with the project's declared
version requirement, ensuring the full Go 1.25 toolchain (including
the covdata tool) is available for coverage testing.
Co-authored-by: tinkle-community <tinklefund@gmail.com>
---------
Co-authored-by: tinkle-community <tinklefund@gmail.com>
2025-11-09 18:40:03 +08:00

155 lines
3.9 KiB
Go

package auth
import (
"crypto/rand"
"fmt"
"log"
"sync"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/pquerna/otp/totp"
"golang.org/x/crypto/bcrypt"
)
// JWTSecret JWT密钥,将从配置中动态设置
var JWTSecret []byte
// tokenBlacklist 用于登出后的token黑名单(仅内存,按过期时间清理)
var tokenBlacklist = struct {
sync.RWMutex
items map[string]time.Time
}{items: make(map[string]time.Time)}
// maxBlacklistEntries 黑名单最大容量阈值
const maxBlacklistEntries = 100_000
// OTPIssuer OTP发行者名称
const OTPIssuer = "nofxAI"
// SetJWTSecret 设置JWT密钥
func SetJWTSecret(secret string) {
JWTSecret = []byte(secret)
}
// BlacklistToken 将token加入黑名单直到过期
func BlacklistToken(token string, exp time.Time) {
tokenBlacklist.Lock()
defer tokenBlacklist.Unlock()
tokenBlacklist.items[token] = exp
// 如果超过容量阈值,则进行一次过期清理;若仍超限,记录警告日志
if len(tokenBlacklist.items) > maxBlacklistEntries {
now := time.Now()
for t, e := range tokenBlacklist.items {
if now.After(e) {
delete(tokenBlacklist.items, t)
}
}
if len(tokenBlacklist.items) > maxBlacklistEntries {
log.Printf("auth: token blacklist size (%d) exceeds limit (%d) after sweep; consider reducing JWT TTL or using a shared persistent store",
len(tokenBlacklist.items), maxBlacklistEntries)
}
}
}
// IsTokenBlacklisted 检查token是否在黑名单中(过期自动清理)
func IsTokenBlacklisted(token string) bool {
tokenBlacklist.Lock()
defer tokenBlacklist.Unlock()
if exp, ok := tokenBlacklist.items[token]; ok {
if time.Now().After(exp) {
delete(tokenBlacklist.items, token)
return false
}
return true
}
return false
}
// Claims JWT声明
type Claims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
jwt.RegisteredClaims
}
// HashPassword 哈希密码
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
// CheckPassword 验证密码
func CheckPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// GenerateOTPSecret 生成OTP密钥
func GenerateOTPSecret() (string, error) {
secret := make([]byte, 20)
_, err := rand.Read(secret)
if err != nil {
return "", err
}
key, err := totp.Generate(totp.GenerateOpts{
Issuer: OTPIssuer,
AccountName: uuid.New().String(),
})
if err != nil {
return "", err
}
return key.Secret(), nil
}
// VerifyOTP 验证OTP码
func VerifyOTP(secret, code string) bool {
return totp.Validate(code, secret)
}
// GenerateJWT 生成JWT token
func GenerateJWT(userID, email string) (string, error) {
claims := Claims{
UserID: userID,
Email: email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // 24小时过期
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "nofxAI",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(JWTSecret)
}
// ValidateJWT 验证JWT token
func ValidateJWT(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("意外的签名方法: %v", token.Header["alg"])
}
return JWTSecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("无效的token")
}
// GetOTPQRCodeURL 获取OTP二维码URL
func GetOTPQRCodeURL(secret, email string) string {
return fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", OTPIssuer, email, secret, OTPIssuer)
}