初始化
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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, ¶llelism); 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
|
||||
}
|
||||
@@ -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[:])
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user