fix: 修复多个安全缺陷和功能问题

- 移除加密相关代码(服务端和所有客户端)
- 修复 JWT 验证未检查签名算法的问题
- 修复重置密码使用不安全随机数的问题
- 实现 Logout 服务端失效机制
- 修复历史版本对比 diff 颜色标记错误
- 修复占位符解析循环引用风险
- 添加用户禁用状态检查
- 更新 bug 清单文档
This commit is contained in:
2026-03-10 21:07:22 +08:00
parent f41d53da41
commit 932db9fbdc
27 changed files with 292 additions and 721 deletions
+20 -1
View File
@@ -2,6 +2,7 @@ package service
import (
"errors"
"fmt"
"kenaito-conf/server/internal/dao"
"kenaito-conf/server/internal/model"
"strconv"
@@ -47,12 +48,14 @@ func (s *AuthService) Login(userName, password string) (string, *model.LoginUser
s.loadPermissions(userInfo, user.Id)
// generate JWT
now := time.Now()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"userId": user.Id,
"name": user.Name,
"type": user.Type,
"groupId": user.GroupId,
"exp": time.Now().Add(time.Duration(s.expireHrs) * time.Hour).Unix(),
"iat": now.Unix(),
"exp": now.Add(time.Duration(s.expireHrs) * time.Hour).Unix(),
})
tokenString, err := token.SignedString([]byte(s.jwtSecret))
if err != nil {
@@ -64,6 +67,9 @@ func (s *AuthService) Login(userName, password string) (string, *model.LoginUser
func (s *AuthService) ValidateToken(tokenString string) (*model.LoginUserInfo, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(s.jwtSecret), nil
})
if err != nil {
@@ -82,6 +88,19 @@ func (s *AuthService) ValidateToken(tokenString string) (*model.LoginUserInfo, e
return nil, errors.New("user not found")
}
// 检查用户是否已被禁用
if !user.Enabled {
return nil, errors.New("user is disabled")
}
// 检查 token 是否在登出后签发
if user.LastLogoutAt != nil {
iat := int64(claims["iat"].(float64))
if iat <= user.LastLogoutAt.Unix() {
return nil, errors.New("token is invalid after logout")
}
}
userInfo := &model.LoginUserInfo{
Id: strconv.Itoa(user.Id),
Name: user.Name,
+11
View File
@@ -133,6 +133,8 @@ func (s *ConfigParseService) resolvePlaceholders(result map[string]interface{})
func (s *ConfigParseService) replacePlaceholder(data map[string]interface{}, value string) string {
result := value
maxIterations := 100
processedPlaceholders := make(map[string]bool)
for i := 0; i < maxIterations; i++ {
start := strings.Index(result, "${")
if start < 0 {
@@ -154,6 +156,12 @@ func (s *ConfigParseService) replacePlaceholder(data map[string]interface{}, val
defaultVal = placeholder[idx+1:]
}
// 检测循环引用
if processedPlaceholders[refKey] {
// 已经处理过这个占位符,说明存在循环引用,保留原样
break
}
var replacement string
if v, ok := data[refKey]; ok {
replacement = fmt.Sprintf("%v", v)
@@ -163,6 +171,9 @@ func (s *ConfigParseService) replacePlaceholder(data map[string]interface{}, val
replacement = "${" + placeholder + "}"
}
// 标记已处理
processedPlaceholders[refKey] = true
result = result[:start] + replacement + result[end+1:]
}
return result
-115
View File
@@ -1,115 +0,0 @@
package service
import (
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"fmt"
"kenaito-conf/server/internal/config"
"strings"
)
const CryptPrefix = "{Cipher}"
type CryptoService struct{}
func NewCryptoService() *CryptoService {
return &CryptoService{}
}
func (s *CryptoService) GetCryptKey(appId int, env string) string {
base := config.Cfg.Crypto.SecretKey + fmt.Sprintf("%x", appId) + env
h := md5.Sum([]byte(base))
return hex.EncodeToString(h[:])
}
func (s *CryptoService) Encrypt(appId int, env string, data string) string {
key := s.GetCryptKey(appId, env)
data = strings.ReplaceAll(data, CryptPrefix, "")
encrypted := encryptWithAES(key, data)
return CryptPrefix + encrypted
}
func (s *CryptoService) IsEncrypted(appId int, env string, data string) bool {
if !strings.HasPrefix(data, CryptPrefix) {
return false
}
key := s.GetCryptKey(appId, env)
_, err := decryptWithAES(key, strings.TrimPrefix(data, CryptPrefix))
return err == nil
}
func (s *CryptoService) Decrypt(secret string, data string) (string, error) {
return decryptWithAES(secret, data)
}
func encryptWithAES(key string, data string) string {
secretKey := md5.Sum([]byte(key))
aesKey := hex.EncodeToString(secretKey[:])[16:]
block, err := aes.NewCipher([]byte(aesKey))
if err != nil {
return ""
}
// ECB mode (compatible with Java version)
plainText := pkcs7Pad([]byte(data), aes.BlockSize)
encrypted := make([]byte, len(plainText))
mode := cipher.NewCBCEncrypter(block, make([]byte, aes.BlockSize))
mode.CryptBlocks(encrypted, plainText)
return base64.StdEncoding.EncodeToString(encrypted)
}
func decryptWithAES(key string, data string) (string, error) {
secretKey := md5.Sum([]byte(key))
aesKey := hex.EncodeToString(secretKey[:])[16:]
cipherText, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return "", err
}
block, err := aes.NewCipher([]byte(aesKey))
if err != nil {
return "", err
}
if len(cipherText)%aes.BlockSize != 0 {
return "", fmt.Errorf("ciphertext is not a multiple of block size")
}
// ECB mode (compatible with Java version)
plainText := make([]byte, len(cipherText))
mode := cipher.NewCBCDecrypter(block, make([]byte, aes.BlockSize))
mode.CryptBlocks(plainText, cipherText)
plainText, err = pkcs7Unpad(plainText, aes.BlockSize)
if err != nil {
return "", err
}
return string(plainText), nil
}
func pkcs7Pad(data []byte, blockSize int) []byte {
padding := blockSize - len(data)%blockSize
padText := make([]byte, padding)
for i := range padText {
padText[i] = byte(padding)
}
return append(data, padText...)
}
func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) {
if len(data) == 0 {
return nil, fmt.Errorf("empty data")
}
padding := int(data[len(data)-1])
if padding > blockSize || padding == 0 {
return nil, fmt.Errorf("invalid padding")
}
return data[:len(data)-padding], nil
}