双因素认证(2FA,Two-Factor Authentication)是一种提高安全性的方法,要求用户在登录或进行某些敏感操作时提供两种不同类型的身份验证信息。这种方法通过引入第二层验证,增加了账户被未经授权访问的难度。
项目结构
spring-boot-2fa-demo
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── demo
│ │ │ ├── DemoApplication.java
│ │ │ ├── security
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ ├── TotpAuthenticationFilter.java
│ │ │ │ ├── TotpAuthenticationProvider.java
│ │ │ │ ├── TotpAuthenticationToken.java
│ │ │ │ └── TotpAuthenticator.java
│ │ │ └── web
│ │ │ ├── TotpSetupController.java
│ │ │ └── TotpVerifyController.java
│ └── main
│ └── resources
│ └── application.properties
└── pom.xml
1. pom.xml
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-2fa-demo</name>
<description>Spring Boot 2FA Demo</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- TOTP Library -->
<dependency>
<groupId>de.taimos</groupId>
<artifactId>totp</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Spring Boot Starter Test -->
<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>
2. DemoApplication.java
package com.example.demo;
import com.example.demo.demo.security.TotpAuthenticator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
String[] beanNames = context.getBeanNamesForType(TotpAuthenticator.class);
for (String beanName : beanNames) {
System.out.println("Found bean: " + beanName);
}
}
}
```
### 3. Security 配置
#### `SecurityConfig.java`
```java
package com.example.demo.demo.security;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 配置不需要认证的路径
.antMatchers("/login", "/totp-setup", "/totp-verify", "/auth/*","/test/*").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/totp-verify")
.permitAll()
.and()
// 在用户名密码过滤器之前添加 TOTP 认证过滤器
.addFilterBefore(new TotpAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
TotpAuthenticationFilter.java
package com.example.demo.demo.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class TotpAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public TotpAuthenticationFilter() {
super(new AntPathRequestMatcher("/totp-verify"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
String totp = request.getParameter("totp");
String username = request.getParameter("username");
// 创建 TOTP 认证令牌
TotpAuthenticationToken token = new TotpAuthenticationToken(username, totp);
return this.getAuthenticationManager().authenticate(token);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authResult)
throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
}
```
#### `TotpAuthenticationProvider.java`
```java
package com.example.demo.demo.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
public class TotpAuthenticationProvider implements AuthenticationProvider {
@Autowired
private TotpAuthenticator totpAuthenticator;
@Autowired
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String totp = (String) authentication.getCredentials();
// 验证 TOTP if (totpAuthenticator.verifyTotp(username, Integer.parseInt(totp))) {
return new TotpAuthenticationToken(username, totp,
userDetailsService.loadUserByUsername(username).getAuthorities());
}
return null;
}
@Override
public boolean supports(Class<?> authentication) {
return TotpAuthenticationToken.class.isAssignableFrom(authentication);
}
}
TotpAuthenticationToken.java
package com.example.demo.demo.security;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class TotpAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private Object credentials;
public TotpAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
public TotpAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
credentials = null;
}
}
TotpAuthenticator.java
package com.example.demo.demo.security;
import com.warrenstrange.googleauth.GoogleAuthenticator;
import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;
import org.springframework.stereotype.Component;
/**
* @author lei
*/@Component
public class TotpAuthenticator {
private final GoogleAuthenticator gAuth = new GoogleAuthenticator();
// 生成 TOTP 密钥并返回 GoogleAuthenticatorKey 对象
public GoogleAuthenticatorKey generateSecret() {
return gAuth.createCredentials();
}
// 获取 TOTP QR 码 URL public String getQRCode(GoogleAuthenticatorKey secret, String account) {
return GoogleAuthenticatorQRGenerator.getOtpAuthTotpURL(account, "SpringBootDemo", secret);
}
// 验证 TOTP public boolean verifyTotp(String secret, int verificationCode) {
return gAuth.authorize(secret, verificationCode);
}
}
4. 控制器
TotpSetupController.java
package com.example.demo.demo.web;
import com.example.demo.demo.dto.QRCodeResponse;
import com.example.demo.demo.security.TotpAuthenticator;
import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/auth")
public class TotpSetupController {
private final TotpAuthenticator totpAuthenticator;
public TotpSetupController(TotpAuthenticator totpAuthenticator) {
this.totpAuthenticator = totpAuthenticator;
}
// 设置 TOTP 密钥并返回 QR 码 URL @GetMapping("/totp-setup")
public Map<String, String> setupTotp(@RequestParam String username) {
// 写死一个 TOTP 密钥
String hardCodedSecret = "OZSNQGV44RGY63BL";
GoogleAuthenticatorKey googleAuthenticatorKey = new GoogleAuthenticatorKey.Builder(hardCodedSecret).build();
String qrCodeUrl = totpAuthenticator.getQRCode(googleAuthenticatorKey, username);
Map<String, String> response = new HashMap<>();
response.put("secret", hardCodedSecret);
response.put("qrCodeUrl", qrCodeUrl);
return response;
}
// 设置 TOTP 密钥并返回 QR 码 URL @GetMapping("/totp-setup1")
public QRCodeResponse setupTotp1(@RequestParam String username) {
GoogleAuthenticatorKey googleAuthenticatorKey = totpAuthenticator.generateSecret();
// 保存密钥与用户名的关联关系,可以使用数据库等存储
// 这里只是示例,没有实际存储
String qrCodeUrl = totpAuthenticator.getQRCode(googleAuthenticatorKey, username);
return new QRCodeResponse(googleAuthenticatorKey.getKey(), qrCodeUrl);
}
}
TotpVerifyController.java
package com.example.demo.demo.web;
import com.example.demo.demo.security.TotpAuthenticator;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/test")
public class TotpVerifyController {
private final TotpAuthenticator totpAuthenticator;
public TotpVerifyController(TotpAuthenticator totpAuthenticator) {
this.totpAuthenticator = totpAuthenticator;
}
@GetMapping("/totp-verify")
public String verifyTotp(@RequestParam int totp) {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
// 从存储中获取与用户名关联的密钥,这里假设已获取
String secret = "OZSNQGV44RGY63BL";
if (totpAuthenticator.verifyTotp(secret, totp)) {
return "2FA 成功!";
} else {
return "无效的 TOTP!";
} }
@GetMapping("/test1")
public String test() {
return "hell1";
}}
5. 配置文件
application.properties
server.port=8080
spring.application.name=2FA-Demo
6. 启动项目
确保所有代码都已编写完成,然后运行 DemoApplication.java
启动项目。你可以通过以下步骤测试 2FA 功能:
- 访问
/totp-setup
端点生成 TOTP 密钥和 QR 码。 - 使用 Google Authenticator 扫描 QR 码。
- 访问
/totp-verify
端点并输入 Google Authenticator 生成的一次性密码。
- 接口输出url可通过二下面工具生成
- 二维码工具:https://www.runoob.com/try/try.php?filename=tryhtml5_QRCode