按设备指纹去重并直出 IP;非当前会话可踢下线,对方需重新输入密码。 Co-authored-by: Cursor <cursoragent@cursor.com>
123 lines
3.6 KiB
Go
123 lines
3.6 KiB
Go
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))
|
||
}
|