package api import ( "encoding/json" "fmt" "io" "net/http" "strings" "time" "home-vue-go/internal/config" "home-vue-go/internal/database" "home-vue-go/internal/ent" "home-vue-go/internal/ent/user" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" ) type LoginRequest struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` } type Claims struct { Username string `json:"username"` jwt.RegisteredClaims } func Login(db *database.Database, cfg *config.Config) gin.HandlerFunc { return func(c *gin.Context) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "用户名和密码不能为空"}) return } ctx := c.Request.Context() user, err := db.Client.User.Query().Where(user.UsernameEQ(req.Username)).First(ctx) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"}) return } // 验证密码 if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"}) return } // 获取登录IP和用户代理 ip := c.ClientIP() userAgent := c.GetHeader("User-Agent") // 查询IP地理位置 location := queryIPLocation(ip) // 记录登录历史到内存 addLoginHistory(user.Username, ip, location, userAgent, true) // 生成JWT token claims := Claims{ Username: user.Username, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), IssuedAt: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString([]byte(cfg.JWTSecret)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "生成token失败"}) return } c.JSON(http.StatusOK, gin.H{ "token": tokenString, }) } } func ChangePassword(db *database.Database) gin.HandlerFunc { return func(c *gin.Context) { var req struct { OldPassword string `json:"oldPassword" binding:"required"` NewPassword string `json:"newPassword" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "当前密码和新密码不能为空"}) return } // 从JWT中获取用户名 username, exists := c.Get("username") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "未找到用户信息"}) return } usernameStr, ok := username.(string) if !ok || strings.TrimSpace(usernameStr) == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "用户信息无效"}) return } ctx := c.Request.Context() // 查询用户 user, err := db.Client.User.Query().Where(user.UsernameEQ(usernameStr)).First(ctx) if err != nil { if ent.IsNotFound(err) { c.JSON(http.StatusUnauthorized, gin.H{"error": "用户不存在"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "查询用户失败"}) } return } // 验证旧密码 if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword)); err != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "当前密码错误"}) return } if err := validateNewPassword(usernameStr, req.OldPassword, req.NewPassword); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 加密新密码 hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"}) return } // 更新密码到数据库 updatedUser, err := db.Client.User.UpdateOneID(user.ID).SetPassword(string(hashedPassword)).Save(ctx) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "密码更新失败"}) return } if err := bcrypt.CompareHashAndPassword([]byte(updatedUser.Password), []byte(req.NewPassword)); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "密码更新校验失败"}) return } c.JSON(http.StatusOK, gin.H{"message": "密码修改成功"}) } } func JWTAuthMiddleware(secret string) gin.HandlerFunc { return func(c *gin.Context) { tokenString := c.GetHeader("Authorization") if tokenString == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供认证token"}) c.Abort() return } // 移除 "Bearer " 前缀 if len(tokenString) > 7 && tokenString[:7] == "Bearer " { tokenString = tokenString[7:] } claims := &Claims{} token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { return []byte(secret), nil }) if err != nil || !token.Valid { // Token失效时,记录登录IP(用于统计) ip := c.ClientIP() userAgent := c.GetHeader("User-Agent") location := queryIPLocation(ip) // 记录为失败的登录尝试(token失效) addLoginHistory("", ip, location, userAgent, false) c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的token"}) c.Abort() return } c.Set("username", claims.Username) c.Next() } } // 查询IP地理位置(使用多个公共API) func queryIPLocation(ip string) string { if ip == "" || ip == "::1" || ip == "127.0.0.1" { return "本地" } // 尝试多个IP查询服务 apis := []struct { name string url string }{ {"ipapi", fmt.Sprintf("http://ip-api.com/json/%s?lang=zh-CN", ip)}, {"ipapi.co", fmt.Sprintf("https://ipapi.co/%s/json/", ip)}, {"ip.sb", fmt.Sprintf("https://api.ip.sb/geoip/%s", ip)}, } for _, api := range apis { if location := queryIPFromAPI(api.url, api.name); location != "" { return location } } return "未知" } func queryIPFromAPI(url, apiName string) string { client := &http.Client{Timeout: 3 * time.Second} resp, err := client.Get(url) if err != nil { return "" } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return "" } var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { return "" } switch apiName { case "ipapi": if country, ok := result["country"].(string); ok { region := "" if r, ok := result["regionName"].(string); ok { region = r } city := "" if c, ok := result["city"].(string); ok { city = c } parts := []string{country} if region != "" { parts = append(parts, region) } if city != "" { parts = append(parts, city) } return strings.Join(parts, " ") } case "ipapi.co": if country, ok := result["country_name"].(string); ok { city := "" if c, ok := result["city"].(string); ok { city = c } if city != "" { return fmt.Sprintf("%s %s", country, city) } return country } case "ip.sb": if country, ok := result["country"].(string); ok { city := "" if c, ok := result["city"].(string); ok { city = c } if city != "" { return fmt.Sprintf("%s %s", country, city) } return country } } return "" } // 获取登录历史 func GetLoginHistory(db *database.Database) gin.HandlerFunc { return func(c *gin.Context) { limit := c.DefaultQuery("limit", "20") limitInt := 20 if n, err := parseInt(limit); err == nil && n > 0 { limitInt = n if limitInt > 100 { limitInt = 100 } } // 从内存获取登录历史 histories := getLoginHistoryRecords(limitInt) result := make([]gin.H, 0, len(histories)) for _, h := range histories { result = append(result, gin.H{ "username": h.Username, "ip": h.IP, "location": h.Location, "userAgent": h.UserAgent, "loginTime": h.LoginTime.Format("2006-01-02 15:04:05"), "success": h.Success, }) } c.JSON(http.StatusOK, gin.H{ "data": result, "count": len(result), }) } } // 简单的整数解析 func parseInt(s string) (int, error) { var n int for _, char := range s { if char >= '0' && char <= '9' { n = n*10 + int(char-'0') } else { return 0, fmt.Errorf("invalid number") } } return n, nil }