2026-07-03 16:01:08 +08:00
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/aes"
|
|
|
|
|
"crypto/cipher"
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
"encoding/base64"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"seeyon-filesystem/config"
|
|
|
|
|
"seeyon-filesystem/dto"
|
|
|
|
|
"seeyon-filesystem/model"
|
|
|
|
|
"seeyon-filesystem/utils"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// OpenAPIService 开放平台服务
|
|
|
|
|
type OpenAPIService struct{}
|
|
|
|
|
|
|
|
|
|
func NewOpenAPIService() *OpenAPIService {
|
|
|
|
|
return &OpenAPIService{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 应用管理 ==========
|
|
|
|
|
|
|
|
|
|
// CreateApp 创建应用
|
|
|
|
|
func (s *OpenAPIService) CreateApp(appName string) (*dto.AppVO, error) {
|
|
|
|
|
appID := generateRandomHex(16)
|
|
|
|
|
appSecret := generateRandomHex(32)
|
|
|
|
|
|
|
|
|
|
app := &model.App{
|
2026-07-18 10:52:32 +08:00
|
|
|
ID: utils.GenID(),
|
2026-07-03 16:01:08 +08:00
|
|
|
AppID: appID,
|
|
|
|
|
AppSecret: appSecret,
|
|
|
|
|
AppName: appName,
|
|
|
|
|
Status: 1,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := config.DB.Create(app).Error; err != nil {
|
|
|
|
|
return nil, fmt.Errorf("创建应用失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &dto.AppVO{
|
|
|
|
|
ID: app.ID,
|
|
|
|
|
AppID: appID,
|
|
|
|
|
AppSecret: appSecret,
|
|
|
|
|
AppName: appName,
|
|
|
|
|
Status: 1,
|
|
|
|
|
CreatedAt: app.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ListApps 获取应用列表(含绑定用户)
|
|
|
|
|
func (s *OpenAPIService) ListApps() ([]dto.AppVO, error) {
|
|
|
|
|
var apps []model.App
|
|
|
|
|
if err := config.DB.Order("created_at DESC").Find(&apps).Error; err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vos := make([]dto.AppVO, 0, len(apps))
|
|
|
|
|
for _, a := range apps {
|
|
|
|
|
vo := dto.AppVO{
|
|
|
|
|
ID: a.ID,
|
|
|
|
|
AppID: a.AppID,
|
|
|
|
|
AppName: a.AppName,
|
|
|
|
|
Status: a.Status,
|
|
|
|
|
CreatedAt: a.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
|
|
|
}
|
|
|
|
|
// 查询绑定用户
|
|
|
|
|
vo.Users = s.getAppUsers(a.ID)
|
|
|
|
|
vos = append(vos, vo)
|
|
|
|
|
}
|
|
|
|
|
return vos, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DeleteApp 删除应用
|
|
|
|
|
func (s *OpenAPIService) DeleteApp(id uint64) error {
|
|
|
|
|
config.DB.Where("app_id = ?", id).Delete(&model.AppUser{})
|
|
|
|
|
return config.DB.Delete(&model.App{}, id).Error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ToggleApp 启用/禁用应用
|
|
|
|
|
func (s *OpenAPIService) ToggleApp(id uint64, status int8) error {
|
|
|
|
|
return config.DB.Model(&model.App{}).Where("id = ?", id).Update("status", status).Error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 应用用户绑定 ==========
|
|
|
|
|
|
|
|
|
|
// BindUsers 绑定用户到应用
|
|
|
|
|
func (s *OpenAPIService) BindUsers(appID uint64, userIDs []uint64) error {
|
|
|
|
|
// 验证应用存在
|
|
|
|
|
var app model.App
|
|
|
|
|
if err := config.DB.First(&app, appID).Error; err != nil {
|
|
|
|
|
return errors.New("应用不存在")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 清除旧绑定
|
|
|
|
|
config.DB.Where("app_id = ?", appID).Delete(&model.AppUser{})
|
|
|
|
|
|
|
|
|
|
// 创建新绑定
|
|
|
|
|
for _, uid := range userIDs {
|
|
|
|
|
appUser := &model.AppUser{
|
|
|
|
|
AppID: appID,
|
|
|
|
|
UserID: uid,
|
|
|
|
|
}
|
|
|
|
|
config.DB.Create(appUser)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UnbindUser 解绑用户
|
|
|
|
|
func (s *OpenAPIService) UnbindUser(appID, userID uint64) error {
|
|
|
|
|
return config.DB.Where("app_id = ? AND user_id = ?", appID, userID).Delete(&model.AppUser{}).Error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getAppUsers 获取应用绑定的用户列表
|
|
|
|
|
func (s *OpenAPIService) getAppUsers(appID uint64) []dto.AppUserVO {
|
|
|
|
|
var appUsers []model.AppUser
|
|
|
|
|
config.DB.Where("app_id = ?", appID).Find(&appUsers)
|
|
|
|
|
|
|
|
|
|
if len(appUsers) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userIDs := make([]uint64, len(appUsers))
|
|
|
|
|
for i, au := range appUsers {
|
|
|
|
|
userIDs[i] = au.UserID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var users []model.User
|
|
|
|
|
config.DB.Where("id IN ?", userIDs).Find(&users)
|
|
|
|
|
|
|
|
|
|
vos := make([]dto.AppUserVO, 0, len(users))
|
|
|
|
|
for _, u := range users {
|
|
|
|
|
vos = append(vos, dto.AppUserVO{
|
|
|
|
|
UserID: u.ID,
|
|
|
|
|
Username: u.Username,
|
|
|
|
|
Nickname: u.Nickname,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return vos
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== Token 生成与验证 ==========
|
|
|
|
|
|
|
|
|
|
// GenerateToken 生成SSO Token
|
|
|
|
|
func (s *OpenAPIService) GenerateToken(appID, username string) (string, error) {
|
|
|
|
|
app, err := s.findApp(appID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
plaintext := fmt.Sprintf("%s:%d", username, time.Now().Unix())
|
|
|
|
|
token, err := aesEncrypt(plaintext, app.AppSecret)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("生成token失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return token, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ValidateToken 验证SSO Token
|
|
|
|
|
// 流程: 解密token → 获取username → 验证用户存在 → 验证用户是否被授权给该应用
|
|
|
|
|
func (s *OpenAPIService) ValidateToken(appID, token string) (string, error) {
|
|
|
|
|
// 1. 查找应用
|
|
|
|
|
app, err := s.findApp(appID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 解密token
|
|
|
|
|
plaintext, err := aesDecrypt(token, app.AppSecret)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", errors.New("token无效或已过期")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. 解析 username:timestamp
|
|
|
|
|
parts := strings.SplitN(plaintext, ":", 2)
|
|
|
|
|
if len(parts) != 2 {
|
|
|
|
|
return "", errors.New("token格式错误")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
username := parts[0]
|
|
|
|
|
|
|
|
|
|
// 4. 验证用户是否存在且启用
|
|
|
|
|
var user model.User
|
|
|
|
|
if err := config.DB.Where("username = ? AND status = 1", username).First(&user).Error; err != nil {
|
|
|
|
|
return "", errors.New("用户不存在或已禁用")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. 验证用户是否被授权给该应用
|
|
|
|
|
var count int64
|
|
|
|
|
config.DB.Model(&model.AppUser{}).Where("app_id = ? AND user_id = ?", app.ID, user.ID).Count(&count)
|
|
|
|
|
if count == 0 {
|
|
|
|
|
return "", fmt.Errorf("用户 %s 未被授权访问此应用", username)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return username, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GenerateJWTForUser 为验证通过的用户生成JWT
|
|
|
|
|
func (s *OpenAPIService) GenerateJWTForUser(username string) (map[string]interface{}, error) {
|
|
|
|
|
var user model.User
|
|
|
|
|
if err := config.DB.Where("username = ?", username).First(&user).Error; err != nil {
|
|
|
|
|
return nil, errors.New("用户不存在")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
token, err := utils.GenerateToken(user.ID, user.Username, user.Role, config.AppConfig.JWT.Secret, config.AppConfig.JWT.Expiration)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("生成JWT失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return map[string]interface{}{
|
|
|
|
|
"token": token,
|
|
|
|
|
"username": user.Username,
|
|
|
|
|
"nickname": user.Nickname,
|
|
|
|
|
"role": user.Role,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 内部方法 ==========
|
|
|
|
|
|
|
|
|
|
func (s *OpenAPIService) findApp(appID string) (*model.App, error) {
|
|
|
|
|
var app model.App
|
|
|
|
|
if err := config.DB.Where("app_id = ? AND status = 1", appID).First(&app).Error; err != nil {
|
|
|
|
|
return nil, errors.New("应用不存在或已禁用")
|
|
|
|
|
}
|
|
|
|
|
return &app, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AES 加解密
|
|
|
|
|
|
|
|
|
|
func aesEncrypt(plaintext, key string) (string, error) {
|
|
|
|
|
keyBytes := padKey(key)
|
|
|
|
|
block, err := aes.NewCipher(keyBytes)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
paddedText := pkcs7Pad([]byte(plaintext), aes.BlockSize)
|
|
|
|
|
iv := make([]byte, aes.BlockSize)
|
|
|
|
|
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mode := cipher.NewCBCEncrypter(block, iv)
|
|
|
|
|
ciphertext := make([]byte, len(paddedText))
|
|
|
|
|
mode.CryptBlocks(ciphertext, paddedText)
|
|
|
|
|
|
|
|
|
|
result := append(iv, ciphertext...)
|
|
|
|
|
return base64.StdEncoding.EncodeToString(result), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func aesDecrypt(ciphertext, key string) (string, error) {
|
|
|
|
|
keyBytes := padKey(key)
|
|
|
|
|
data, err := base64.StdEncoding.DecodeString(ciphertext)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(data) < aes.BlockSize {
|
|
|
|
|
return "", errors.New("ciphertext too short")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
block, err := aes.NewCipher(keyBytes)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
iv := data[:aes.BlockSize]
|
|
|
|
|
encrypted := data[aes.BlockSize:]
|
|
|
|
|
if len(encrypted)%aes.BlockSize != 0 {
|
|
|
|
|
return "", errors.New("ciphertext is not a multiple of block size")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mode := cipher.NewCBCDecrypter(block, iv)
|
|
|
|
|
plaintext := make([]byte, len(encrypted))
|
|
|
|
|
mode.CryptBlocks(plaintext, encrypted)
|
|
|
|
|
|
|
|
|
|
plaintext, err = pkcs7Unpad(plaintext)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return string(plaintext), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func padKey(key string) []byte {
|
|
|
|
|
keyBytes := []byte(key)
|
|
|
|
|
if len(keyBytes) >= 32 {
|
|
|
|
|
return keyBytes[:32]
|
|
|
|
|
}
|
|
|
|
|
padded := make([]byte, 32)
|
|
|
|
|
copy(padded, keyBytes)
|
|
|
|
|
return padded
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) ([]byte, error) {
|
|
|
|
|
if len(data) == 0 {
|
|
|
|
|
return nil, errors.New("empty data")
|
|
|
|
|
}
|
|
|
|
|
padding := int(data[len(data)-1])
|
|
|
|
|
if padding > aes.BlockSize || padding == 0 {
|
|
|
|
|
return nil, errors.New("invalid padding")
|
|
|
|
|
}
|
|
|
|
|
return data[:len(data)-padding], nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func generateRandomHex(length int) string {
|
|
|
|
|
bytes := make([]byte, length/2)
|
|
|
|
|
rand.Read(bytes)
|
|
|
|
|
return hex.EncodeToString(bytes)
|
|
|
|
|
}
|