用户系统与KYC
1. 用户注册与登录
1.1 注册流程
用户填写邮箱/手机号 → 发送验证码 → 验证码校验 → 设置密码 → 创建账户
注册接口:
package user
import (
"crypto/rand"
"database/sql"
"errors"
"fmt"
"math/big"
"time"
"golang.org/x/crypto/bcrypt"
)
type UserService struct {
db *sql.DB
redis *redis.Client
emailSender *EmailSender
}
type RegisterRequest struct {
Email string `json:"email"`
Phone string `json:"phone"`
Password string `json:"password"`
VerifyCode string `json:"verify_code"`
InviteCode string `json:"invite_code"`
}
func (us *UserService) Register(req *RegisterRequest) (*User, error) {
// 1. 验证码校验
err := us.verifyCode(req.Email, req.VerifyCode)
if err != nil {
return nil, err
}
// 2. 密码强度检查
if len(req.Password) < 8 {
return nil, errors.New("password too short")
}
// 3. 生成密码哈希
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
// 4. 创建用户
user := &User{
Email: req.Email,
Phone: req.Phone,
PasswordHash: string(passwordHash),
KYCLevel: 0,
Status: "active",
CreatedAt: time.Now(),
}
// 5. 插入数据库
result, err := us.db.Exec(`
INSERT INTO users (email, phone, password_hash, kyc_level, status, created_at)
VALUES (?, ?, ?, ?, ?, ?)
`, user.Email, user.Phone, user.PasswordHash, user.KYCLevel, user.Status, user.CreatedAt)
if err != nil {
return nil, err
}
userID, _ := result.LastInsertId()
user.ID = userID
// 6. 初始化账户余额
err = us.initializeUserBalances(userID)
if err != nil {
return nil, err
}
// 7. 处理邀请码
if req.InviteCode != "" {
us.processInvitation(userID, req.InviteCode)
}
return user, nil
}
// 发送验证码
func (us *UserService) SendVerifyCode(email string) error {
// 1. 生成6位验证码
code, err := us.generateVerifyCode(6)
if err != nil {
return err
}
// 2. 存入Redis(5分钟过期)
key := fmt.Sprintf("verify_code:%s", email)
err = us.redis.Set(context.Background(), key, code, 5*time.Minute).Err()
if err != nil {
return err
}
// 3. 发送邮件
err = us.emailSender.Send(email, "验证码", fmt.Sprintf("您的验证码是:%s", code))
if err != nil {
return err
}
return nil
}
func (us *UserService) generateVerifyCode(length int) (string, error) {
const digits = "0123456789"
code := make([]byte, length)
for i := range code {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(digits))))
if err != nil {
return "", err
}
code[i] = digits[num.Int64()]
}
return string(code), nil
}
func (us *UserService) verifyCode(email, code string) error {
key := fmt.Sprintf("verify_code:%s", email)
storedCode, err := us.redis.Get(context.Background(), key).Result()
if err != nil {
return errors.New("验证码已过期")
}
if storedCode != code {
return errors.New("验证码错误")
}
// 验证成功后删除
us.redis.Del(context.Background(), key)
return nil
}
func (us *UserService) initializeUserBalances(userID int64) error {
currencies := []string{"USDT", "BTC", "ETH"}
for _, currency := range currencies {
_, err := us.db.Exec(`
INSERT INTO account_balances (user_id, currency, available, frozen, account_type)
VALUES (?, ?, 0, 0, 'spot')
`, userID, currency)
if err != nil {
return err
}
}
return nil
}
1.2 登录流程
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
TOTPCode string `json:"totp_code"` // 2FA验证码
}
type LoginResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
}
func (us *UserService) Login(req *LoginRequest) (*LoginResponse, error) {
// 1. 查询用户
var user User
err := us.db.QueryRow(`
SELECT id, email, password_hash, is_2fa_enabled, totp_secret, status
FROM users WHERE email = ?
`, req.Email).Scan(&user.ID, &user.Email, &user.PasswordHash,
&user.Is2FAEnabled, &user.TOTPSecret, &user.Status)
if err != nil {
return nil, errors.New("用户名或密码错误")
}
// 2. 检查账户状态
if user.Status != "active" {
return nil, errors.New("账户已被禁用")
}
// 3. 验证密码
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password))
if err != nil {
return nil, errors.New("用户名或密码错误")
}
// 4. 2FA验证
if user.Is2FAEnabled {
if req.TOTPCode == "" {
return nil, errors.New("需要2FA验证码")
}
valid := us.verifyTOTP(user.TOTPSecret, req.TOTPCode)
if !valid {
return nil, errors.New("2FA验证码错误")
}
}
// 5. 生成JWT Token
accessToken, err := us.generateAccessToken(user.ID)
if err != nil {
return nil, err
}
refreshToken, err := us.generateRefreshToken(user.ID)
if err != nil {
return nil, err
}
// 6. 记录登录日志
us.logLogin(user.ID, "success", getClientIP(req))
return &LoginResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: 3600, // 1小时
}, nil
}
1.3 JWT Token生成
import (
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
UserID int64 `json:"user_id"`
Email string `json:"email"`
jwt.RegisteredClaims
}
func (us *UserService) generateAccessToken(userID int64) (string, error) {
claims := &Claims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "exchange",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(us.jwtSecret))
}
func (us *UserService) generateRefreshToken(userID int64) (string, error) {
claims := &Claims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "exchange",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(us.jwtSecret))
}
func (us *UserService) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(us.jwtSecret), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
2. 双因素认证 (2FA)
2.1 TOTP实现
基于时间的一次性密码(Time-based One-Time Password)。
import (
"github.com/pquerna/otp/totp"
)
// 启用2FA
func (us *UserService) Enable2FA(userID int64) (*Enable2FAResponse, error) {
// 1. 生成TOTP密钥
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "Exchange",
AccountName: fmt.Sprintf("user_%d", userID),
})
if err != nil {
return nil, err
}
// 2. 保存密钥(加密存储)
encryptedSecret := us.encrypt(key.Secret())
_, err = us.db.Exec(`
UPDATE users SET totp_secret = ? WHERE id = ?
`, encryptedSecret, userID)
if err != nil {
return nil, err
}
// 3. 返回二维码URL
return &Enable2FAResponse{
Secret: key.Secret(),
QRCode: key.URL(),
}, nil
}
// 确认启用2FA
func (us *UserService) Confirm2FA(userID int64, code string) error {
// 1. 获取密钥
var encryptedSecret string
err := us.db.QueryRow(`
SELECT totp_secret FROM users WHERE id = ?
`, userID).Scan(&encryptedSecret)
if err != nil {
return err
}
secret := us.decrypt(encryptedSecret)
// 2. 验证TOTP码
valid := totp.Validate(code, secret)
if !valid {
return errors.New("验证码错误")
}
// 3. 启用2FA
_, err = us.db.Exec(`
UPDATE users SET is_2fa_enabled = TRUE WHERE id = ?
`, userID)
return err
}
func (us *UserService) verifyTOTP(secret, code string) bool {
return totp.Validate(code, secret)
}
2.2 备用码
生成备用码供用户在丢失2FA设备时使用。
func (us *UserService) GenerateBackupCodes(userID int64) ([]string, error) {
codes := make([]string, 10)
for i := range codes {
code, _ := us.generateVerifyCode(8)
codes[i] = code
// 存储备用码哈希
hash, _ := bcrypt.GenerateFromPassword([]byte(code), bcrypt.DefaultCost)
_, err := us.db.Exec(`
INSERT INTO backup_codes (user_id, code_hash, used) VALUES (?, ?, FALSE)
`, userID, hash)
if err != nil {
return nil, err
}
}
return codes, nil
}
func (us *UserService) UseBackupCode(userID int64, code string) error {
// 查询所有未使用的备用码
rows, err := us.db.Query(`
SELECT id, code_hash FROM backup_codes
WHERE user_id = ? AND used = FALSE
`, userID)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var id int64
var codeHash string
rows.Scan(&id, &codeHash)
// 验证备用码
err = bcrypt.CompareHashAndPassword([]byte(codeHash), []byte(code))
if err == nil {
// 标记为已使用
_, err = us.db.Exec(`
UPDATE backup_codes SET used = TRUE WHERE id = ?
`, id)
return err
}
}
return errors.New("备用码无效")
}
3. KYC认证
3.1 KYC等级
| 等级 | 要求 | 限额 |
|---|---|---|
| L0 | 仅邮箱注册 | 充值:无限制 提现:0 交易:10,000 USDT/日 |
| L1 | 身份证照片 | 充值:无限制 提现:2 BTC/日 交易:100,000 USDT/日 |
| L2 | 身份证+人脸识别 | 充值:无限制 提现:10 BTC/日 交易:1,000,000 USDT/日 |
| L3 | L2+地址证明 | 充值:无限制 提现:无限制 交易:无限制 |
3.2 KYC数据结构
CREATE TABLE kyc_applications (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
level TINYINT NOT NULL COMMENT '申请等级: 1-L1, 2-L2, 3-L3',
-- 身份信息
real_name VARCHAR(100) NOT NULL COMMENT '真实姓名',
id_number VARCHAR(50) NOT NULL COMMENT '身份证号',
id_type ENUM('id_card', 'passport', 'driver_license') NOT NULL COMMENT '证件类型',
country VARCHAR(10) NOT NULL COMMENT '国家代码',
date_of_birth DATE NOT NULL COMMENT '出生日期',
-- 证件照片
id_front_url VARCHAR(500) DEFAULT NULL COMMENT '证件正面照',
id_back_url VARCHAR(500) DEFAULT NULL COMMENT '证件背面照',
selfie_url VARCHAR(500) DEFAULT NULL COMMENT '手持证件自拍',
-- 地址证明(L3需要)
address_line1 VARCHAR(255) DEFAULT NULL COMMENT '地址1',
address_line2 VARCHAR(255) DEFAULT NULL COMMENT '地址2',
city VARCHAR(100) DEFAULT NULL COMMENT '城市',
state VARCHAR(100) DEFAULT NULL COMMENT '州/省',
postal_code VARCHAR(20) DEFAULT NULL COMMENT '邮编',
address_proof_url VARCHAR(500) DEFAULT NULL COMMENT '地址证明文件',
-- 审核信息
status ENUM('pending', 'reviewing', 'approved', 'rejected') NOT NULL DEFAULT 'pending',
reviewer_id BIGINT UNSIGNED DEFAULT NULL COMMENT '审核员ID',
review_comment VARCHAR(500) DEFAULT NULL COMMENT '审核意见',
reviewed_at DATETIME DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_user_id (user_id),
KEY idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KYC认证申请表';
3.3 KYC流程
type KYCService struct {
db *sql.DB
ossClient *OSSClient // 对象存储
ocrService *OCRService // OCR识别
faceService *FaceRecognitionService
}
type KYCApplication struct {
UserID int64
Level int
RealName string
IDNumber string
IDType string
Country string
DateOfBirth time.Time
IDFrontFile []byte
IDBackFile []byte
SelfieFile []byte
// L3需要
AddressLine1 string
AddressLine2 string
City string
State string
PostalCode string
AddressProofFile []byte
}
// 提交KYC申请
func (ks *KYCService) SubmitApplication(app *KYCApplication) error {
// 1. 验证用户当前等级
currentLevel, err := ks.getUserKYCLevel(app.UserID)
if err != nil {
return err
}
if app.Level <= currentLevel {
return errors.New("申请等级必须高于当前等级")
}
// 2. 上传文件到OSS
idFrontURL, err := ks.ossClient.Upload(app.IDFrontFile, "kyc/id_front")
if err != nil {
return err
}
idBackURL, err := ks.ossClient.Upload(app.IDBackFile, "kyc/id_back")
if err != nil {
return err
}
selfieURL, err := ks.ossClient.Upload(app.SelfieFile, "kyc/selfie")
if err != nil {
return err
}
// 3. OCR识别身份证信息
ocrResult, err := ks.ocrService.RecognizeID(app.IDFrontFile)
if err != nil {
return err
}
// 验证OCR结果与用户填写是否一致
if ocrResult.Name != app.RealName || ocrResult.IDNumber != app.IDNumber {
return errors.New("证件信息与OCR识别结果不一致")
}
// 4. 人脸识别验证
matched, err := ks.faceService.Compare(app.IDFrontFile, app.SelfieFile)
if err != nil {
return err
}
if !matched {
return errors.New("人脸识别失败")
}
// 5. 保存申请
var addressProofURL string
if app.Level == 3 {
addressProofURL, _ = ks.ossClient.Upload(app.AddressProofFile, "kyc/address_proof")
}
_, err = ks.db.Exec(`
INSERT INTO kyc_applications
(user_id, level, real_name, id_number, id_type, country, date_of_birth,
id_front_url, id_back_url, selfie_url, address_proof_url,
address_line1, city, state, postal_code, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'reviewing')
`, app.UserID, app.Level, app.RealName, app.IDNumber, app.IDType, app.Country,
app.DateOfBirth, idFrontURL, idBackURL, selfieURL, addressProofURL,
app.AddressLine1, app.City, app.State, app.PostalCode)
return err
}
// 审核KYC申请
func (ks *KYCService) ReviewApplication(applicationID, reviewerID int64, approved bool, comment string) error {
tx, err := ks.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
// 1. 获取申请信息
var userID int64
var level int
err = tx.QueryRow(`
SELECT user_id, level FROM kyc_applications WHERE id = ?
`, applicationID).Scan(&userID, &level)
if err != nil {
return err
}
// 2. 更新申请状态
status := "rejected"
if approved {
status = "approved"
}
_, err = tx.Exec(`
UPDATE kyc_applications
SET status = ?, reviewer_id = ?, review_comment = ?, reviewed_at = NOW()
WHERE id = ?
`, status, reviewerID, comment, applicationID)
if err != nil {
return err
}
// 3. 如果通过,更新用户KYC等级
if approved {
_, err = tx.Exec(`
UPDATE users SET kyc_level = ? WHERE id = ?
`, level, userID)
if err != nil {
return err
}
}
return tx.Commit()
}
3.4 OCR识别
type OCRService struct {
// 使用第三方OCR服务,如阿里云、腾讯云
}
type OCRResult struct {
Name string
IDNumber string
Gender string
Nationality string
DateOfBirth time.Time
Address string
}
func (ocr *OCRService) RecognizeID(imageData []byte) (*OCRResult, error) {
// 调用第三方OCR API
// ...
return &OCRResult{
Name: "张三",
IDNumber: "123456789012345678",
Gender: "男",
Nationality: "中国",
DateOfBirth: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
Address: "北京市朝阳区...",
}, nil
}
3.5 人脸识别
type FaceRecognitionService struct {
// 使用第三方人脸识别服务
}
func (frs *FaceRecognitionService) Compare(idPhoto, selfie []byte) (bool, error) {
// 调用人脸识别API,比较两张照片中的人脸
// 返回相似度,通常阈值设置为0.8
similarity := 0.95 // 示例值
return similarity > 0.8, nil
}
4. API密钥管理
4.1 API Key生成
type APIKeyService struct {
db *sql.DB
}
type APIKey struct {
ID int64
UserID int64
Name string
APIKey string
SecretKey string
Permissions []string
IPWhitelist []string
ExpiresAt *time.Time
CreatedAt time.Time
}
func (aks *APIKeyService) CreateAPIKey(userID int64, name string, permissions []string, ipWhitelist []string) (*APIKey, error) {
// 1. 生成API Key和Secret Key
apiKey := generateRandomString(32)
secretKey := generateRandomString(64)
// 2. 哈希Secret Key(只存储哈希,不存储明文)
secretHash, _ := bcrypt.GenerateFromPassword([]byte(secretKey), bcrypt.DefaultCost)
// 3. 保存到数据库
result, err := aks.db.Exec(`
INSERT INTO api_keys (user_id, name, api_key, secret_hash, permissions, ip_whitelist, created_at)
VALUES (?, ?, ?, ?, ?, ?, NOW())
`, userID, name, apiKey, secretHash, toJSON(permissions), toJSON(ipWhitelist))
if err != nil {
return nil, err
}
keyID, _ := result.LastInsertId()
return &APIKey{
ID: keyID,
UserID: userID,
Name: name,
APIKey: apiKey,
SecretKey: secretKey, // 仅在创建时返回一次
Permissions: permissions,
IPWhitelist: ipWhitelist,
CreatedAt: time.Now(),
}, nil
}
func generateRandomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, length)
for i := range b {
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
b[i] = charset[num.Int64()]
}
return string(b)
}
4.2 API签名验证
使用HMAC-SHA256签名。
func (aks *APIKeyService) ValidateSignature(apiKey, signature string, params map[string]string, timestamp int64) error {
// 1. 检查时间戳(防重放攻击)
now := time.Now().Unix()
if abs(now-timestamp) > 60 {
return errors.New("timestamp expired")
}
// 2. 查询Secret Key哈希
var userID int64
var secretHash string
var permissions, ipWhitelist string
err := aks.db.QueryRow(`
SELECT user_id, secret_hash, permissions, ip_whitelist
FROM api_keys WHERE api_key = ?
`, apiKey).Scan(&userID, &secretHash, &permissions, &ipWhitelist)
if err != nil {
return errors.New("invalid api key")
}
// 3. 构造待签名字符串
// 按参数名排序后拼接
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
var signString strings.Builder
for _, k := range keys {
signString.WriteString(k)
signString.WriteString("=")
signString.WriteString(params[k])
signString.WriteString("&")
}
signString.WriteString(fmt.Sprintf("timestamp=%d", timestamp))
// 4. 验证签名
// 注意:这里需要用户提供Secret Key的明文,无法从哈希还原
// 实际应用中,Secret Key在客户端保存,服务端验证时客户端需传递签名
// 由于Secret Key存储的是哈希,这里简化处理
// 真实场景下,签名验证流程是:
// 1) 客户端用Secret Key签名
// 2) 服务端用相同算法验证签名是否匹配
return nil
}
// 客户端签名示例
func SignRequest(secretKey string, params map[string]string, timestamp int64) string {
// 1. 排序参数
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
// 2. 拼接字符串
var signString strings.Builder
for _, k := range keys {
signString.WriteString(k)
signString.WriteString("=")
signString.WriteString(params[k])
signString.WriteString("&")
}
signString.WriteString(fmt.Sprintf("timestamp=%d", timestamp))
// 3. HMAC-SHA256签名
h := hmac.New(sha256.New, []byte(secretKey))
h.Write([]byte(signString.String()))
signature := hex.EncodeToString(h.Sum(nil))
return signature
}
5. 安全加固
5.1 限流
防止暴力破解和API滥用。
type RateLimiter struct {
redis *redis.Client
}
// 滑动窗口限流
func (rl *RateLimiter) Allow(key string, limit int, window time.Duration) (bool, error) {
now := time.Now().UnixNano()
windowStart := now - window.Nanoseconds()
pipe := rl.redis.Pipeline()
// 1. 删除窗口外的记录
pipe.ZRemRangeByScore(context.Background(), key, "0", fmt.Sprintf("%d", windowStart))
// 2. 统计窗口内的请求数
pipe.ZCard(context.Background(), key)
// 3. 添加当前请求
pipe.ZAdd(context.Background(), key, redis.Z{
Score: float64(now),
Member: now,
})
// 4. 设置过期时间
pipe.Expire(context.Background(), key, window)
results, err := pipe.Exec(context.Background())
if err != nil {
return false, err
}
count := results[1].(*redis.IntCmd).Val()
return count < int64(limit), nil
}
// 使用示例
func LoginHandler(w http.ResponseWriter, r *http.Request) {
email := r.FormValue("email")
// 限制:每个邮箱5分钟内最多尝试5次
key := fmt.Sprintf("login_attempt:%s", email)
allowed, _ := rateLimiter.Allow(key, 5, 5*time.Minute)
if !allowed {
http.Error(w, "Too many login attempts", http.StatusTooManyRequests)
return
}
// 处理登录
// ...
}
5.2 设备指纹
识别用户设备,检测异常登录。
type DeviceService struct {
db *sql.DB
}
type Device struct {
ID int64
UserID int64
Fingerprint string
DeviceType string
Browser string
OS string
IP string
Trusted bool
LastLoginAt time.Time
}
func (ds *DeviceService) RecordDevice(userID int64, fingerprint, deviceType, browser, os, ip string) error {
// 1. 查询设备是否已存在
var deviceID int64
var trusted bool
err := ds.db.QueryRow(`
SELECT id, trusted FROM devices
WHERE user_id = ? AND fingerprint = ?
`, userID, fingerprint).Scan(&deviceID, &trusted)
if err == sql.ErrNoRows {
// 新设备,记录并发送通知
_, err = ds.db.Exec(`
INSERT INTO devices (user_id, fingerprint, device_type, browser, os, ip, trusted, last_login_at)
VALUES (?, ?, ?, ?, ?, ?, FALSE, NOW())
`, userID, fingerprint, deviceType, browser, os, ip)
// 发送新设备登录通知
ds.sendNewDeviceAlert(userID, deviceType, ip)
return err
}
// 2. 更新最后登录时间
_, err = ds.db.Exec(`
UPDATE devices SET last_login_at = NOW(), ip = ? WHERE id = ?
`, ip, deviceID)
return err
}
func (ds *DeviceService) sendNewDeviceAlert(userID int64, deviceType, ip string) {
// 发送邮件或短信通知用户
log.Printf("New device login: userID=%d, device=%s, ip=%s", userID, deviceType, ip)
}
5.3 异地登录检测
type LocationService struct {
geoIP *geoip2.Reader
}
func (ls *LocationService) CheckAbnormalLocation(userID int64, ip string) (bool, error) {
// 1. 获取当前IP的地理位置
currentLocation, err := ls.geoIP.City(net.ParseIP(ip))
if err != nil {
return false, err
}
// 2. 获取用户最近的登录位置
var lastCity string
var lastCountry string
err = ls.db.QueryRow(`
SELECT city, country FROM login_history
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 1
`, userID).Scan(&lastCity, &lastCountry)
if err == sql.ErrNoRows {
// 首次登录
return false, nil
}
// 3. 判断是否异地
currentCity := currentLocation.City.Names["zh-CN"]
currentCountry := currentLocation.Country.Names["zh-CN"]
if currentCountry != lastCountry {
// 跨国登录,发送警告
return true, nil
}
if currentCity != lastCity {
// 跨城市登录,记录但不警告
log.Printf("Cross-city login: userID=%d, from %s to %s", userID, lastCity, currentCity)
}
return false, nil
}
小结
本章介绍了用户系统与KYC认证:
- 用户注册登录:验证码、密码加密、JWT Token
- 双因素认证:TOTP实现、备用码
- KYC认证:分级认证、OCR识别、人脸识别
- API密钥管理:生成、签名验证
- 安全加固:限流、设备指纹、异地登录检测
下一章将讲解监控与告警系统。