Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第四十七变 认证

七十二变,变的是形;真身难辨,靠的是证。

认证之道,在于确认“你是谁“——从古老的口令到现代的生物特征,从单点信任到多因素验证,人类在身份验证的道路上不断探索更安全、更便捷的方案。

47.1 认证的基本概念

47.1.1 身份识别与身份验证

在信息安全领域,身份识别(Identification)身份验证(Authentication) 是两个密切相关但本质不同的概念:

  • 身份识别:用户声称自己是谁,提供标识符(如用户名、邮箱、身份证号)。这是一个单向声明的过程。
  • 身份验证:系统验证用户的声明是否属实,确认该用户确实是其所声称的那个人。

类比现实场景:你走进一栋大楼,对保安说“我是张三“——这是身份识别;保安查看你的工作证并与本人比对——这是身份验证。只有两者结合,才能建立可信的身份确认。

47.1.2 认证、授权与审计

安全领域常提及“AAA“框架:

缩写全称含义核心问题
AAuthentication(认证)验证用户身份你是谁?
AAuthorization(授权)决定用户能做什么你能做什么?
AAccounting/Auditing(审计)记录用户做了什么你做了什么?

这三者构成完整的安全访问控制链条:先确认身份,再分配权限,最后记录行为。本章聚焦认证,下一章将深入授权。

47.2 认证因素

认证因素(Authentication Factors)是用于验证身份的凭据类别。根据NIST(美国国家标准与技术研究院)的定义,认证因素分为以下几类:

47.2.1 知识因素(Something You Know)

用户知道的信息,是最传统、最广泛使用的认证方式:

  • 密码/口令:字符串形式的秘密
  • PIN码:数字形式的短密码
  • 安全问题:如“你的第一只宠物叫什么名字“

优点:实现简单,无需额外硬件。 缺点:可被猜测、窃取、社会工程学攻击;用户倾向于选择弱密码或在多站点复用。

47.2.2 持有因素(Something You Have)

用户拥有的物理设备:

  • 硬件令牌:如RSA SecurID、YubiKey
  • 智能卡:嵌入芯片的卡片(如银行卡、身份证)
  • 手机:接收短信验证码或推送通知
  • U2F/FIDO2安全密钥:基于公钥密码学的硬件设备

优点:难以远程窃取,与知识因素结合可大幅提升安全性。 缺点:可能丢失、损坏或被盗;需要携带额外设备。

47.2.3 生物因素(Something You Are)

用户固有的生理或行为特征:

  • 生理特征:指纹、人脸、虹膜、声纹、掌静脉
  • 行为特征:打字节奏、鼠标移动模式、签名动态

优点:难以伪造,用户无需记忆。 缺点:一旦泄露无法更换(你无法换一根手指);存在隐私争议;可能受环境因素影响。

47.2.4 位置因素(Somewhere You Are)

基于用户的地理位置:

  • IP地址:判断请求来源是否可信
  • GPS定位:移动设备的物理位置
  • 网络环境:是否来自公司内网

优点:可检测异常登录(如短时间内跨洲访问)。 缺点:VPN和代理可伪造位置;移动设备位置变化频繁。

47.2.5 行为因素(Something You Do)

用户的行为模式:

  • 设备指纹:操作系统、浏览器、屏幕分辨率等组合
  • 操作习惯:点击模式、滑动轨迹
  • 时间模式:通常的登录时间段

这类因素常用于风险自适应认证(Risk-based Authentication),在不打扰用户的情况下评估登录风险。

47.3 密码认证

47.3.1 密码存储的演进

密码绝不能以明文存储。存储方式的演进反映了安全意识的提升:

  1. 明文存储:最危险的做法,数据库泄露即全部暴露。
  2. 哈希存储:存储密码的哈希值,但相同密码产生相同哈希,易被彩虹表攻击。
  3. 加盐哈希:为每个密码附加随机盐值,再计算哈希,有效防御彩虹表。
  4. 自适应哈希:计算成本可配置的哈希算法(如bcrypt、scrypt、Argon2),抵抗硬件加速暴力破解。

47.3.2 盐(Salt)与胡椒(Pepper)

盐(Salt) 是每个密码独有的随机字符串,与密码拼接后计算哈希:

hash = H(password || salt)
  • 每个用户有独立的盐值
  • 盐值与哈希结果一同存储
  • 即使两个用户密码相同,存储的哈希也不同

胡椒(Pepper) 是全局的秘密值,存储在应用配置中而非数据库:

hash = H(password || salt || pepper)
  • 所有用户共享同一个胡椒值
  • 胡椒不存储在数据库中
  • 即使数据库泄露,没有胡椒也无法验证密码

47.3.3 现代密码哈希算法

算法设计目标特点推荐场景
bcrypt基于Blowfish密码自适应成本因子,广泛支持传统应用兼容
scrypt内存困难型高内存消耗,抗ASIC加密货币、高安全需求
Argon2密码哈希竞赛 winner2015年Password Hashing Competition冠军,可配置内存、时间和并行度现代应用首选

Argon2有三个变体:

  • Argon2d:抵抗GPU破解,适合加密货币
  • Argon2i:抵抗侧信道攻击,适合密码哈希
  • Argon2id:两者兼顾,推荐使用

47.3.4 Rust实现:Argon2密码哈希

use argon2::{
    password_hash::{
        rand_core::OsRng,
        PasswordHash, PasswordHasher, PasswordVerifier, SaltString
    },
    Argon2,
};

/// 生成密码哈希
fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
    let salt = SaltString::generate(&mut OsRng);
    let argon2 = Argon2::default();
    let password_hash = argon2
        .hash_password(password.as_bytes(), &salt)?
        .to_string();
    Ok(password_hash)
}

/// 验证密码
fn verify_password(password: &str, hash: &str) -> Result<bool, argon2::password_hash::Error> {
    let parsed_hash = PasswordHash::new(hash)?;
    let argon2 = Argon2::default();
    match argon2.verify_password(password.as_bytes(), &parsed_hash) {
        Ok(()) => Ok(true),
        Err(argon2::password_hash::Error::Password) => Ok(false),
        Err(e) => Err(e),
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let password = "my_secure_password_123";
    
    // 注册时:生成哈希
    let hash = hash_password(password)?;
    println!("密码哈希: {}", hash);
    // 输出示例: $argon2id$v=19$m=65536,t=3,p=4$...$
    
    // 登录时:验证密码
    let is_valid = verify_password(password, &hash)?;
    println!("验证结果: {}", is_valid);
    
    let is_invalid = verify_password("wrong_password", &hash)?;
    println!("错误密码验证: {}", is_invalid);
    
    Ok(())
}

Cargo.toml 依赖:

[dependencies]
argon2 = "0.5"

47.3.5 密码策略

良好的密码策略应平衡安全性与可用性:

  • 最小长度:至少12个字符(NIST建议)
  • 复杂度要求:大小写字母、数字、符号的组合
  • 常见密码检查:拒绝已泄露的密码(如Have I Been Pwned数据库)
  • 密码历史:防止近期密码复用
  • 锁定策略:多次失败后暂时锁定账户
  • 密码提示:不提供可能暴露密码的提示

现代趋势是鼓励使用密码短语(Passphrase) 而非复杂短密码,例如“correct-horse-battery-staple“比“Tr0ub4dor&3“更安全且易记。

47.4 多因素认证

47.4.1 MFA与2FA

多因素认证(Multi-Factor Authentication, MFA) 要求用户提供两种或以上的不同类别认证因素。最常见的组合是:

  • 知识因素 + 持有因素(密码 + 手机验证码)
  • 知识因素 + 生物因素(密码 + 指纹)

双因素认证(Two-Factor Authentication, 2FA) 是MFA的特例,恰好使用两个因素。

MFA显著提升了安全性:即使密码泄露,攻击者仍需要第二因素才能通过认证。

47.4.2 OTP:一次性密码

一次性密码(One-Time Password, OTP) 是MFA中持有因素的常见实现,分为两类:

HOTP:基于计数器

HOTP(HMAC-based One-Time Password, RFC 4226)使用计数器生成密码:

HOTP(K, C) = Truncate(HMAC-SHA-1(K, C))

其中K是共享密钥,C是计数器值。每次验证后计数器递增。

缺点:需要同步计数器,若用户多次生成未使用验证码,会导致不同步。

TOTP:基于时间

TOTP(Time-based One-Time Password, RFC 6238)是HOTP的扩展,使用当前时间作为计数器:

TOTP(K, T) = HOTP(K, T)
T = (Current Unix time - T0) / X
  • T0是起始时间(通常为0)
  • X是时间步长(通常为30秒)

TOTP更实用,无需同步计数器,但要求设备时间大致准确。

47.4.3 TOTP实现原理

TOTP生成过程:

  1. 密钥共享:服务器生成随机密钥,通过安全通道(如二维码)传递给认证器应用
  2. 时间对齐:双方使用UTC时间,按30秒窗口对齐
  3. HMAC计算HMAC-SHA-1(key, time_counter)
  4. 截断(Truncate):从20字节的HMAC结果中提取4字节动态码
  5. 取模dynamic_code % 10^digits(通常6位数字)

验证时,服务器通常检查当前时间窗口及前后各一个窗口(共3个窗口,约90秒),以容纳时间偏差。

47.4.4 Rust实现:TOTP生成与验证

use totp_rs::{Algorithm, Secret, TOTP};
use qrcode::QrCode;
use qrcode::render::unicode;

fn create_totp(account: &str, issuer: &str) -> Result<TOTP, Box<dyn std::error::Error>> {
    let secret = Secret::generate_secret().to_bytes()?;
    
    let totp = TOTP::new(
        Algorithm::SHA1,    // HMAC算法
        6,                   // 验证码位数
        1,                   // 容错窗口(前后各1个时间步)
        30,                  // 时间步长(秒)
        secret,
    )?;
    
    Ok(totp)
}

fn generate_qr_code(totp: &TOTP, account: &str, issuer: &str) {
    // 生成符合Google Authenticator格式的URI
    let uri = totp.get_uri(issuer.to_string(), account.to_string());
    println!("TOTP URI: {}", uri);
    
    // 生成二维码
    let code = QrCode::new(uri).unwrap();
    let string = code.render::<unicode::Dense1x2>()
        .dark_color(unicode::Dense1x2::Light)
        .light_color(unicode::Dense1x2::Dark)
        .build();
    println!("{}", string);
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let account = "user@example.com";
    let issuer = "RustApp";
    
    // 创建TOTP
    let totp = create_totp(account, issuer)?;
    
    // 生成二维码供用户扫描
    generate_qr_code(&totp, account, issuer);
    
    // 生成当前验证码
    let token = totp.generate_current()?;
    println!("当前验证码: {}", token);
    
    // 验证用户输入的验证码
    let user_input = "123456"; // 假设用户输入
    let is_valid = totp.check(user_input, 
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)?
            .as_secs()
    );
    println!("验证结果: {}", is_valid);
    
    Ok(())
}

Cargo.toml 依赖:

[dependencies]
totp-rs = { version = "5", features = ["qr"] }

47.4.5 备份码与恢复机制

MFA必须考虑设备丢失的情况:

  • 备份码:注册时生成一组一次性使用的备用码
  • 多设备注册:允许在多个设备上配置同一TOTP
  • 替代验证:通过已验证的邮箱或手机进行身份验证后重置MFA

47.5 生物特征认证

47.5.1 指纹认证

指纹识别是最成熟的生物特征技术:

  • 采集:光学、电容、超声波传感器
  • 特征提取:识别细节特征点(ridge endings和bifurcations)
  • 匹配:将采集的模板与注册模板比对,计算相似度分数
  • 阈值决策:分数超过阈值则接受

安全性考量:指纹模板应加密存储,且不可逆推原始指纹图像。

47.5.2 人脸识别

现代人脸识别依赖深度学习:

  • 人脸检测:定位图像中的人脸区域
  • 特征提取:神经网络生成高维特征向量(如128维或512维)
  • 相似度计算:余弦相似度或欧氏距离
  • 活体检测:防止照片、视频、面具攻击

隐私风险:人脸是公开可见的生物特征,且难以更换。欧盟GDPR将其列为敏感个人数据。

47.5.3 虹膜识别

虹膜模式具有极高的唯一性和稳定性:

  • 准确性:错误接受率(FAR)可达10^-7级别
  • 稳定性:虹膜模式终身不变
  • 非接触:可在1米外采集

缺点:设备成本高;部分眼疾或手术会影响识别;用户接受度较低。

47.5.4 生物特征认证的Rust生态

Rust在生物特征领域的生态尚在发展中。对于Web应用,通常通过操作系统API(如Windows Hello、Apple Touch ID、WebAuthn)间接支持生物特征认证。

#![allow(unused)]
fn main() {
// WebAuthn示例(webauthn-rs库)
use webauthn_rs::prelude::*;

fn setup_webauthn() -> Result<Webauthn, WebauthnError> {
    let rp_id = "example.com";
    let rp_origin = Url::parse("https://example.com")?;
    
    let builder = WebauthnBuilder::new(rp_id, &rp_origin)?;
    let webauthn = builder.build()?;
    Ok(webauthn)
}
}

47.6 单点登录

47.6.1 SSO的概念

单点登录(Single Sign-On, SSO) 允许用户使用一组凭据访问多个相互信任的应用系统。其核心优势:

  • 用户体验:一次登录,处处通行
  • 安全管理:集中管理身份和权限
  • 审计追踪:统一的登录日志

47.6.2 OAuth 2.0

OAuth 2.0(RFC 6749)是授权框架,常被用于实现SSO:

角色定义

  • 资源所有者(Resource Owner):用户本人
  • 客户端(Client):请求访问的第三方应用
  • 授权服务器(Authorization Server):颁发令牌
  • 资源服务器(Resource Server):托管受保护资源

授权流程

  1. 客户端引导用户到授权服务器
  2. 用户登录并同意授权
  3. 授权服务器重定向回客户端,附带授权码
  4. 客户端用授权码换取访问令牌
  5. 客户端使用访问令牌访问资源

四种授权模式

模式适用场景安全性
授权码模式(Authorization Code)服务器端应用高,支持PKCE
隐式授权(Implicit)单页应用(已废弃)
密码凭证(Password)受信任的第一方应用
客户端凭证(Client Credentials)服务间通信

47.6.3 OpenID Connect

OpenID Connect(OIDC)构建于OAuth 2.0之上,专门用于身份认证:

  • ID Token:JWT格式的身份令牌,包含用户声明(claims)
  • UserInfo Endpoint:获取用户详细信息的API
  • Discovery:自动发现配置端点

OIDC标准化了“用Google/微信/GitHub登录“的实现方式。

47.6.4 SAML

安全断言标记语言(Security Assertion Markup Language, SAML) 是企业级SSO的老牌标准:

  • 基于XML的断言交换
  • 身份提供者(IdP)与服务提供者(SP)之间的信任关系
  • 常用于企业应用(如Office 365、Salesforce的SSO集成)

相比OIDC,SAML配置更复杂,但在传统企业环境中仍广泛使用。

47.6.5 Rust实现:JWT处理

use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,        // 主题(用户ID)
    iss: String,        // 签发者
    aud: String,        // 受众
    exp: usize,         // 过期时间
    iat: usize,         // 签发时间
    #[serde(skip_serializing_if = "Option::is_none")]
    name: Option<String>,
}

fn current_timestamp() -> usize {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs() as usize
}

fn create_jwt(user_id: &str, secret: &str) -> Result<String, jsonwebtoken::errors::Error> {
    let now = current_timestamp();
    let claims = Claims {
        sub: user_id.to_string(),
        iss: "rust-app".to_string(),
        aud: "rust-client".to_string(),
        iat: now,
        exp: now + 3600, // 1小时后过期
        name: Some("张三".to_string()),
    };
    
    encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(secret.as_bytes()),
    )
}

fn verify_jwt(token: &str, secret: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
    let mut validation = Validation::default();
    validation.set_issuer(&["rust-app"]);
    validation.set_audience(&["rust-client"]);
    
    let token_data = decode::<Claims>(
        token,
        &DecodingKey::from_secret(secret.as_bytes()),
        &validation,
    )?;
    
    Ok(token_data.claims)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let secret = "your-256-bit-secret-key-here!!!";
    let user_id = "user_12345";
    
    // 生成JWT
    let token = create_jwt(user_id, secret)?;
    println!("生成的JWT: {}", token);
    
    // 验证JWT
    let claims = verify_jwt(&token, secret)?;
    println!("验证通过,用户: {:?}", claims);
    
    // 尝试用错误密钥验证
    let result = verify_jwt(&token, "wrong-secret");
    match result {
        Ok(_) => println!("不应成功"),
        Err(e) => println!("验证失败(预期): {}", e),
    }
    
    Ok(())
}

Cargo.toml 依赖:

[dependencies]
jsonwebtoken = "9"
serde = { version = "1", features = ["derive"] }

47.7 认证最佳实践

47.7.1 安全设计原则

  1. 纵深防御:不依赖单一安全机制
  2. 最小信任:验证所有输入,不信任任何来源
  3. 失败安全:认证失败时默认拒绝访问
  4. 安全默认值:默认启用最强安全策略

47.7.2 常见攻击与防御

攻击类型描述防御措施
暴力破解尝试所有可能的密码组合速率限制、账户锁定、CAPTCHA
字典攻击使用常见密码列表尝试密码强度策略、泄露密码检查
彩虹表预计算的哈希值查找表加盐哈希、自适应哈希
中间人拦截通信窃取凭据TLS/HTTPS、证书固定
重放攻击重复发送截获的认证信息时间戳、随机数、一次性令牌
会话劫持窃取会话标识符HttpOnly Cookie、短过期时间、绑定IP
钓鱼攻击伪造登录页面骗取凭据多因素认证、用户教育

47.7.3 会话管理

认证成功后,系统需要维持会话状态:

  • 会话标识符:随机生成的高熵字符串
  • 存储方式:服务器端会话存储或客户端JWT
  • 过期策略:绝对过期时间 + 空闲超时
  • 安全传输:仅通过HTTPS传输
  • Cookie属性:Secure、HttpOnly、SameSite

47.8 本章总结

主题核心要点
认证基础身份识别是声明,身份验证是确认;认证、授权、审计构成AAA框架
认证因素知识/持有/生物/位置/行为五类因素;多因素组合提升安全性
密码认证使用Argon2id等自适应哈希;盐值防彩虹表,胡椒防数据库泄露
MFA/2FATOTP基于时间窗口,广泛支持;HOTP基于计数器,需同步
生物特征指纹/人脸/虹膜各有优劣;注意隐私保护和活体检测
SSOOAuth 2.0授权框架,OIDC身份层,SAML企业标准
Rust实现argon2密码哈希、totp-rs验证码、jsonwebtokenJWT处理

47.9 练习建议

  1. 密码哈希实践:实现一个用户注册/登录系统,使用Argon2存储密码,比较不同成本参数对性能的影响。

  2. TOTP认证器:开发命令行TOTP生成器,支持从标准输入读取密钥,每30秒输出新验证码。与Google Authenticator交叉验证。

  3. JWT中间件:为Actix-web或Axum编写JWT认证中间件,实现令牌签发、验证和刷新机制。

  4. 多因素认证流程:设计完整的MFA注册和验证流程,包括二维码生成、备份码、设备管理功能。

  5. 安全审计:对开源Rust认证库进行代码审计,检查是否遵循OWASP认证安全建议。


认证如守门,门后有万千世界。门把守不严,世界便危如累卵;门把守过严, legitimate 访客亦寸步难行。在Rust的内存安全与类型系统之上,构建坚固的认证体系,方能让系统在便捷与安全之间找到最佳平衡。