双重身份验证(Two-Factor Authentication,简称2FA)是一种安全措施,旨在通过要求用户提供两种不同类型的身份验证信息来增强账户访问的安全性。这通常包括用户知道的东西(知识因素)和用户拥有的东西( possession factor),或者用户本身的特征( inherence factor)。双重认证结合了两个不同类别的验证,即使攻击者获取到了其中一个因素,也很难同时获取另一个因素,从而大大降低了账户被盗用的风险。
用户输入用户名和密码(第一重验证)。
系统要求用户通过第二种验证方式提供额外的信息或响应,如输入从手机收到的验证码、生成的动态口令或进行生物特征扫描。
一旦两个验证步骤都成功完成,用户才能被授权访问系统或服务。
这里采用的是应用生成的验证码(Authenticator App):如Google Authenticator、Microsoft Authenticator等应用生成基于时间或计数器的一次性密码(TOTP)。
基于时间的一次性密码(TOTP)
-
TOTP是一种动态密码生成算法,它依赖于当前时间来产生一次性有效的密码。其工作原理如下:
时间步长:系统使用一个固定的时间间隔(通常是30秒)作为时间步长,每个时间步长内生成一个唯一的密码。
密钥:用户和服务之间共享一个秘密密钥(Secret Key),这个密钥在初始化时通过安全的方式分发,比如通过扫描二维码。
哈希算法:使用如HMAC-SHA-1、SHA-256等加密哈希算法,结合当前时间戳和共享密钥计算出一个哈希值,然后取哈希值的一部分转换成易于用户输入的形式,通常是一个6位数字。
时间同步:客户端(如手机上的身份验证应用)和服务端都需要保持大致相同的时间,以确保在正确的时间步内验证密码。
基于计数器的一次性密码(COTP)
COTP与TOTP相似,但不是基于时间而是基于递增的计数器(Counter)来生成密码。其特点包括: -
计数器:每次生成新密码时,客户端和服务端的计数器都会递增。计数器的初始值 和步长在初始化时协商确定。
密钥与哈希:同样使用共享密钥和哈希算法,不过这次是结合当前计数器的值而非时间戳来计算哈希。
同步问题:相比TOTP,COTP对时间同步的要求较低,但在计数器不同步的情况下(例如,如果客户端长时间离线后再上线),需要有机制处理计数器的重新同步问题。
使用
1. 注册Config
import dev.samstevens.totp.code.CodeVerifier;
import dev.samstevens.totp.code.DefaultCodeGenerator;
import dev.samstevens.totp.code.DefaultCodeVerifier;
import dev.samstevens.totp.code.HashingAlgorithm;
import dev.samstevens.totp.qr.QrDataFactory;
import dev.samstevens.totp.qr.QrGenerator;
import dev.samstevens.totp.qr.ZxingPngQrGenerator;
import dev.samstevens.totp.secret.DefaultSecretGenerator;
import dev.samstevens.totp.secret.SecretGenerator;
import dev.samstevens.totp.time.SystemTimeProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TwoFAConfig {
@Bean
public SecretGenerator secretGenerator() {
return new DefaultSecretGenerator();
}
@Bean
public QrDataFactory qrDataFactory() {
return new QrDataFactory(HashingAlgorithm.SHA1, 6, 30);
}
@Bean
public QrGenerator qrGenerator() {
return new ZxingPngQrGenerator();
}
@Bean
public CodeVerifier codeVerifier() {
return new DefaultCodeVerifier(new DefaultCodeGenerator(HashingAlgorithm.SHA1, 6), new SystemTimeProvider());
}
}
2. Service
- setupDevice()用于生成QR码和密钥,完成和Microsoft Authenticator客户端的初始绑定。
- codeVerifier.isValidCode(secret, code) 验证提供的code和真实的密码是否一致。
import dev.samstevens.totp.code.CodeVerifier;
import dev.samstevens.totp.exceptions.QrGenerationException;
import dev.samstevens.totp.qr.QrData;
import dev.samstevens.totp.qr.QrDataFactory;
import dev.samstevens.totp.qr.QrGenerator;
import im.gy.zfile.core.exception.LoginVerifyException;
import im.gy.zfile.module.config.model.dto.SystemConfigDTO;
import im.gy.zfile.module.config.service.SystemConfigService;
import im.gy.zfile.module.login.model.enums.LoginVerifyModeEnum;
import im.gy.zfile.module.login.model.request.VerifyLoginTwoFactorAuthenticatorRequest;
import im.gy.zfile.module.login.model.result.LoginTwoFactorAuthenticatorResult;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import dev.samstevens.totp.secret.SecretGenerator;
import static dev.samstevens.totp.util.Utils.getDataUriForImage;
/**
* 2FA 双因素认证 Service
*/
@Service
public class TwoFactorAuthenticatorVerifyService {
@Resource
private SecretGenerator secretGenerator;
@Resource
private QrDataFactory qrDataFactory;
@Resource
private QrGenerator qrGenerator;
@Resource
private CodeVerifier codeVerifier;
@Resource
private SystemConfigService systemConfigService;
/**
* 生成2FA 双因素认证二维码和密钥
* @return
* @throws QrGenerationException
*/
public LoginTwoFactorAuthenticatorResult setupDevice() throws QrGenerationException {
// 生成的2FA密钥
String secret = secretGenerator.generate();
QrData data = qrDataFactory.newBuilder().secret(secret).issuer("ZFile").build();
// 密钥转base64图像字符串
String qrCodeImg = getDataUriForImage(
qrGenerator.generate(data),
qrGenerator.getImageMimeType()
);
return new LoginTwoFactorAuthenticatorResult(qrCodeImg, secret);
}
/**
* 验证 2FA 双因素是否正确。正确则进行绑定
* @param verifyLoginTwoFactorAuthenticatorRequest
*/
public void deviceVerify(VerifyLoginTwoFactorAuthenticatorRequest verifyLoginTwoFactorAuthenticatorRequest) {
String code = verifyLoginTwoFactorAuthenticatorRequest.getCode();
String secret = verifyLoginTwoFactorAuthenticatorRequest.getSecret();
if (codeVerifier.isValidCode(secret, code)) {
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
systemConfig.setLoginVerifyMode(LoginVerifyModeEnum.TWO_FACTOR_AUTHENTICATION_MODE);
systemConfig.setLoginVerifySecret(secret);
systemConfigService.updateSystemConfig(systemConfig);
} else {
throw new LoginVerifyException("验证码错误");
}
}
public void checkCode(String loginVerifySecret, String verifyCode) {
if (!codeVerifier.isValidCode(loginVerifySecret, verifyCode)) {
throw new LoginVerifyException("验证码错误或已经失效");
}
}
}