最新Spring Security实战教程(十七)企业级安全方案设计 - 多因素认证(MFA)实现
🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志
🎐 个人CSND主页——Micro麦可乐的博客
🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战
🌺《RabbitMQ》专栏19年编写主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战
🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解
🌛《开源项目》本专栏主要介绍目前热门的开源项目,带大家快速了解并轻松上手使用
✨《开发技巧》本专栏包含了各种系统的设计原理以及注意事项,并分享一些日常开发的功能小技巧
💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程
🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整
🌞《Spring Security》专栏中我们将逐步深入Spring Security的各个技术细节,带你从入门到精通,全面掌握这一安全技术
如果文章能够给大家带来一定的帮助!欢迎关注、评论互动~
最新Spring Security实战教程(十七)企业级安全方案设计 - 多因素认证(MFA)实现
- 1. 前言
- 2. 为什么需要多因素认证?
- 传统认证的风险
- MFA的核心优势
- 常见多因素认证实现方案
- 3. 多因素认证的核心原理
- 4. 系统架构与流程设计
- ❶ 用户注册/初始化
- ❷ 第一步:用户名+密码登录
- ❸ TOTP 验证
- ❹ 完整流程图
- 5. Spring Security整合MFA实现
- 5.1 引入依赖
- 5.2 用户实体
- 5.3 用户 Mapper
- 5.4 TOTP 工具类(Google Authenticator 兼容)
- 5.5 Service 层:用户与 MFA 逻辑
- 5.6 安全配置(SecurityConfig.java)
- 5.6 .1 自定义异常 MfaRequiredException.java
- 6.5.2 自定义 AuthenticationProvider
- 5.6.3 自定义过滤器 MfaAuthenticationFilter.java
- 5.6.4 自定义失败处理器 CustomAuthenticationFailureHandler.java
- 5.6.5 安全配置 SecurityConfig.java
- 5.7 控制器:登录、MFA 验证、注册与秘钥初始化
- 5.8 前端页面示例(Thymeleaf)
- 6. 总结与落地建议
- 实际生产环境推荐:
回顾链接:
最新Spring Security实战教程(一)初识Spring Security安全框架
最新Spring Security实战教程(二)表单登录定制到处理逻辑的深度改造
最新Spring Security实战教程(三)Spring Security 的底层原理解析
最新Spring Security实战教程(四)基于内存的用户认证
最新Spring Security实战教程(五)基于数据库的动态用户认证传统RBAC角色模型实战开发
最新Spring Security实战教程(六)最新Spring Security实战教程(六)基于数据库的ABAC属性权限模型实战开发
最新Spring Security实战教程(七)方法级安全控制@PreAuthorize注解的灵活运用
最新Spring Security实战教程(八)Remember-Me实现原理 - 持久化令牌与安全存储方案
最新Spring Security实战教程(九)前后端分离认证实战 - JWT+SpringSecurity无缝整合
最新Spring Security实战教程(十)权限表达式进阶 - 在SpEL在安全控制中的高阶魔法
最新Spring Security实战教程(十一)CSRF攻防实战 - 从原理到防护的最佳实践
最新Spring Security实战教程(十二)CORS安全配置 - 跨域请求的安全边界设定
最新Spring Security实战教程(十三)会话管理机制 - 并发控制与会话固定攻击防护
最新Spring Security实战教程(十四)OAuth2.0精讲 - 四种授权模式与资源服务器搭建
最新Spring Security实战教程(十五)快速集成 GitHub 与 Gitee 的社交登录
最新Spring Security实战教程(十六)微服务间安全通信 - JWT令牌传递与校验机制
1. 前言
在微服务与分布式架构日益普及的今天,传统的 单一凭证(用户名+密码) 已经难以满足企业对于身份验证的高安全性需求。多因素认证(Multi‐Factor Authentication,简称 MFA) 通过用户知道的东西
(如密码)+ 用户拥有的东西
(如动态验证码)或 用户自身的一部分
(如指纹)三种因素的组合,大幅提升了系统防护能力。
比如我们常的 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(你自身的一部分):生物特征(指纹、面部识别等)。
当密码被破解或泄露后,如果没有第二因素(如手机动态验证码),攻击者依然无法登录。
常见多因素认证实现方案
认证方式 | 安全性 | 用户体验 | 实施成本 |
---|---|---|---|
SMS验证码 | ★★☆ | ★★★ | ★★☆ |
邮件验证 | ★★☆ | ★★☆ | ★★☆ |
TOTP | ★★★ | ★★★☆ | ★★★ |
生物识别 | ★★★☆ | ★★★★ | ★★★★ |
本方案选择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 数据库。关键流程:
❶ 用户注册/初始化
后台管理员或用户注册时,系统为用户生成一对 RSA 密钥(可选)或仅生成 TOTP Secret,保存用户表中。
将生成的 Secret 以二维码或明文形式呈现给用户,用户通过 Google Authenticator
等扫描或手动录入。
❷ 第一步:用户名+密码登录
用户提交用户名+密码,Service 层校验密码(结合 BCrypt)。
校验成功后,将用户标记为“已通过第一步认证”,并生成一个短期令牌(可存放到 session 或 JWT)表示“待 MFA”状态,重定向到 MFA 验证页。
❸ TOTP 验证
用户在 MFA 验证页中输入 6 位动态验证码,提交后,后台从数据库中取出该用户的 Secret,通过 TOTP 算法生成当前时刻的合法验证码,进行比对。
若校验通过,则完成整个登录流程,Spring Security
将真正的 Authentication
对象置入 SecurityContext
中,登录成功,跳转到首页;否则,提示错误并重试。
❹ 完整流程图
5. Spring Security整合MFA实现
根据前面的章节我们已经整合好了 mysql + mybatis等的项目案例,我们继续追加子模块,引入Google Authenticator
兼容 TOTP
实现:com.warrenstrange:googleauth:1.5.0
5.1 引入依赖
下面以 pom.xml
为例,列出主要依赖:
<!-- pom.xml --><dependencies><!-- Spring Boot Starter Web + Thymeleaf --><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><!-- Spring Security 6 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- MyBatis-Plus & MySQL 驱动 --><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><!-- Google Authenticator TOTP 实现 --><dependency><groupId>com.warrenstrange</groupId><artifactId>googleauth</artifactId><version>1.5.0</version></dependency><!-- Lombok(可选) --><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 用户实体
@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;
}
使用 Lombok @Data 简化 getter/setter
mfaEnabled 与 mfaSecret 字段分别表示该用户是否启用 MFA 及其对应的 TOTP 密钥
5.3 用户 Mapper
@Mapper
public interface UserMapper extends BaseMapper<User> {// 如果需要自定义 SQL,可在此处声明
}
5.4 TOTP 工具类(Google Authenticator 兼容)
我们将使用 com.warrenstrange.googleauth.GoogleAuthenticator
来生成并验证动态验证码(TOTP)
public class TotpUtils {private static final GoogleAuthenticator gAuth = new GoogleAuthenticator();/*** 为用户生成一个新的 TOTP 密钥(Base32 编码格式)** @return Base32 编码的密钥*/public static String generateSecretKey() {GoogleAuthenticatorKey key = gAuth.createCredentials();return key.getKey();}/*** 验证用户提交的 TOTP 码是否合法(基于用户的 Secret Key)** @param secretKey Base32 编码的 TOTP 密钥* @param code 用户提交的 6 位验证码* @return true 如果校验通过;false 否则*/public static boolean verifyTotp(String secretKey, int code) {return gAuth.authorize(secretKey, code);}/*** 将 Base32 编码的密钥转换为 Hex,若业务需要展示给前端 URI 可用该方法*/public static String getHexKey(String base32Secret) {Base32 codec = new Base32();byte[] bytes = codec.decode(base32Secret);return Hex.encodeHexString(bytes);}/*** 生成在 Google Authenticator 中添加账户的二维码 URI** @param username 用户名* @param secret Base32 编码密钥* @param issuer 应用或企业名称,比如 "MyCompany"* @return otpauth://totp/issuer:username?secret=SECRET&issuer=issuer*/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(接口)
public interface IUserService {User findByUsername(String username);void register(User user);void enableMfa(Long userId);boolean verifyTotp(Long userId, int code);
}
UserServiceImpl.java(实现)
@Service
public class UserServiceImpl implements IUserService {@Autowiredprivate UserMapper userMapper;private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();@Overridepublic User findByUsername(String username) {return userMapper.selectOne(new QueryWrapper<User>().eq("username", username));}@Overridepublic void register(User user) {// 加密密码user.setPassword(passwordEncoder.encode(user.getPassword()));user.setEnabled(true);user.setMfaEnabled(false);user.setMfaSecret(null);userMapper.insert(user);}@Overridepublic void enableMfa(Long userId) {// 为用户生成 TOTP Secret 并更新User u = userMapper.selectById(userId);String secret = TotpUtils.generateSecretKey();u.setMfaSecret(secret);u.setMfaEnabled(true);userMapper.updateById(u);}@Overridepublic 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.java
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;}
}
6.5.2 自定义 AuthenticationProvider
/*** 第一步:校验用户名 + 密码* 如果用户启用 MFA,则抛出 MfaRequiredException,后续由 MfaAuthenticationFilter 处理*/
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {@Autowiredprivate IUserService userService;private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();@Overridepublic 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("用户名或密码错误");}// 如果用户启用了 MFA,则抛出自定义异常,提示进行第二步验证if (Boolean.TRUE.equals(user.getMfaEnabled())) {throw new MfaRequiredException("MFA 验证必需", username);}// 未启用 MFA 或继承走这里,直接构建 Authenticationreturn new UsernamePasswordAuthenticationToken(username, null,Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));}@Overridepublic boolean supports(Class<?> authentication) {return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);}
}
说明:
- 如果用户开启
mfaEnabled
,校验密码后不直接登录,而是通过抛出异常告知后续过滤器进行 MFA 验证。
5.6.3 自定义过滤器 MfaAuthenticationFilter.java
/*** 该过滤器负责处理 /mfa-verify POST 请求,* 从 session 中获取待验证用户名,校验用户提交的 TOTP 码。*/
@Component
public class MfaAuthenticationFilter extends AbstractAuthenticationProcessingFilter {private final IUserService userService;public MfaAuthenticationFilter(IUserService userService) {super(new AntPathRequestMatcher("/mfa-verify", "POST"));this.userService = userService;// 不让 Spring Security 为我们阻止 CSRF,示例中 CSRF 已关闭}@Overridepublic org.springframework.security.core.Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException {// 应用前端将用户名暂存到 sessionAttribute: "MFA_USER"String username = (String) request.getSession().getAttribute("MFA_USER");if (username == null) {throw new RuntimeException("会话中找不到待 MFA 用户");}// 获取用户提交的 TOTP 码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 码格式不正确");}// 从数据库校验 TOTPUser user = userService.findByUsername(username);boolean valid = userService.verifyTotp(user.getId(), code);if (!valid) {throw new RuntimeException("TOTP 验证失败");}// 验证成功,构建真正的 Authentication 对象UsernamePasswordAuthenticationToken auth =new UsernamePasswordAuthenticationToken(username, null, Collections.singletonList(() -> "ROLE_USER"));return auth;}@Overrideprotected void successfulAuthentication(HttpServletRequest request,HttpServletResponse response,FilterChain chain,org.springframework.security.core.Authentication authResult) throws IOException, ServletException {// 将最终的 Authentication 填入 SecurityContextSecurityContextHolder.getContext().setAuthentication(authResult);// 登录成功后清除 session 中的 MFA 用户标志request.getSession().removeAttribute("MFA_USER");// 跳转到首页response.sendRedirect("/");}@Overrideprotected void unsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response,org.springframework.security.core.AuthenticationException failed) throws IOException, ServletException {// 验证失败,跳回 MFA 验证页面response.setContentType(MediaType.TEXT_PLAIN_VALUE);response.getWriter().write("MFA 验证失败:" + failed.getMessage());response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);}
}
说明:
- 该过滤器拦截
POST /mfa-verify
请求,读取 session 中预先放置的 “MFA_USER” 用户名,以及前端提交的code
。- 调用
userService.verifyTotp(...)
校验动态验证码,若通过则构建最终的Authentication
。
5.6.4 自定义失败处理器 CustomAuthenticationFailureHandler.java
/*** 处理第一步用户名/密码登录失败或触发 MFA 的情况*/
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request,HttpServletResponse response,AuthenticationException exception) throws IOException, ServletException {// 如果是 MfaRequiredException,重定向到 /mfa 页面,并将用户名存入 sessionif (exception instanceof MfaRequiredException) {String username = ((MfaRequiredException) exception).getUsername();request.getSession().setAttribute("MFA_USER", username);// 重定向到 MFA 验证页面response.sendRedirect("/mfa");} else {// 普通登录失败,重定向回 /login?errorresponse.sendRedirect("/login?error=true");}}
}
说明:
- 当
CustomAuthenticationProvider
抛出MfaRequiredException
时,说明用户通过密码校验但需要第二步 MFA,此时将“待 MFA”用户名写入 session,并重定向到 MFA 验证页面/mfa
。- 普通失败(如密码错误)则带上
?error=true
重定向回登录页。
5.6.5 安全配置 SecurityConfig.java
/*** 核心安全配置:* 1. 注入自定义 AuthenticationProvider* 2. 配置表单登录和 MfaAuthenticationFilter*/
@Configuration
public class SecurityConfig {@Autowiredprivate CustomAuthenticationProvider customAuthenticationProvider;@Autowiredprivate MfaAuthenticationFilter mfaAuthenticationFilter;@Autowiredprivate CustomAuthenticationFailureHandler customFailureHandler;@Beanpublic SecurityFilterChain filterChain(HttpSecurity http, AuthenticationConfiguration authConfig) throws Exception {// 禁用 CSRF 简化示例http.csrf(csrf -> csrf.disable());// 使用自定义 AuthenticationProvider 替换默认的 DaoAuthenticationProviderhttp.authenticationProvider(customAuthenticationProvider);// 1. 首先,配置表单登录http.authorizeHttpRequests(auth -> auth.requestMatchers("/login", "/register", "/css/**", "/js/**").permitAll().anyRequest().authenticated()).formLogin(form -> form.loginPage("/login").loginProcessingUrl("/login") // 与表单提交 action 保持一致.failureHandler(customFailureHandler).defaultSuccessUrl("/", true));// 2. 注册 MFA 过滤器,它要在 UsernamePasswordAuthenticationFilter 之后执行http.addFilterAfter(mfaAuthenticationFilter, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class);// 3. Session 管理:MFA 过程中会话保持http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));// 4. 未授权时返回 401http.exceptionHandling(ex -> ex.authenticationEntryPoint((request, response, authException) ->response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")));return http.build();}// 若需手动获取 AuthenticationManager,可使用以下 Bean@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {return authConfig.getAuthenticationManager();}
}
关键点:
- 将
CustomAuthenticationProvider
注册到 Spring Security,替代默认的用户名/密码校验逻辑。- 配置表单登录,登录失败由
CustomAuthenticationFailureHandler
处理。- 注册
MfaAuthenticationFilter
,拦截/mfa-verify
提交。- SessionPersist 保持“待 MFA”状态直到第二步完成。
5.7 控制器:登录、MFA 验证、注册与秘钥初始化
我们需要提供几个页面和对应的 Controller:
/login
:自定义登录页面(用户名+密码)/register
:用户注册页面/mfa
:MFA 验证页面,用户输入 6 位 TOTP 码/mfa-verify
:MFA 验证提交接口,由MfaAuthenticationFilter
处理/enable-mfa
:在用户登录后打开此接口可为用户生成 TOTP Secret,并展示二维码
@Controller
public class AuthController {@Autowiredprivate 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";}/*** MFA 验证页:用户输入动态验证码*/@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";}/*** 启用 MFA:登录后用户请求此接口可获取 TOTP Secret 与二维码 URL*/@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";}// 为用户生成秘钥并开启 MFAuserService.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
用于用户主动绑定 MFA(生成 Secret 并呈现给用户)。若业务要求后台自动开通,可在注册后直接调用userService.enableMfa(...)
。
5.8 前端页面示例(Thymeleaf)
为简化,以下示例仅为最基本表单。生产环境可加入更丰富的样式与 JS 验证。
login.html
<!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
<!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
<!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
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>启用 MFA</title>
</head>
<body>
<h2>启用多因素认证 (MFA)</h2>
<p>请使用 Google Authenticator 或其他兼容 TOTP 的应用扫描下方二维码,或使用秘钥手动添加:</p>
<div><!-- 可以使用前端库生成二维码,此处直接展示 URI,方便生成 QR --><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
<!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 验证
- 自定义
MfaAuthenticationFilter
,拦截/mfa-verify
请求; - 从 Session 中获取“待 MFA”用户名,调用服务端 TOTP 校验逻辑;
- 校验通过后,构建最终
Authentication
并置入SecurityContext
。
- 自定义
-
MySQL + MyBatis-Plus
- 在数据库
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 的多因素认证,大幅提升系统安全性,抵御常见的账户破解与钓鱼风险。
如果你在实践过程中有任何疑问或更好的扩展思路,欢迎在评论区留言,最后希望大家 一键三连 给博主一点点鼓励!
相关文章:

最新Spring Security实战教程(十七)企业级安全方案设计 - 多因素认证(MFA)实现
🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志 🎐 个人CSND主页——Micro麦可乐的博客 🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战 🌺《RabbitMQ》…...

html+css+js趣味小游戏~Cookie Clicker放置休闲(附源码)
下面是一个简单的记忆卡片配对游戏的完整代码,使用HTML、CSS和JavaScript实现: html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"wid…...
宝塔面板安装nodejs后,通过node -v获取不到版本号,报错node: command not found
如果在 宝塔面板 安装了 Node.js,但运行 node -v 或 npm -v 时提示 command not found,通常是因为 Node.js 的路径未正确添加到系统环境变量。以下是解决方法: 1. 确认 Node.js 是否安装成功 (1)检查宝塔面板的 Node.…...

SDC命令详解:使用set_propagated_clock命令进行约束
相关阅读 SDC命令详解https://blog.csdn.net/weixin_45791458/category_12931432.html?spm1001.2014.3001.5482 目录 指定端口列表/集合 简单使用 注意事项 传播时钟是在进行了时钟树综合后,使用set_propagated_clock命令可以将一个理想时钟转换为传播时钟&#x…...

win32相关(消息Hook)
消息Hook 要想实现消息Hook需要使用到三个相关的Api SetWindowsHookEx // 设置钩子CallNextHookEx // 将钩子信息传递到当前钩子链中的下一个子程序UnhookWindowsHookEx // 卸载钩子 我们编写的消息钩子需要将设置钩子的函数写到dll里面,当钩住一个线程后ÿ…...
vue3单独封装表单校验函数
1.在页面中建一个.ts文件 import { useI18n } from /hooks/web/useI18n import { FormItemRule } from element-plusconst { t } useI18n()interface LengthRange {min: numbermax: numbermessage?: string } //必输项校验 export const useValidator () > {const requi…...

mysql 页的理解和实际分析
目录 页(Page)是 Innodb 存储引擎用于管理数据的最小磁盘单位B树的一般高度记录在页中的存储 innodb ibd文件innodb 页类型分析ibd文件查看数据表的行格式查看ibd文件 分析 ibd的第4个页:B-tree Node类型先分析File Header(38字节-描述页信息…...
分享一道力扣
刚刚笔试遇到的。好像很简单,但又不容易写的 611 有效三角形 def triangleNumber(self, nums):count 0nums.sort()for i in range(len(nums) - 2):k i 2for j in range(i 1, len(nums) - 1):if nums[i] 0:breakwhile k < len(nums) and nums[i] nums[j] &g…...
青少年编程与数学 01-011 系统软件简介 06 Android操作系统
青少年编程与数学 01-011 系统软件简介 06 Android操作系统 一、历史发展二、核心架构1. Linux 内核层 (Linux Kernel)2. 硬件抽象层 (Hardware Abstraction Layer - HAL)3. Native 层 (Native Libraries & Android Runtime)4. Java API 框架层 (Java Framework Layer)5. 应…...

构建 MCP 服务器:第 2 部分 — 使用资源模板扩展资源
该图像是使用 AI 图像创建程序创建的。 这个故事是在多位人工智能助手的帮助下写成的。 这是构建MCP 服务器教程(共四部分)的第二部分。在第一部分中,我们使用基本资源创建了第一个 MCP 服务器。现在,我们将使用资源模板扩展服务…...

【算法设计与分析】实验——汽车加油问题, 删数问题(算法实现:代码,测试用例,结果分析,算法思路分析,总结)
说明:博主是大学生,有一门课是算法设计与分析,这是博主记录课程实验报告的内容,题目是老师给的,其他内容和代码均为原创,可以参考学习,转载和搬运需评论吱声并注明出处哦。 4-1算法实现题 汽车…...
Ubuntu2404 下搭建 Zephyr 开发环境
1. 系统要求 操作系统:Ubuntu2404(64位)磁盘空间:至少 8GB 可用空间(Zephyr 及其工具链较大) 2. 安装必要工具 Tool Min. Version CMake 3.20.5 Python 3.10 Devicetree compiler 1.4.6 2.1 安装系…...
现代C++特性(一):基本数据类型扩展
文章目录 基础数据类型long long (C 11)numeric_limits()获取当前数据类型的最值warning C4309: “”: 截断常量值新字符类型char16_t和char32_tWindows编程常用字符类型wchar_tchar8_t (C 20) 基础数据类型 C中的基本类型是构建其他数据类型的基础,常见的基础类型…...

【C++进阶篇】C++11新特性(下篇)
C函数式编程黑魔法:Lambda与包装器实战全解析 一. lambda表达式1.1 仿函数使用1.2 lambda表达式的语法1.3 lambda表达式使用1.3.1 传值和传引用捕捉1.3.2 隐式捕捉1.3.3 混合捕捉 1.4 lambda表达式原理1.5 lambda优点及建议 二. 包装器2.1 function2.2 bind绑定 三.…...

全生命周期的智慧城市管理
前言 全生命周期的智慧城市管理。未来,城市将在 实现从基础设施建设、日常运营到数据管理的 全生命周期统筹。这将避免过去智慧城市建设 中出现的“碎片化”问题,实现资源的高效配 置和项目的协调发展。城市管理者将运用先进 的信息技术,如物…...

echarts柱状图实现动态展示时报错
echarts柱状图实现动态展示时报错 1、问题: 在使用Echarts柱状图时,当数据量过多,x轴展示不下的时候,可以使用dataZoom实现动态展示。如下图所示: 但是当鼠标放在图上面滚动滚轮时或拖动滚动条时会报错,…...
Redis故障转移
概述 本文主要讲述了Redis故障转移的原理及过程,可与「Redis高可用架构」文章一同阅读,可更好理解相关内容,及整个Redis高可用架构的实现原理。 Leader 选举 哨兵首先进入 WATI_START 状态进行准备,等待哨兵成为哨兵集群的 Leade…...
STM32学习笔记:定时器(TIM)原理与应用(详解篇)
前言 定时器是STM32微控制器中最重要且最常用的外设之一,它不仅能提供精确的定时功能,还能实现PWM输出、输入捕获、编码器接口等多种功能。本文将全面介绍STM32的通用定时器,包括其工作原理、配置方法和典型应用。 一、STM32定时器概述 定…...
JAVA获取ES连接并查询所有数据
我们的项目要获取es连接,新版本和旧版本有不小的区别,在8.17.0版本使用的是 ElasticsearchClient <dependency><groupId>co.elastic.clients</groupId><artifactId>elasticsearch-java</artifactId><version>8.17…...

408第一季 - 数据结构 - 线性表
只能用C/C! 顺序表 闲聊 线性表的逻辑顺序和物理顺序相同 都是1234 顺序表的优点: 随机访问,随机访问的意思是访问的时间 和位置没有关系,访问下标1和100一样的,更深层就是直接计算 a100 * 数组大小,随便…...

第23讲、Odoo18 邮件系统整体架构
目录 Odoo 邮件系统整体架构邮件发送方式邮件模板配置SMTP 邮件服务器配置邮件发送过程开发中常见邮件发送需求常见问题排查提示与最佳实践完整示例:审批通过自动发邮件门户表单自动邮件通知案例邮件队列与异步发送邮件添加附件邮件日志与调试多语言邮件模板邮件安…...
【QT面试题】(三)
文章目录 Qt信号槽的优点及缺点Qt中的文件流和数据流区别?Qt中show和exec区别QT多线程使用的方法 (4种)QString与基本数据类型如何转换?QT保证多线程安全事件与信号的区别connect函数的连接方式?信号与槽的多种用法Qt的事件过滤器有哪些同步和…...
DeepSeek09-open-webui使用
Open WebUI 完全指南:从安装到知识库搭建与异常处理 最后更新:2025年6月7日 | 适用版本:Open WebUI v0.6.x 一、安装部署 1.1 系统要求 **Python 3.12 **(严格版本要求,更高版本3.13不兼容)Node.js 20.x内…...

HarmonyOS:Counter计数器组件
一、概述 计数器组件,提供相应的增加或者减少的计数操作。 说明 该组件从API Version 7开始支持。后续版本如有新增内容,则采用上角标单独标记该内容的起始版本。 二、属性 除支持通用属性外,还支持以下属性。 enableInc enableInc(value: b…...
数据类型 -- 字符
在C中,字符型(char)用于存储单个字符,如字母、数字、符号等。字符型是最基本的数据类型之一,常用于处理文本、字符数组(字符串)等场景。 1. 基本类型 • char:标准字符类型&#x…...
WordZero:让Markdown与Word文档自由转换的Golang利器
在日常工作中,我们经常需要在Markdown和Word文档之间进行转换。Markdown方便编写和版本控制,而Word文档更适合正式的商务环境。作为一名Golang开发者,我开发了WordZero这个库,专门解决这个痛点。 项目背景 GitHub仓库࿱…...

sqlsugar WhereIF条件的大于等于和等于查出来的坑
一、如下图所示,当我用 .WhereIF(input.Plancontroltype > 0, u > u.Plancontroltype (DnjqPlancontroltype)input.Plancontroltype) 这里面用等于的时候,返回结果一条数据都没有。 上图中生成的SQL如下: SELECT id AS Id ,code AS …...

Pandas 技术解析:从数据结构到应用场景的深度探索
序 我最早用Python做大数据项目时,接触最早的就是Pandas了。觉得对于IT技术人员而言,它是可以属于多场景的存在,因为它的本身就是数据驱动的技术生态中,对于软件工程师而言,它是快速构建数据处理管道的基石࿱…...

数据库系统概论(十七)超详细讲解数据库规范化与五大范式(从函数依赖到多值依赖,再到五大范式,附带例题,表格,知识图谱对比带你一步步掌握)
数据库系统概论(十七)超详细讲解数据库规范化与五大范式(从函数依赖到多值依赖,再到五大范式,附带例题,表格,知识图谱对比带你一步步掌握) 前言一、为什么需要规范化1. 我们先想一个…...
[c#]判定当前软件是否用管理员权限打开
有时一些软件的逻辑中需要使用管理员权限对某些文件进行修改时,那么该软件在执行或者打开的场合,就需要用使用管理员身份运行才能达到效果。那么在c#里,如何判定该软件是否是对管理员身份运的呢? 1.取得当前的windows用户。 2.取得…...