1. 前言
在微服务与分布式架构日益普及的今天,传统的单一凭证(用户名+密码) 已经难以满足企业对于身份验证的高安全性需求。多因素认证(Multi‑Factor Authentication,简称 MFA) 通过“用户知道的东西”(如密码)+“用户拥有的东西”(如动态验证码)或“用户自身的一部分”(如指纹)三种因素的组合,大幅提升了系统防护能力。
企业级安全方案设计 - 多因素认证(MFA)实现-1.png)
比如我们常见的 GitHub、腾讯云等就开启了 MFA。GitHub 开启 MFA 后可以使用 Authenticator 应用扫描,而腾讯云则需要短信验证码来进行校验。
本章节笔者将带领大家深入解析 MFA,并基于 Spring Security 6,结合 MySQL 与 MyBatis-Plus,从理论到实战,快速构建一套企业级的 MFA 认证方案。
2. 为什么需要多因素认证?
传统认证的风险
- 密码脆弱性:大部分的数据泄露源于弱密码或重复密码。
- 撞库攻击:黑客利用泄露的密码库尝试登录其他系统。
- 钓鱼攻击:伪造登录页面窃取用户凭证。
MFA 的核心优势
多因素认证(MFA)通过多种不同类别的凭证来共同完成身份验证,显著提升安全性:
- Something you know(你知道的东西):用户名与密码、PIN 码等;
- Something you have(你拥有的东西):手机收到的 OTP、应用令牌(Authenticator)等;
- Something you are(你自身的一部分):生物特征(指纹、面部识别等)。
当密码被破解或泄露后,如果没有第二因素(如手机动态验证码),攻击者依然无法登录。
常见多因素认证实现方案
| 认证方式 |
安全性 |
用户体验 |
实施成本 |
| 短信验证码 |
★★★ |
★★★☆ |
★★★ |
| TOTP(如 Google Authenticator) |
★★★☆ |
★★★☆ |
★★☆ |
| 生物识别 |
★★★☆ |
★★★★ |
★★★★ |
本方案选择 TOTP:平衡安全性与实施成本,兼容 Google Authenticator 等标准应用。
3. 多因素认证的核心原理
以 TOTP(Time‑based One‑Time Password) 为例:
- 服务端生成用户专属密钥(Secret Key),并在用户首次登录或在安全设置中心将其展示给用户(通常通过二维码形式扫描到 Google Authenticator、Authy 等应用中)。
- 手机端应用(如 Google Authenticator)基于 Secret Key 与当前时间戳,通过 HMAC‑SHA1 算法计算出 6 位动态验证码。
- 用户登录时,输入用户名+密码(第 1 因素),若校验通过,跳转到 MFA 验证页面,要求输入手机上展示的 6 位动态验证码(第 2 因素)。
- 服务端验证客户端提交的动态验证码是否与基于相同 Secret Key 和当前时间戳计算出的值一致。若一致,则认为通过 MFA,登录成功;否则,拒绝登录或提示重试。
整个流程中,只有用户掌握 Secret Key(存在手机应用中),且需实时生成动态验证码。即使攻击者获得了用户名+密码,没有手机和 Secret Key,也无法通过第二因素验证。
4. 系统架构与流程设计
本章节以单体 Spring Boot 应用演示 MFA 流程,生产环境可拆分成独立的认证服务(Auth Service)与业务服务(Resource Service),二者均依赖集中管理的用户与 MFA 数据库。关键流程:
❶ MFA 初始化
后台管理员或用户注册时,系统为用户生成一对 RSA 密钥(可选)或仅生成 TOTP Secret,保存至用户表中。将生成的 Secret 以二维码或明文形式呈现给用户,用户通过 Google Authenticator 等扫描或手动录入。
❷ 第一步:用户名+密码登录
用户提交用户名+密码,Service 层校验密码(结合 BCrypt)。校验成功后,将用户标记为“已通过第一步认证”,并生成一个短期令牌(可存放到 session 或 JWT)表示“待 MFA”状态,重定向到 MFA 验证页。
❸ TOTP 验证
用户在 MFA 验证页中输入 6 位动态验证码,提交后,后台从数据库中取出该用户的 Secret,通过 TOTP 算法生成当前时刻的合法验证码,进行比对。若校验通过,则完成整个登录流程,Spring Security 将真正的 Authentication 对象置入 SecurityContext 中,登录成功,跳转到首页;否则,提示错误并重试。
❹ 完整流程图
企业级安全方案设计 - 多因素认证(MFA)实现-2.png)
5. Spring Security 整合 MFA 实现
根据前面的章节我们已经整合好了 mysql + mybatis 等的项目案例,我们继续追加子模块,引入 Google Authenticator 兼容 TOTP 实现:com.warrenstrange:googleauth:1.5.0。
5.1 引入依赖
下面以 pom.xml 为例,列出主要依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.5</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
<dependency> <groupId>com.warrenstrange</groupId> <artifactId>googleauth</artifactId> <version>1.5.0</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>
|
5.2 用户实体类(User.java)
使用 Lombok @Data 简化 getter/setter,mfaEnabled 与 mfaSecret 字段分别表示该用户是否启用 MFA 及其对应的 TOTP 密钥。
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Data @TableName("users") public class User { @TableId(type = IdType.AUTO) private Long id; private String username; private String password; private Boolean enabled; private Boolean mfaEnabled; private String mfaSecret; private LocalDateTime createdAt; private LocalDateTime updatedAt; }
|
5.3 用户 Mapper
1 2 3 4
| @Mapper public interface UserMapper extends BaseMapper<User> { }
|
5.4 TOTP 工具类(Google Authenticator 兼容)
我们将使用 com.warrenstrange.googleauth.GoogleAuthenticator 来生成并验证动态验证码(TOTP)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| public class TotpUtils { private static final GoogleAuthenticator gAuth = new GoogleAuthenticator();
public static String generateSecretKey() { GoogleAuthenticatorKey key = gAuth.createCredentials(); return key.getKey(); }
public static boolean verifyTotp(String secretKey, int code) { return gAuth.authorize(secretKey, code); }
public static String getHexKey(String base32Secret) { Base32 codec = new Base32(); byte[] bytes = codec.decode(base32Secret); return Hex.encodeHexString(bytes); }
public static String getOtpAuthURL(String username, String secret, String issuer) { return String.format( "otpauth://totp/%s:%s?secret=%s&issuer=%s", issuer, username, secret, issuer ); } }
|
说明:
generateSecretKey():生成一个新的 Base32 格式秘钥,用于 TOTP 绑定。
verifyTotp(secretKey, code):校验用户提交的 6 位 TOTP 码是否与当前时刻计算值匹配。
getOtpAuthURL(...):方便在前端生成二维码,让用户用 Google Authenticator 扫描。
5.5 Service 层:用户与 MFA 逻辑
我们封装用户管理与 MFA 相关的业务逻辑到 UserService。
IUserService.java(接口)
1 2 3 4 5 6
| public interface IUserService { User findByUsername(String username); void register(User user); void enableMfa(Long userId); boolean verifyTotp(Long userId, int code); }
|
UserServiceImpl.java(实现)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| @Service public class UserServiceImpl implements IUserService {
@Autowired private UserMapper userMapper;
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@Override public User findByUsername(String username) { return userMapper.selectOne(new QueryWrapper<User>().eq("username", username)); }
@Override public void register(User user) { user.setPassword(passwordEncoder.encode(user.getPassword())); user.setEnabled(true); user.setMfaEnabled(false); user.setMfaSecret(null); userMapper.insert(user); }
@Override public void enableMfa(Long userId) { User u = userMapper.selectById(userId); String secret = TotpUtils.generateSecretKey(); u.setMfaSecret(secret); u.setMfaEnabled(true); userMapper.updateById(u); }
@Override public boolean verifyTotp(Long userId, int code) { User u = userMapper.selectById(userId); if (u == null || !u.getMfaEnabled() || u.getMfaSecret() == null) { return false; } return TotpUtils.verifyTotp(u.getMfaSecret(), code); } }
|
说明:
register(User):用户注册时将密码加密存库,初始不启用 MFA。
enableMfa(Long):为指定用户生成 TOTP Secret,更新到数据库,并将 mfaEnabled 标记为 true。
verifyTotp(Long, int):验证用户提交的 TOTP 码是否正确。
5.6 安全配置(SecurityConfig.java)
Spring Security 6 中,我们需要覆盖默认的认证流程,实现分为两步的 MFA 登录。思路如下:
- 自定义
AuthenticationProvider:首先校验用户名+密码,如果用户启用了 MFA,就抛出一个自定义异常(MfaRequiredException),在 AuthenticationFailureHandler 中捕获并重定向到 MFA 验证页。
- 在 MFA 验证页中,用户提交 TOTP 码后,我们自定义一个
MfaAuthenticationFilter,从 session 中读取“待 MFA”状态的用户信息,再调用 Service 校验 TOTP。如果通过,则直接构建最终的 UsernamePasswordAuthenticationToken 并置入 SecurityContext。
5.6.1 自定义异常 MfaRequiredException
1 2 3 4 5 6 7 8 9 10 11 12
| public class MfaRequiredException extends AuthenticationException { private final String username;
public MfaRequiredException(String msg, String username) { super(msg); this.username = username; }
public String getUsername() { return username; } }
|
5.6.2 自定义 AuthenticationProvider
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
@Component public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired private IUserService userService;
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getName(); String password = (String) authentication.getCredentials(); User user = userService.findByUsername(username);
if (user == null || !user.getEnabled()) { throw new BadCredentialsException("用户名或密码错误"); } if (!passwordEncoder.matches(password, user.getPassword())) { throw new BadCredentialsException("用户名或密码错误"); }
if (Boolean.TRUE.equals(user.getMfaEnabled())) { throw new MfaRequiredException("MFA 验证必需", username); }
return new UsernamePasswordAuthenticationToken( username, null, Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) ); }
@Override public boolean supports(Class<?> authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } }
|
说明:如果用户开启 mfaEnabled,校验密码后不直接登录,而是通过抛出异常告知后续过滤器进行 MFA 验证。
5.6.3 自定义 MFA 过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
|
@Component public class MfaAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private final IUserService userService;
public MfaAuthenticationFilter(IUserService userService) { super(new AntPathRequestMatcher("/mfa-verify", "POST")); this.userService = userService; }
@Override public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response ) throws IOException, ServletException { String username = (String) request.getSession().getAttribute("MFA_USER"); if (username == null) { throw new RuntimeException("会话中找不到待 MFA 用户"); }
String codeStr = request.getParameter("code"); if (codeStr == null || codeStr.isEmpty()) { throw new RuntimeException("TOTP 码不能为空"); }
int code; try { code = Integer.parseInt(codeStr); } catch (NumberFormatException e) { throw new RuntimeException("TOTP 码格式不正确"); }
User user = userService.findByUsername(username); boolean valid = userService.verifyTotp(user.getId(), code); if (!valid) { throw new RuntimeException("TOTP 验证失败"); }
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken( username, null, Collections.singletonList(() -> "ROLE_USER") ); return auth; }
@Override protected void successfulAuthentication( HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult ) throws IOException, ServletException { SecurityContextHolder.getContext().setAuthentication(authResult); request.getSession().removeAttribute("MFA_USER"); response.sendRedirect("/"); }
@Override protected void unsuccessfulAuthentication( HttpServletRequest request, HttpServletResponse response, AuthenticationException failed ) throws IOException, ServletException { response.setContentType(MediaType.TEXT_PLAIN_VALUE); response.getWriter().write("MFA 验证失败:" + failed.getMessage()); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); } }
|
5.6.4 自定义 AuthenticationFailureHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
@Component public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override public void onAuthenticationFailure( HttpServletRequest request, HttpServletResponse response, AuthenticationException exception ) throws IOException, ServletException { if (exception instanceof MfaRequiredException) { String username = ((MfaRequiredException) exception).getUsername(); request.getSession().setAttribute("MFA_USER", username); response.sendRedirect("/mfa"); } else { response.sendRedirect("/login?error=true"); } } }
|
说明:当 CustomAuthenticationProvider 抛出 MfaRequiredException 时,说明用户通过密码校验但需要第二步 MFA,此时将“待 MFA”用户名写入 session,并重定向到 MFA 验证页面 /mfa。普通失败(如密码错误)则带上 ?error=true 重定向回登录页。
5.6.5 安全配置 SecurityConfig.java
将 CustomAuthenticationProvider 注册到 Spring Security,替代默认的用户名/密码校验逻辑。配置表单登录,登录失败由 CustomAuthenticationFailureHandler 处理。通过 Session 保持“待 MFA”状态直到第二步完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
|
@Configuration public class SecurityConfig {
@Autowired private CustomAuthenticationProvider customAuthenticationProvider;
@Autowired private MfaAuthenticationFilter mfaAuthenticationFilter;
@Autowired private CustomAuthenticationFailureHandler customFailureHandler;
@Bean public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationConfiguration authConfig) throws Exception { http.csrf(csrf -> csrf.disable());
http.authenticationProvider(customAuthenticationProvider);
http.authorizeHttpRequests(auth -> auth .requestMatchers("/login", "/register", "/css/**", "/js/**").permitAll() .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .loginProcessingUrl("/login") .failureHandler(customFailureHandler) .defaultSuccessUrl("/", true) );
http.addFilterAfter(mfaAuthenticationFilter, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class);
http.sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) );
http.exceptionHandling(ex -> ex.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")) );
return http.build(); }
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } }
|
关键点:
- 将
CustomAuthenticationProvider 注册到 Spring Security。
- 使用
CustomAuthenticationFailureHandler 处理第一步登录失败逻辑。
- 在
UsernamePasswordAuthenticationFilter 之后添加 MfaAuthenticationFilter,专门处理 /mfa-verify 请求。
5.7 控制器:登录、MFA 验证、注册与秘钥初始化
我们需要提供几个页面和对应的 Controller:
/login:自定义登录页面(用户名+密码)
/register:用户注册页面
/mfa:MFA 验证页面,用户输入 6 位 TOTP 码
/mfa-verify:MFA 验证提交接口,由 MfaAuthenticationFilter 处理
/enable-mfa:在用户登录后打开此接口可为用户生成 TOTP Secret,并展示二维码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| @Controller public class AuthController {
@Autowired private IUserService userService;
@GetMapping("/login") public String loginPage(@RequestParam(required = false) String error, Model model) { model.addAttribute("error", error != null); return "login"; }
@GetMapping("/register") public String registerPage() { return "register"; }
@PostMapping("/register") public String doRegister(@RequestParam String username, @RequestParam String password) { User u = new User(); u.setUsername(username); u.setPassword(password); userService.register(u); return "redirect:/login"; }
@GetMapping("/mfa") public String mfaPage(HttpSession session, Model model) { String username = (String) session.getAttribute("MFA_USER"); if (username == null) { return "redirect:/login"; } model.addAttribute("username", username); return "mfa"; }
@GetMapping("/enable-mfa") public String enableMfa(Authentication authentication, Model model) { if (authentication == null || !authentication.isAuthenticated()) { return "redirect:/login"; } String username = authentication.getName(); User u = userService.findByUsername(username); if (u.getMfaEnabled()) { model.addAttribute("message", "MFA 已启用"); return "home"; } userService.enableMfa(u.getId()); u = userService.findByUsername(username); String secret = u.getMfaSecret(); String otpAuthURL = TotpUtils.getOtpAuthURL(username, secret, "MyCompany"); model.addAttribute("otpAuthURL", otpAuthURL); model.addAttribute("secret", secret); return "enable-mfa"; }
@GetMapping("/") public String homePage() { return "home"; } }
|
说明:
GET /enable-mfa:已登录用户访问此接口,系统调用 userService.enableMfa(...) 为其生成并保存 TOTP Secret,并展示二维码 URL。
5.8 页面模板(Thymeleaf)
为简化,以下示例仅为最基本表单。生产环境可加入更丰富的样式与 JS 验证。
login.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>登录</title> </head> <body> <h2>登录</h2> <form th:action="@{/login}" method="post"> <div> <label>用户名:</label> <input type="text" name="username" required/> </div> <div> <label>密码:</label> <input type="password" name="password" required/> </div> <div th:if="${error}"> <p style="color:red;">用户名或密码错误</p> </div> <div> <button type="submit">登录</button> </div> </form> <p>没有账号?<a th:href="@{/register}">注册</a></p> </body> </html>
|
register.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>注册</title> </head> <body> <h2>注册</h2> <form th:action="@{/register}" method="post"> <div> <label>用户名:</label> <input type="text" name="username" required/> </div> <div> <label>密码:</label> <input type="password" name="password" required/> </div> <div> <button type="submit">注册</button> </div> </form> <p>已有账号?<a th:href="@{/login}">登录</a></p> </body> </html>
|
mfa.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>MFA 验证</title> </head> <body> <h2>多因素认证</h2> <p>用户 <span th:text="${username}"></span>,请输入手机应用上的 6 位动态验证码:</p> <form th:action="@{/mfa-verify}" method="post"> <div> <label>动态验证码:</label> <input type="text" name="code" pattern="\\d{6}" maxlength="6" required/> </div> <div> <button type="submit">验证</button> </div> </form> </body> </html>
|
enable-mfa.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>启用 MFA</title> </head> <body> <h2>启用多因素认证 (MFA)</h2> <p>请使用 Google Authenticator 或其他兼容 TOTP 的应用扫描下方二维码,或使用秘钥手动添加:</p> <div> <p>OTPAuth URL: <span th:text="${otpAuthURL}"></span></p> <p>Secret Key: <span th:text="${secret}"></span></p> </div> <p>设置完成后,请退出重新登录并输入动态验证码。</p> </body> </html>
|
home.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>首页</title> </head> <body> <h2>欢迎来到系统</h2> <p>您已成功登录(且通过 MFA 验证)。</p> <p><a th:href="@{/enable-mfa}">启用 MFA(如果尚未启用)</a></p> <form th:action="@{/logout}" method="post"> <button type="submit">退出登录</button> </form> </body> </html>
|
注意:如果需要在页面中展示二维码,可以使用前端 QRCode.js 等库,将 otpAuthURL 渲染为二维码。
6. 总结与落地建议
本文从“为什么需要多因素认证”入手,讲解了基于 TOTP 的 MFA 核心原理,并详细演示了如何在 Spring Security 6 中分两步完成登录与 MFA 验证的流程。关键点回顾:
第一步:用户名+密码
- 自定义
AuthenticationProvider,校验用户名与密码;
- 若用户启用 MFA,则抛出
MfaRequiredException,并将用户名暂存到 Session。
第二步:TOTP 验证
- 在数据库
users 表中增加 mfa_enabled 与 mfa_secret 字段;
- Service 层通过 Google Authenticator 兼容库生成并验证动态验证码;
- MyBatis-Plus 简化了实体与 Mapper 的开发。
实际生产环境推荐
- 二维码展示与绑定:在
/enable-mfa 页面使用前端二维码生成库(如 qrcode.js)将 otpAuthURL 渲染为二维码图片,方便用户扫码。
- 密钥保护:
mfa_secret 为敏感数据,建议对其进行数据库加密存储或使用 KMS 等专用系统保护。
- 备份码与恢复:当用户手机丢失时,可预先生成一组一次性“恢复码”,用户在绑定 MFA 时妥善保存,避免无法登录。
- 登录失败锁定:对于连续多次 TOTP 验证失败的账户,可暂时锁定或触发告警,防止暴力破解。
- SSL/TLS 强制:确保所有页面(尤其是登录与 MFA 页面)使用 HTTPS,防止中间人攻击截获验证码。
- 会话超时与防并发:可考虑对“待 MFA”状态的会话设置合理的超时时间(如 2 分钟),超时后必须重新进行第一步登录。
通过上面的设计与实现,企业级应用即可在原有用户名+密码的基础上,平滑地接入基于 TOTP 的多因素认证,大幅提升系统安全性,抵御常见的账户破解与钓鱼风险。
如果你在实践过程中有任何疑问或更好的扩展思路,欢迎在评论区留言,最后希望大家 一键三连 给博主一点点鼓励!