Files
jiang13-bbs/backend/service/ua.go
freefire 3a7bfe59df feat: 安全设置展示有效登录会话,支持剔除其它设备
按设备指纹去重并直出 IP;非当前会话可踢下线,对方需重新输入密码。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-18 01:48:52 +08:00

123 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"net/netip"
"strings"
)
// DeviceKind 设备形态(前端据此选图标)
const (
DeviceKindComputer = "computer"
DeviceKindMobile = "mobile"
DeviceKindTablet = "tablet"
)
// ParsedUA 从 User-Agent 抽出的可读设备信息
type ParsedUA struct {
Kind string // computer | mobile | tablet
OS string // Windows / macOS / Android / iOS / Linux
Label string // Windows 电脑 / Android 手机
Browser string
}
// ParseUserAgent 轻量 UA 解析,覆盖常见桌面/移动浏览器,不引入第三方库。
func ParseUserAgent(ua string) ParsedUA {
u := strings.TrimSpace(ua)
if u == "" {
return ParsedUA{Kind: DeviceKindComputer, OS: "未知", Label: "未知设备", Browser: "未知浏览器"}
}
low := strings.ToLower(u)
kind, osName, label := classifyDevice(u, low)
browser := classifyBrowser(u, low)
return ParsedUA{Kind: kind, OS: osName, Label: label, Browser: browser}
}
func classifyDevice(u, low string) (kind, osName, label string) {
hasAndroid := strings.Contains(low, "android")
hasIPhone := strings.Contains(low, "iphone")
hasIPad := strings.Contains(low, "ipad") || (strings.Contains(low, "macintosh") && strings.Contains(low, "mobile"))
hasMobileToken := strings.Contains(low, "mobile") || strings.Contains(u, "Mobile")
switch {
case hasIPad:
return DeviceKindTablet, "iOS", "iPad"
case hasIPhone:
return DeviceKindMobile, "iOS", "iOS 手机"
case hasAndroid:
if strings.Contains(low, "tablet") || !hasMobileToken {
return DeviceKindTablet, "Android", "Android 平板"
}
return DeviceKindMobile, "Android", "Android 手机"
case strings.Contains(low, "windows"):
return DeviceKindComputer, "Windows", "Windows 电脑"
case strings.Contains(low, "mac os") || strings.Contains(low, "macintosh"):
return DeviceKindComputer, "macOS", "macOS 电脑"
case strings.Contains(low, "cros"):
return DeviceKindComputer, "ChromeOS", "ChromeOS 电脑"
case strings.Contains(low, "linux"):
return DeviceKindComputer, "Linux", "Linux 电脑"
default:
if hasMobileToken {
return DeviceKindMobile, "未知", "手机"
}
return DeviceKindComputer, "未知", "电脑"
}
}
func classifyBrowser(u, low string) string {
switch {
case strings.Contains(u, "MicroMessenger"):
return "微信"
case strings.Contains(low, "edg/") || strings.Contains(low, "edgios/") || strings.Contains(low, "edga/"):
return "Microsoft Edge"
case strings.Contains(low, "opr/") || strings.Contains(low, "opera"):
return "Opera"
case strings.Contains(low, "firefox/") || strings.Contains(low, "fxios/"):
return "Firefox"
case strings.Contains(low, "crios/"):
return "Google Chrome"
case strings.Contains(low, "chrome/") && !strings.Contains(low, "edg"):
return "Google Chrome"
case strings.Contains(low, "safari/") && !strings.Contains(low, "chrome") && !strings.Contains(low, "chromium"):
return "Safari"
default:
return "未知浏览器"
}
}
func uaFingerprint(p ParsedUA) string {
return p.Kind + "|" + p.OS + "|" + p.Browser
}
func parseIPAddr(ip string) (netip.Addr, bool) {
ip = strings.TrimSpace(ip)
if ip == "" {
return netip.Addr{}, false
}
if i := strings.IndexByte(ip, '%'); i >= 0 {
ip = ip[:i]
}
addr, err := netip.ParseAddr(ip)
if err != nil {
return netip.Addr{}, false
}
return addr.Unmap(), true
}
// canonicalIP 归一化 IP:IPv4-mapped 解开;本机 ::1 / 127.0.0.1 视为同一地址。
func canonicalIP(ip string) string {
addr, ok := parseIPAddr(ip)
if !ok {
return strings.TrimSpace(ip)
}
if addr.IsLoopback() {
return "127.0.0.1"
}
return addr.String()
}
func deviceFingerprint(ip, ua string) string {
return canonicalIP(ip) + "|" + uaFingerprint(ParseUserAgent(ua))
}