|
|
@@ -1,13 +1,71 @@
|
|
|
package controllers
|
|
|
|
|
|
import (
|
|
|
+ "crypto/hmac"
|
|
|
+ "crypto/sha256"
|
|
|
+ "encoding/hex"
|
|
|
+ "encoding/json"
|
|
|
+ "fmt"
|
|
|
+ "io"
|
|
|
+ "sort"
|
|
|
+ "strings"
|
|
|
+ "time"
|
|
|
+
|
|
|
"ems-backend/models"
|
|
|
"ems-backend/utils"
|
|
|
"net/http"
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
+ "gorm.io/gorm"
|
|
|
+)
|
|
|
+
|
|
|
+const (
|
|
|
+ SSOAppID = "app_e3d1afe340085e24"
|
|
|
+ SSOAppSecret = "xnbJjZghQzZ0gPkc7e6ngirrAZq0oXum"
|
|
|
+ SSOValidateURL = "https://api.hnyunzhu.com/api/v1/simple/validate"
|
|
|
)
|
|
|
|
|
|
+type SSOUserStruct struct {
|
|
|
+ Username string
|
|
|
+ Name string
|
|
|
+ Email string
|
|
|
+}
|
|
|
+
|
|
|
+// FindOrCreateUserFromSSO handles user creation for SSO login
|
|
|
+func FindOrCreateUserFromSSO(ssoUser SSOUserStruct) (*models.User, error) {
|
|
|
+ var user models.User
|
|
|
+
|
|
|
+ // 1. Check if user exists by unique identifier
|
|
|
+ err := models.DB.Where("username = ?", ssoUser.Username).First(&user).Error
|
|
|
+
|
|
|
+ if err == nil {
|
|
|
+ // User exists
|
|
|
+ return &user, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ if err == gorm.ErrRecordNotFound {
|
|
|
+ // 2. User not found, create a NEW user without permissions
|
|
|
+ newUser := models.User{
|
|
|
+ Username: ssoUser.Username,
|
|
|
+ Name: ssoUser.Name,
|
|
|
+ Status: "0", // Normal status
|
|
|
+ }
|
|
|
+
|
|
|
+ if ssoUser.Email != "" {
|
|
|
+ email := ssoUser.Email
|
|
|
+ newUser.Email = &email
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := models.DB.Create(&newUser).Error; err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ return &newUser, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ return nil, err
|
|
|
+}
|
|
|
+
|
|
|
type LoginRequest struct {
|
|
|
Username string `json:"username" binding:"required"`
|
|
|
Password string `json:"password" binding:"required"`
|
|
|
@@ -27,7 +85,7 @@ func Login(c *gin.Context) {
|
|
|
|
|
|
// 1. Database User Check
|
|
|
var user models.User
|
|
|
- // 使用 MD5 或其他加密方式比较密码(这里为了演示先用明文,实际项目请使用 bcrypt)
|
|
|
+ // 使用 MD5 或其他加密方式比较密码
|
|
|
if err := models.DB.Where("username = ? AND password = ?", req.Username, req.Password).First(&user).Error; err != nil {
|
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
|
|
|
return
|
|
|
@@ -104,3 +162,140 @@ func UpdateProfilePwd(c *gin.Context) {
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Password updated successfully"})
|
|
|
}
|
|
|
|
|
|
+type SSOLoginRequest struct {
|
|
|
+ Ticket string `json:"ticket" binding:"required"`
|
|
|
+}
|
|
|
+
|
|
|
+type SSOResponse struct {
|
|
|
+ Valid bool `json:"valid"`
|
|
|
+ UserID int `json:"user_id"`
|
|
|
+ Mobile string `json:"mobile"`
|
|
|
+ MappedKey string `json:"mapped_key"`
|
|
|
+ MappedEmail string `json:"mapped_email"`
|
|
|
+}
|
|
|
+
|
|
|
+func generateSSOSignature(params map[string]interface{}, secret string) string {
|
|
|
+ // 1. Sort keys
|
|
|
+ keys := make([]string, 0, len(params))
|
|
|
+ for k := range params {
|
|
|
+ keys = append(keys, k)
|
|
|
+ }
|
|
|
+ sort.Strings(keys)
|
|
|
+
|
|
|
+ // 2. Concatenate
|
|
|
+ var sb strings.Builder
|
|
|
+ for _, k := range keys {
|
|
|
+ if k == "sign" {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ // Filter out nil/empty if needed, but docs say "filter empty values and sign"
|
|
|
+ // Assuming non-empty for simplicity or add check
|
|
|
+ val := params[k]
|
|
|
+ if val == nil {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ strVal := fmt.Sprintf("%v", val)
|
|
|
+ if strVal == "" {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ if sb.Len() > 0 {
|
|
|
+ sb.WriteString("&")
|
|
|
+ }
|
|
|
+ sb.WriteString(k + "=" + strVal)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Debug log: String to sign
|
|
|
+ fmt.Printf("SSO Signing String: %s\n", sb.String())
|
|
|
+
|
|
|
+ // 3. HMAC-SHA256
|
|
|
+ h := hmac.New(sha256.New, []byte(secret))
|
|
|
+ h.Write([]byte(sb.String()))
|
|
|
+ return hex.EncodeToString(h.Sum(nil))
|
|
|
+}
|
|
|
+
|
|
|
+func SSOLogin(c *gin.Context) {
|
|
|
+ var req SSOLoginRequest
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. Build validation request
|
|
|
+ timestamp := time.Now().Unix()
|
|
|
+ payload := map[string]interface{}{
|
|
|
+ "app_id": SSOAppID,
|
|
|
+ "ticket": req.Ticket,
|
|
|
+ "timestamp": timestamp,
|
|
|
+ }
|
|
|
+
|
|
|
+ // Calculate signature
|
|
|
+ sign := generateSSOSignature(payload, SSOAppSecret)
|
|
|
+ payload["sign"] = sign
|
|
|
+
|
|
|
+ // 2. Call UAP Validate API
|
|
|
+ client := &http.Client{Timeout: 10 * time.Second}
|
|
|
+ jsonBody, _ := json.Marshal(payload)
|
|
|
+
|
|
|
+ resp, err := client.Post(SSOValidateURL, "application/json", strings.NewReader(string(jsonBody)))
|
|
|
+ if err != nil {
|
|
|
+ c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to connect to SSO provider"})
|
|
|
+ return
|
|
|
+ }
|
|
|
+ defer resp.Body.Close()
|
|
|
+
|
|
|
+ if resp.StatusCode != 200 {
|
|
|
+ bodyBytes, _ := io.ReadAll(resp.Body)
|
|
|
+ fmt.Printf("SSO Validation Failed: Status=%d, Body=%s\n", resp.StatusCode, string(bodyBytes))
|
|
|
+ c.JSON(resp.StatusCode, gin.H{"error": "SSO Validation Failed", "details": string(bodyBytes)})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. Parse Response
|
|
|
+ var ssoResp SSOResponse
|
|
|
+ bodyBytes, _ := io.ReadAll(resp.Body)
|
|
|
+ if err := json.Unmarshal(bodyBytes, &ssoResp); err != nil {
|
|
|
+ fmt.Printf("SSO Parse Error: %v, Body=%s\n", err, string(bodyBytes))
|
|
|
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse SSO response"})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if !ssoResp.Valid {
|
|
|
+ fmt.Printf("SSO Invalid Ticket: %+v\n", ssoResp)
|
|
|
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid SSO Ticket"})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. Find or Create User
|
|
|
+ // Strict: Only use MappedKey
|
|
|
+ if ssoResp.MappedKey == "" {
|
|
|
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "SSO Login Failed: No mapped user key provided"})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ ssoUser := SSOUserStruct{
|
|
|
+ Username: ssoResp.MappedKey,
|
|
|
+ Name: ssoResp.MappedKey,
|
|
|
+ Email: ssoResp.MappedEmail,
|
|
|
+ }
|
|
|
+
|
|
|
+ user, err := FindOrCreateUserFromSSO(ssoUser)
|
|
|
+ if err != nil {
|
|
|
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process user: " + err.Error()})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. Generate Token
|
|
|
+ token, err := utils.GenerateToken(*user)
|
|
|
+ if err != nil {
|
|
|
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ user.Password = ""
|
|
|
+ c.JSON(http.StatusOK, LoginResponse{
|
|
|
+ Token: token,
|
|
|
+ User: *user,
|
|
|
+ })
|
|
|
+}
|