HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 交易所技术完整体系

    • 交易所技术完整体系
    • 交易所技术架构总览
    • 交易基础概念
    • 撮合引擎原理
    • 撮合引擎实现-内存撮合
    • 撮合引擎优化 - 延迟与吞吐
    • 撮合引擎高可用
    • 清算系统设计
    • 风控系统设计
    • 资金管理系统
    • 行情系统设计
    • 去中心化交易所(DEX)设计
    • 合约交易系统
    • 数据库设计与优化
    • 缓存与消息队列
    • 用户系统与KYC
    • 交易所API设计
    • 监控与告警系统
    • 安全防护与攻防
    • 高可用架构设计
    • 压力测试与性能优化
    • 项目实战-完整交易所实现

用户系统与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/日
L3L2+地址证明充值:无限制
提现:无限制
交易:无限制

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认证:

  1. 用户注册登录:验证码、密码加密、JWT Token
  2. 双因素认证:TOTP实现、备用码
  3. KYC认证:分级认证、OCR识别、人脸识别
  4. API密钥管理:生成、签名验证
  5. 安全加固:限流、设备指纹、异地登录检测

下一章将讲解监控与告警系统。

Prev
缓存与消息队列
Next
交易所API设计