初始化

This commit is contained in:
Ran
2026-08-25 17:59:42 +08:00
commit 4b7380dd9b
408 changed files with 327400 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
package security
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"io"
)
type Encryptor struct{ key []byte }
func NewEncryptor(encodedKey string) (*Encryptor, error) {
key, err := base64.StdEncoding.DecodeString(encodedKey)
if err != nil || len(key) != 32 {
return nil, errors.New("CONFIG_ENCRYPTION_KEY 必须是 32 字节随机值的 Base64 编码")
}
return &Encryptor{key: key}, nil
}
func (e *Encryptor) Encrypt(plain string) (string, error) {
block, err := aes.NewCipher(e.key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
sealed := gcm.Seal(nonce, nonce, []byte(plain), nil)
return base64.RawStdEncoding.EncodeToString(sealed), nil
}
func (e *Encryptor) Decrypt(encoded string) (string, error) {
data, err := base64.RawStdEncoding.DecodeString(encoded)
if err != nil {
return "", err
}
block, err := aes.NewCipher(e.key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
if len(data) < gcm.NonceSize() {
return "", errors.New("密文无效")
}
plain, err := gcm.Open(nil, data[:gcm.NonceSize()], data[gcm.NonceSize():], nil)
return string(plain), err
}
+55
View File
@@ -0,0 +1,55 @@
package security
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
type PasswordHasher struct {
Time uint32
Memory uint32
Parallelism uint8
HashLength uint32
SaltLength uint32
}
func (h PasswordHasher) Hash(password string) (string, error) {
if len(password) < 8 || len(password) > 128 {
return "", errors.New("密码长度必须为 8 至 128 个字符")
}
salt := make([]byte, h.SaltLength)
if _, err := rand.Read(salt); err != nil {
return "", err
}
hash := argon2.IDKey([]byte(password), salt, h.Time, h.Memory, h.Parallelism, h.HashLength)
return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s", h.Memory, h.Time, h.Parallelism,
base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(hash)), nil
}
func (h PasswordHasher) Verify(encoded, password string) bool {
parts := strings.Split(encoded, "$")
if len(parts) != 6 || parts[1] != "argon2id" {
return false
}
var memory, iterations uint32
var parallelism uint8
if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &iterations, &parallelism); err != nil {
return false
}
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
return false
}
want, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil {
return false
}
got := argon2.IDKey([]byte(password), salt, iterations, memory, parallelism, uint32(len(want)))
return subtle.ConstantTimeCompare(got, want) == 1
}
+63
View File
@@ -0,0 +1,63 @@
package security
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
type AdminClaims struct {
AdminID string `json:"admin_id"`
Username string `json:"username"`
TokenType string `json:"token_type"`
jwt.RegisteredClaims
}
type TokenService struct {
Secret []byte
AccessTTL time.Duration
RefreshTTL time.Duration
}
func (s TokenService) NewAccessToken(adminID, username string) (string, time.Time, error) {
now := time.Now()
expires := now.Add(s.AccessTTL)
claims := AdminClaims{AdminID: adminID, Username: username, TokenType: "access", RegisteredClaims: jwt.RegisteredClaims{Subject: adminID, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(expires)}}
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(s.Secret)
return token, expires, err
}
func (s TokenService) ParseAccessToken(raw string) (*AdminClaims, error) {
claims := new(AdminClaims)
token, err := jwt.ParseWithClaims(raw, claims, func(token *jwt.Token) (any, error) {
if token.Method != jwt.SigningMethodHS256 {
return nil, errors.New("不支持的签名算法")
}
return s.Secret, nil
})
if err != nil || !token.Valid || claims.TokenType != "access" {
return nil, errors.New("登录凭证无效或已过期")
}
return claims, nil
}
func (s TokenService) NewRefreshToken() (plain, hash string, expires time.Time, err error) {
data := make([]byte, 32)
if _, err = rand.Read(data); err != nil {
return
}
plain = base64.RawURLEncoding.EncodeToString(data)
hash = HashToken(plain)
expires = time.Now().Add(s.RefreshTTL)
return
}
func HashToken(token string) string {
sum := sha256.Sum256([]byte(token))
return hex.EncodeToString(sum[:])
}
+47
View File
@@ -0,0 +1,47 @@
package security
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
type WebClaims struct {
UserID string `json:"user_id"`
UID string `json:"uid"`
Username string `json:"username"`
SessionVersion int64 `json:"session_version"`
TokenType string `json:"token_type"`
jwt.RegisteredClaims
}
type WebTokenService struct {
Secret []byte
AccessTTL time.Duration
}
func (s WebTokenService) NewAccessToken(userID, uid, username string, sessionVersion int64) (string, time.Time, error) {
now := time.Now()
expires := now.Add(s.AccessTTL)
claims := WebClaims{
UserID: userID, UID: uid, Username: username, SessionVersion: sessionVersion, TokenType: "web_access",
RegisteredClaims: jwt.RegisteredClaims{Subject: userID, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(expires)},
}
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(s.Secret)
return token, expires, err
}
func (s WebTokenService) ParseAccessToken(raw string) (*WebClaims, error) {
claims := new(WebClaims)
token, err := jwt.ParseWithClaims(raw, claims, func(token *jwt.Token) (any, error) {
if token.Method != jwt.SigningMethodHS256 {
return nil, errors.New("不支持的签名算法")
}
return s.Secret, nil
})
if err != nil || !token.Valid || claims.TokenType != "web_access" {
return nil, errors.New("登录凭证无效或已过期")
}
return claims, nil
}