安全無憂!在 Spring Boot 3.3 中輕松實(shí)現(xiàn) TOTP 雙因素認(rèn)證
隨著互聯(lián)網(wǎng)的快速發(fā)展,網(wǎng)絡(luò)安全問題日益嚴(yán)峻。傳統(tǒng)的用戶名和密碼認(rèn)證方式已經(jīng)無法滿足現(xiàn)代應(yīng)用對(duì)安全性的要求,因此雙因素認(rèn)證(2FA)成為了提升安全性的有效手段。雙因素認(rèn)證不僅要求用戶輸入密碼,還需通過第二種方式進(jìn)行身份驗(yàn)證,例如手機(jī)生成的動(dòng)態(tài)驗(yàn)證碼。
時(shí)間同步一次性密碼(TOTP)是一種基于時(shí)間的雙因素認(rèn)證方式,它通過算法生成短期有效的驗(yàn)證碼。用戶在登錄時(shí),需要輸入從手機(jī)應(yīng)用(如 Google Authenticator)獲取的 TOTP 代碼。由于 TOTP 代碼每 30 秒更新一次,即使攻擊者獲取了用戶的密碼,沒有有效的 TOTP 代碼,也無法登錄賬戶。
本文將詳細(xì)介紹如何在 Spring Boot 3.3 中實(shí)現(xiàn)基于 TOTP 的雙因素認(rèn)證,涵蓋從依賴配置、服務(wù)實(shí)現(xiàn)到前端展示的完整過程。
什么是 TOTP?
TOTP(Time-based One-Time Password)是一種用于雙因素認(rèn)證的算法,它基于當(dāng)前時(shí)間和用戶的共享秘密(密鑰)生成一次性密碼。TOTP 主要遵循以下步驟:
- 密鑰生成:在用戶賬戶創(chuàng)建時(shí)生成一個(gè)共享密鑰,并與用戶的身份綁定。該密鑰通常以 Base32 編碼格式存儲(chǔ)。
- 時(shí)間戳使用:TOTP 使用當(dāng)前時(shí)間戳,將時(shí)間分成固定的時(shí)間段(例如,30 秒)。每個(gè)時(shí)間段生成一個(gè)唯一的 TOTP 密碼。
- 動(dòng)態(tài)密碼生成:通過將共享密鑰和當(dāng)前時(shí)間戳作為輸入,使用 HMAC-SHA1 或類似算法生成一次性密碼。
- 驗(yàn)證過程:在用戶登錄時(shí),服務(wù)器端也使用相同的共享密鑰和當(dāng)前時(shí)間戳生成 TOTP 密碼,并與用戶輸入的密碼進(jìn)行比對(duì)。
這種機(jī)制保證了每次登錄時(shí)生成的密碼都是唯一且短暫的,極大地提升了賬戶的安全性。
運(yùn)行效果:
若想獲取項(xiàng)目完整代碼以及其他文章的項(xiàng)目源碼,且在代碼編寫時(shí)遇到問題需要咨詢交流,歡迎加入下方的知識(shí)星球。
項(xiàng)目依賴配置
首先,在 pom.xml
中添加所需的依賴:
<?xml versinotallow="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.icoderoad</groupId>
<artifactId>totp-authentication</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>totp-authentication</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>dev.samstevens.totp</groupId>
<artifactId>totp</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置文件
接下來,我們在 application.yml
中配置所需的屬性:
server:
port: 8080
totp:
time-step: 30
length: 6
生成和配置密鑰
生成密鑰服務(wù)類
package com.icoderoad.totp.service;
import org.springframework.stereotype.Service;
import dev.samstevens.totp.secret.DefaultSecretGenerator;
import dev.samstevens.totp.secret.SecretGenerator;
@Service
public class SecretService {
private final SecretGenerator secretGenerator = new DefaultSecretGenerator();
public String generateSecret() {
// 生成安全的隨機(jī) base32 編碼字符串
return secretGenerator.generate();
}
}
屬性配置類
package com.icoderoad.totp.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import lombok.Data;
@Data
@Component
@ConfigurationProperties(prefix = "totp")
public class TotpProperties {
private int timeStep = 30; // 默認(rèn)值為 30 秒
private int length = 6; // 默認(rèn)值為 6 位
}
配置 TOTP 生成器
package com.icoderoad.totp.service;
import com.icoderoad.totp.config.TotpProperties;
import dev.samstevens.totp.time.TimeProvider;
import dev.samstevens.totp.time.SystemTimeProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TotpConfiguration {
private final TotpProperties totpProperties;
public TotpConfiguration(TotpProperties totpProperties) {
this.totpProperties = totpProperties;
}
@Bean
public TimeProvider timeProvider() {
return new SystemTimeProvider(); // 使用系統(tǒng)時(shí)間提供者
}
@Bean
public int getTotpLength() {
return totpProperties.getLength();
}
public int getTimeStepInSeconds() {
return totpProperties.getTimeStep();
}
}
TOTP 生成和驗(yàn)證
TOTP 生成服務(wù)
package com.icoderoad.totp.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.icoderoad.totp.config.TotpProperties;
import dev.samstevens.totp.code.CodeGenerator;
import dev.samstevens.totp.code.DefaultCodeGenerator;
import dev.samstevens.totp.exceptions.CodeGenerationException;
import dev.samstevens.totp.time.SystemTimeProvider;
import dev.samstevens.totp.time.TimeProvider;
@Service
public class TotpGeneratorService {
@Autowired
private TotpProperties totpProperties;
private final CodeGenerator codeGenerator;
private final TimeProvider timeProvider;
@Autowired
public TotpGeneratorService(TimeProvider timeProvider) {
this.timeProvider = timeProvider != null ? timeProvider : new SystemTimeProvider();
this.codeGenerator = new DefaultCodeGenerator(); // 使用默認(rèn)構(gòu)造函數(shù)
}
public String generateTotp(String secret) {
long counter = getCounter();
try {
return codeGenerator.generate(secret, counter);
} catch (CodeGenerationException e) {
return "";
}
}
private long getCounter() {
long timeStep = totpProperties.getTimeStep();
return timeProvider.getTime() / timeStep;
}
}
TOTP 驗(yàn)證服務(wù)
package com.icoderoad.totp.service;
import dev.samstevens.totp.code.CodeVerifier;
import dev.samstevens.totp.code.DefaultCodeVerifier;
import dev.samstevens.totp.code.DefaultCodeGenerator;
import dev.samstevens.totp.exceptions.CodeGenerationException;
import dev.samstevens.totp.time.TimeProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.icoderoad.totp.config.TotpProperties;
@Service
public class TotpVerificationService {
private final DefaultCodeVerifier codeVerifier;
private final TimeProvider timeProvider;
private final TotpProperties totpProperties;
@Autowired
public TotpVerificationService(TimeProvider timeProvider, TotpProperties totpProperties) {
this.totpProperties = totpProperties;
this.timeProvider = timeProvider;
this.codeVerifier = new DefaultCodeVerifier(new DefaultCodeGenerator(), timeProvider);
this.codeVerifier.setTimePeriod(this.totpProperties.getTimeStep()); // 從配置文件中讀取或設(shè)置
this.codeVerifier.setAllowedTimePeriodDiscrepancy( this.totpProperties.getLength() ); // 可配置的時(shí)間誤差
}
public boolean verifyTotp(String secret, String code) {
return codeVerifier.isValidCode(secret, code);
}
}
用戶注冊與 TOTP 集成
UserService 類
package com.icoderoad.totp.service;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserService {
// 使用 HashMap 模擬用戶存儲(chǔ)(可以替換為數(shù)據(jù)庫實(shí)現(xiàn))
private final Map<String, String> userSecrets = new HashMap<>();
/**
* 保存用戶的 TOTP 秘密
*
* @param username 用戶名
* @param secret 用戶的 TOTP 秘密
*/
public void saveUserSecret(String username, String secret) {
userSecrets.put(username, secret);
}
/**
* 根據(jù)用戶名獲取 TOTP 秘密
*
* @param username 用戶名
* @return TOTP 秘密
*/
public String findSecretByUsername(String username) {
return userSecrets.get(username);
}
// 可以添加更多與用戶相關(guān)的方法,如驗(yàn)證用戶、獲取用戶信息等
}
QRCodeGenerator類
package com.icoderoad.totp.generator;
import org.springframework.stereotype.Component;
import dev.samstevens.totp.exceptions.QrGenerationException;
import dev.samstevens.totp.qr.QrData;
import dev.samstevens.totp.qr.ZxingPngQrGenerator;
@Component
public class QRCodeGenerator {
private final ZxingPngQrGenerator qrGenerator;
public QRCodeGenerator() {
this.qrGenerator = new ZxingPngQrGenerator();
}
public byte[] generate(String secret, String username, String issuer, int digits, int period) throws QrGenerationException {
// 創(chuàng)建 QR 數(shù)據(jù)
QrData qrData = new QrData.Builder()
.label(username)
.secret(secret)
.issuer(issuer)
.digits(digits)
.period(period)
.build();
// 生成 QR 代碼
return qrGenerator.generate(qrData);
}
public String generateQrCodeUrl(String secret, String username, String issuer, int digits, int period) throws QrGenerationException {
byte[] qrCodeBytes = generate(secret, username, issuer, digits, period);
// 將生成的 QR 代碼轉(zhuǎn)換為 Base64 URL,便于在 HTML 中顯示
return "data:image/png;base64," + java.util.Base64.getEncoder().encodeToString(qrCodeBytes);
}
}
RegistrationResponse類
package com.icoderoad.totp.controller;
public class RegistrationResponse {
private final String secret;
private final String qrCodeUrl;
public RegistrationResponse(String secret, String qrCodeUrl) {
this.secret = secret;
this.qrCodeUrl = qrCodeUrl;
}
public String getSecret() {
return secret;
}
public String getQrCodeUrl() {
return qrCodeUrl;
}
}
注冊控制器
package com.icoderoad.totp.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.icoderoad.totp.dto.UserDto;
import com.icoderoad.totp.generator.QRCodeGenerator;
import com.icoderoad.totp.service.SecretService;
import com.icoderoad.totp.service.UserService;
import dev.samstevens.totp.exceptions.QrGenerationException;
@RestController
public class RegistrationController {
總結(jié)
本 TOTP 注冊系統(tǒng)通過結(jié)合現(xiàn)代前端技術(shù)與穩(wěn)健的后端架構(gòu),成功實(shí)現(xiàn)了高效、安全的用戶注冊流程。系統(tǒng)的設(shè)計(jì)充分考慮了安全性與用戶體驗(yàn),確保用戶在注冊過程中能夠快速獲取所需信息,而不影響安全標(biāo)準(zhǔn)。總體而言,該系統(tǒng)不僅提升了用戶賬戶的安全性,也通過友好的操作流程增強(qiáng)了用戶的信任感,為未來的擴(kuò)展和優(yōu)化打下了堅(jiān)實(shí)基礎(chǔ)。