feat: 初始化配置中心项目
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"kenaito-conf/server/internal/dao"
|
||||
"kenaito-conf/server/internal/model"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type AuthService struct {
|
||||
jwtSecret string
|
||||
expireHrs int
|
||||
}
|
||||
|
||||
func NewAuthService(secret string, expireHours int) *AuthService {
|
||||
return &AuthService{jwtSecret: secret, expireHrs: expireHours}
|
||||
}
|
||||
|
||||
func (s *AuthService) Login(userName, password string) (string, *model.LoginUserInfo, error) {
|
||||
var user model.User
|
||||
has, err := dao.Engine.Where("name = ? AND enabled = 1", userName).Get(&user)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if !has {
|
||||
return "", nil, errors.New("用户不存在或已禁用")
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
return "", nil, errors.New("密码错误")
|
||||
}
|
||||
|
||||
userInfo := &model.LoginUserInfo{
|
||||
Id: strconv.Itoa(user.Id),
|
||||
Name: user.Name,
|
||||
Type: user.Type,
|
||||
GroupId: user.GroupId,
|
||||
GrantAppIds: make([]int, 0),
|
||||
GrantPermissons: make([]string, 0),
|
||||
}
|
||||
|
||||
// load permissions
|
||||
s.loadPermissions(userInfo, user.Id)
|
||||
|
||||
// generate JWT
|
||||
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(),
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(s.jwtSecret))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return tokenString, userInfo, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) ValidateToken(tokenString string) (*model.LoginUserInfo, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(s.jwtSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
userId := int(claims["userId"].(float64))
|
||||
var user model.User
|
||||
has, err := dao.Engine.ID(userId).Get(&user)
|
||||
if err != nil || !has {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
userInfo := &model.LoginUserInfo{
|
||||
Id: strconv.Itoa(user.Id),
|
||||
Name: user.Name,
|
||||
Type: user.Type,
|
||||
GroupId: user.GroupId,
|
||||
GrantAppIds: make([]int, 0),
|
||||
GrantPermissons: make([]string, 0),
|
||||
}
|
||||
s.loadPermissions(userInfo, user.Id)
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) loadPermissions(userInfo *model.LoginUserInfo, userId int) {
|
||||
if userInfo.IsSuperAdmin() {
|
||||
return
|
||||
}
|
||||
|
||||
// load owned apps
|
||||
var apps []model.Application
|
||||
if userInfo.IsGroupAdmin() {
|
||||
dao.Engine.Where("group_id = ?", userInfo.GroupId).Find(&apps)
|
||||
} else {
|
||||
dao.Engine.Where("owner_id = ?", userId).Find(&apps)
|
||||
}
|
||||
for _, app := range apps {
|
||||
userInfo.GrantAppIds = append(userInfo.GrantAppIds, app.Id)
|
||||
userInfo.GrantPermissons = append(userInfo.GrantPermissons, buildPermissionCode(app.Id, "RW"))
|
||||
}
|
||||
|
||||
// load granted permissions
|
||||
var perms []model.UserPermission
|
||||
dao.Engine.Where("user_id = ?", userId).Find(&perms)
|
||||
for _, p := range perms {
|
||||
permCode := buildPermissionCode(p.AppId, p.Operate)
|
||||
// avoid duplicate
|
||||
found := false
|
||||
for _, existing := range userInfo.GrantPermissons {
|
||||
if existing == permCode {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
userInfo.GrantPermissons = append(userInfo.GrantPermissons, permCode)
|
||||
}
|
||||
appIdFound := false
|
||||
for _, id := range userInfo.GrantAppIds {
|
||||
if id == p.AppId {
|
||||
appIdFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !appIdFound {
|
||||
userInfo.GrantAppIds = append(userInfo.GrantAppIds, p.AppId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildPermissionCode(appId int, operate string) string {
|
||||
return strconv.Itoa(appId) + ":" + operate
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"kenaito-conf/server/internal/model"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type ConfigParseService struct{}
|
||||
|
||||
func NewConfigParseService() *ConfigParseService {
|
||||
return &ConfigParseService{}
|
||||
}
|
||||
|
||||
// ParseConfigToKVMap parses config content based on type
|
||||
func (s *ConfigParseService) ParseConfigToKVMap(config *model.AppConfig) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
switch config.Type {
|
||||
case 1: // file
|
||||
if strings.HasSuffix(strings.ToLower(config.Name), ".xml") {
|
||||
s.parseXML(result, config.Contents)
|
||||
} else if strings.HasSuffix(strings.ToLower(config.Name), ".yml") ||
|
||||
strings.HasSuffix(strings.ToLower(config.Name), ".yaml") {
|
||||
s.parseYAML(result, config.Contents)
|
||||
} else {
|
||||
s.parseProperties(result, config.Contents)
|
||||
}
|
||||
case 2: // key-value
|
||||
result[config.Name] = config.Contents
|
||||
case 3: // JSON
|
||||
json.Unmarshal([]byte(config.Contents), &result)
|
||||
}
|
||||
|
||||
// resolve placeholders
|
||||
s.resolvePlaceholders(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *ConfigParseService) parseProperties(result map[string]interface{}, content string) {
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
idx := strings.Index(line, "=")
|
||||
if idx < 0 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(line[:idx])
|
||||
value := strings.TrimSpace(line[idx+1:])
|
||||
if key != "" {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ConfigParseService) parseXML(result map[string]interface{}, content string) {
|
||||
// simple XML parsing for <entry key="...">value</entry> format
|
||||
// This is a simplified version - for production use a proper XML parser
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.Contains(line, "<entry") {
|
||||
continue
|
||||
}
|
||||
// extract key
|
||||
keyStart := strings.Index(line, `key="`)
|
||||
if keyStart < 0 {
|
||||
continue
|
||||
}
|
||||
keyStart += 5
|
||||
keyEnd := strings.Index(line[keyStart:], `"`)
|
||||
if keyEnd < 0 {
|
||||
continue
|
||||
}
|
||||
key := line[keyStart : keyStart+keyEnd]
|
||||
|
||||
// extract value
|
||||
valStart := strings.Index(line, ">")
|
||||
if valStart < 0 {
|
||||
continue
|
||||
}
|
||||
valEnd := strings.Index(line, "</entry>")
|
||||
if valEnd < 0 {
|
||||
continue
|
||||
}
|
||||
value := strings.TrimSpace(line[valStart+1 : valEnd])
|
||||
if key != "" && value != "" {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ConfigParseService) parseYAML(result map[string]interface{}, content string) {
|
||||
var data map[string]interface{}
|
||||
if err := yaml.Unmarshal([]byte(content), &data); err != nil {
|
||||
return
|
||||
}
|
||||
s.flattenYAML("", result, data)
|
||||
}
|
||||
|
||||
func (s *ConfigParseService) flattenYAML(prefix string, result map[string]interface{}, data map[string]interface{}) {
|
||||
for k, v := range data {
|
||||
key := k
|
||||
if prefix != "" {
|
||||
key = prefix + "." + k
|
||||
}
|
||||
if subMap, ok := v.(map[string]interface{}); ok {
|
||||
s.flattenYAML(key, result, subMap)
|
||||
} else {
|
||||
result[key] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ConfigParseService) resolvePlaceholders(result map[string]interface{}) {
|
||||
for key, val := range result {
|
||||
strVal, ok := val.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(strVal, "${") {
|
||||
result[key] = s.replacePlaceholder(result, strVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ConfigParseService) replacePlaceholder(data map[string]interface{}, value string) string {
|
||||
result := value
|
||||
maxIterations := 100
|
||||
for i := 0; i < maxIterations; i++ {
|
||||
start := strings.Index(result, "${")
|
||||
if start < 0 {
|
||||
break
|
||||
}
|
||||
end := strings.Index(result[start:], "}")
|
||||
if end < 0 {
|
||||
break
|
||||
}
|
||||
end += start
|
||||
|
||||
placeholder := result[start+2 : end]
|
||||
defaultVal := ""
|
||||
refKey := placeholder
|
||||
|
||||
// check for default value
|
||||
if idx := strings.Index(placeholder, ":"); idx >= 0 {
|
||||
refKey = placeholder[:idx]
|
||||
defaultVal = placeholder[idx+1:]
|
||||
}
|
||||
|
||||
var replacement string
|
||||
if v, ok := data[refKey]; ok {
|
||||
replacement = fmt.Sprintf("%v", v)
|
||||
} else if defaultVal != "" {
|
||||
replacement = defaultVal
|
||||
} else {
|
||||
replacement = "${" + placeholder + "}"
|
||||
}
|
||||
|
||||
result = result[:start] + replacement + result[end+1:]
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"kenaito-conf/server/internal/model"
|
||||
)
|
||||
|
||||
type PermissionService struct{}
|
||||
|
||||
func NewPermissionService() *PermissionService {
|
||||
return &PermissionService{}
|
||||
}
|
||||
|
||||
func (s *PermissionService) RequireSuperAdmin(userInfo *model.LoginUserInfo) error {
|
||||
if !userInfo.IsSuperAdmin() {
|
||||
return errors.New("超级管理员才有权限操作")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PermissionService) RequirePermission(userInfo *model.LoginUserInfo, groupId *int, appId *int, operate string) error {
|
||||
if userInfo.IsSuperAdmin() {
|
||||
return nil
|
||||
}
|
||||
if groupId != nil && userInfo.GroupId != nil && *groupId == *userInfo.GroupId {
|
||||
return nil
|
||||
}
|
||||
if appId == nil || *appId == 0 {
|
||||
return errors.New("你没有全局配置权限")
|
||||
}
|
||||
permCode := buildPermissionCode(*appId, operate)
|
||||
for _, p := range userInfo.GrantPermissons {
|
||||
if p == permCode {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("你没有该应用权限")
|
||||
}
|
||||
|
||||
func (s *PermissionService) HasPermissionForUser(userInfo *model.LoginUserInfo, targetUser *model.User) error {
|
||||
if userInfo.IsSuperAdmin() {
|
||||
return nil
|
||||
}
|
||||
if !userInfo.IsGroupAdmin() {
|
||||
return errors.New("你不是业务组管理员")
|
||||
}
|
||||
if userInfo.GroupId == nil || targetUser.GroupId == nil || *userInfo.GroupId != *targetUser.GroupId {
|
||||
return errors.New("不能操作其他组成员")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user