最新Spring Security实战教程(十七)企业级安全方案设计 - 多因素认证(MFA)实现

最新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)实现

回顾链接:
最新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腾讯云等就开启了MFAGitHub 开启 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)为例:

  1. 服务端生成用户专属密钥(Secret Key),并在用户首次登录或在安全设置中心将其展示给用户(通常通过二维码形式扫描到 Google AuthenticatorAuthy 等应用中)
  2. 手机端应用(如 Google Authenticator)基于 Secret Key 与当前时间戳,通过 HMAC‐SHA1 算法计算出 6 位动态验证码
  3. 用户登录时,输入用户名+密码(第 1 因素),若校验通过,跳转到 MFA 验证页面,要求输入手机上展示的 6 位动态验证码(第 2 因素)
  4. 服务端验证客户端提交的动态验证码是否与基于相同 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")publicclassUser{@TableId(type =IdType.AUTO)privateLong id;privateString username;privateString password;privateBoolean enabled;privateBoolean mfaEnabled;privateString mfaSecret;privateLocalDateTime createdAt;privateLocalDateTime updatedAt;}
使用 Lombok @Data 简化 getter/setter
mfaEnabled 与 mfaSecret 字段分别表示该用户是否启用 MFA 及其对应的 TOTP 密钥

5.3 用户 Mapper

@MapperpublicinterfaceUserMapperextendsBaseMapper<User>{// 如果需要自定义 SQL,可在此处声明}

5.4 TOTP 工具类(Google Authenticator 兼容)

我们将使用 com.warrenstrange.googleauth.GoogleAuthenticator 来生成并验证动态验证码(TOTP)

publicclassTotpUtils{privatestaticfinalGoogleAuthenticator gAuth =newGoogleAuthenticator();/** * 为用户生成一个新的 TOTP 密钥(Base32 编码格式) * * @return Base32 编码的密钥 */publicstaticStringgenerateSecretKey(){GoogleAuthenticatorKey key = gAuth.createCredentials();return key.getKey();}/** * 验证用户提交的 TOTP 码是否合法(基于用户的 Secret Key) * * @param secretKey Base32 编码的 TOTP 密钥 * @param code 用户提交的 6 位验证码 * @return true 如果校验通过;false 否则 */publicstaticbooleanverifyTotp(String secretKey,int code){return gAuth.authorize(secretKey, code);}/** * 将 Base32 编码的密钥转换为 Hex,若业务需要展示给前端 URI 可用该方法 */publicstaticStringgetHexKey(String base32Secret){Base32 codec =newBase32();byte[] bytes = codec.decode(base32Secret);returnHex.encodeHexString(bytes);}/** * 生成在 Google Authenticator 中添加账户的二维码 URI * * @param username 用户名 * @param secret Base32 编码密钥 * @param issuer 应用或企业名称,比如 "MyCompany" * @return otpauth://totp/issuer:username?secret=SECRET&issuer=issuer */publicstaticStringgetOtpAuthURL(String username,String secret,String issuer){returnString.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(接口)

publicinterfaceIUserService{UserfindByUsername(String username);voidregister(User user);voidenableMfa(Long userId);booleanverifyTotp(Long userId,int code);}

UserServiceImpl.java(实现)

@ServicepublicclassUserServiceImplimplementsIUserService{@AutowiredprivateUserMapper userMapper;privatefinalBCryptPasswordEncoder passwordEncoder =newBCryptPasswordEncoder();@OverridepublicUserfindByUsername(String username){return userMapper.selectOne(newQueryWrapper<User>().eq("username", username));}@Overridepublicvoidregister(User user){// 加密密码 user.setPassword(passwordEncoder.encode(user.getPassword())); user.setEnabled(true); user.setMfaEnabled(false); user.setMfaSecret(null); userMapper.insert(user);}@OverridepublicvoidenableMfa(Long userId){// 为用户生成 TOTP Secret 并更新User u = userMapper.selectById(userId);String secret =TotpUtils.generateSecretKey(); u.setMfaSecret(secret); u.setMfaEnabled(true); userMapper.updateById(u);}@OverridepublicbooleanverifyTotp(Long userId,int code){User u = userMapper.selectById(userId);if(u ==null||!u.getMfaEnabled()|| u.getMfaSecret()==null){returnfalse;}returnTotpUtils.verifyTotp(u.getMfaSecret(), code);}}
说明register(User):用户注册时将密码加密存库,初始不启用 MFA。enableMfa(Long):为指定用户生成 TOTP Secret,更新到数据库,并将 mfaEnabled 标记为 trueverifyTotp(Long, int):验证用户提交的 TOTP 码是否正确。

5.6 安全配置(SecurityConfig.java)

Spring Security 6 中,我们需要覆盖默认的认证流程,实现分为两步的 MFA 登录。思路如下:

  1. 自定义 AuthenticationProvider:首先校验用户名+密码,如果用户启用了 MFA,就抛出一个自定义异常(MfaRequiredException),在 AuthenticationFailureHandler 中捕获并重定向到 MFA 验证页。
  2. 在 MFA 验证页中,用户提交 TOTP 码后,我们自定义一个 MfaAuthenticationFilter,从 session 中读取“待 MFA”状态的用户信息,再调用 Service 校验 TOTP。如果通过,则直接构建最终的 UsernamePasswordAuthenticationToken 并置入 SecurityContext。
5.6 .1 自定义异常 MfaRequiredException.java
publicclassMfaRequiredExceptionextendsAuthenticationException{privatefinalString username;publicMfaRequiredException(String msg,String username){super(msg);this.username = username;}publicStringgetUsername(){return username;}}
6.5.2 自定义 AuthenticationProvider
/** * 第一步:校验用户名 + 密码 * 如果用户启用 MFA,则抛出 MfaRequiredException,后续由 MfaAuthenticationFilter 处理 */@ComponentpublicclassCustomAuthenticationProviderimplementsAuthenticationProvider{@AutowiredprivateIUserService userService;privatefinalBCryptPasswordEncoder passwordEncoder =newBCryptPasswordEncoder();@OverridepublicAuthenticationauthenticate(Authentication authentication)throwsAuthenticationException{String username = authentication.getName();String password =(String) authentication.getCredentials();User user = userService.findByUsername(username);if(user ==null||!user.getEnabled()){thrownewBadCredentialsException("用户名或密码错误");}if(!passwordEncoder.matches(password, user.getPassword())){thrownewBadCredentialsException("用户名或密码错误");}// 如果用户启用了 MFA,则抛出自定义异常,提示进行第二步验证if(Boolean.TRUE.equals(user.getMfaEnabled())){thrownewMfaRequiredException("MFA 验证必需", username);}// 未启用 MFA 或继承走这里,直接构建 AuthenticationreturnnewUsernamePasswordAuthenticationToken( username,null,Collections.singletonList(newSimpleGrantedAuthority("ROLE_USER")));}@Overridepublicbooleansupports(Class<?> authentication){returnUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);}}
说明:如果用户开启 mfaEnabled,校验密码后不直接登录,而是通过抛出异常告知后续过滤器进行 MFA 验证。
5.6.3 自定义过滤器 MfaAuthenticationFilter.java
/** * 该过滤器负责处理 /mfa-verify POST 请求, * 从 session 中获取待验证用户名,校验用户提交的 TOTP 码。 */@ComponentpublicclassMfaAuthenticationFilterextendsAbstractAuthenticationProcessingFilter{privatefinalIUserService userService;publicMfaAuthenticationFilter(IUserService userService){super(newAntPathRequestMatcher("/mfa-verify","POST"));this.userService = userService;// 不让 Spring Security 为我们阻止 CSRF,示例中 CSRF 已关闭}@Overridepublicorg.springframework.security.core.AuthenticationattemptAuthentication(HttpServletRequest request,HttpServletResponse response )throwsIOException,ServletException{// 应用前端将用户名暂存到 sessionAttribute: "MFA_USER"String username =(String) request.getSession().getAttribute("MFA_USER");if(username ==null){thrownewRuntimeException("会话中找不到待 MFA 用户");}// 获取用户提交的 TOTP 码String codeStr = request.getParameter("code");if(codeStr ==null|| codeStr.isEmpty()){thrownewRuntimeException("TOTP 码不能为空");}int code;try{ code =Integer.parseInt(codeStr);}catch(NumberFormatException e){thrownewRuntimeException("TOTP 码格式不正确");}// 从数据库校验 TOTPUser user = userService.findByUsername(username);boolean valid = userService.verifyTotp(user.getId(), code);if(!valid){thrownewRuntimeException("TOTP 验证失败");}// 验证成功,构建真正的 Authentication 对象UsernamePasswordAuthenticationToken auth =newUsernamePasswordAuthenticationToken( username,null,Collections.singletonList(()->"ROLE_USER"));return auth;}@OverrideprotectedvoidsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response,FilterChain chain,org.springframework.security.core.Authentication authResult )throwsIOException,ServletException{// 将最终的 Authentication 填入 SecurityContextSecurityContextHolder.getContext().setAuthentication(authResult);// 登录成功后清除 session 中的 MFA 用户标志 request.getSession().removeAttribute("MFA_USER");// 跳转到首页 response.sendRedirect("/");}@OverrideprotectedvoidunsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response,org.springframework.security.core.AuthenticationException failed )throwsIOException,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 的情况 */@ComponentpublicclassCustomAuthenticationFailureHandlerimplementsAuthenticationFailureHandler{@OverridepublicvoidonAuthenticationFailure(HttpServletRequest request,HttpServletResponse response,AuthenticationException exception )throwsIOException,ServletException{// 如果是 MfaRequiredException,重定向到 /mfa 页面,并将用户名存入 sessionif(exception instanceofMfaRequiredException){String username =((MfaRequiredException) exception).getUsername(); request.getSession().setAttribute("MFA_USER", username);// 重定向到 MFA 验证页面 response.sendRedirect("/mfa");}else{// 普通登录失败,重定向回 /login?error response.sendRedirect("/login?error=true");}}}
说明:当 CustomAuthenticationProvider 抛出 MfaRequiredException 时,说明用户通过密码校验但需要第二步 MFA,此时将“待 MFA”用户名写入 session,并重定向到 MFA 验证页面 /mfa。普通失败(如密码错误)则带上 ?error=true 重定向回登录页。
5.6.5 安全配置 SecurityConfig.java
/** * 核心安全配置: * 1. 注入自定义 AuthenticationProvider * 2. 配置表单登录和 MfaAuthenticationFilter */@ConfigurationpublicclassSecurityConfig{@AutowiredprivateCustomAuthenticationProvider customAuthenticationProvider;@AutowiredprivateMfaAuthenticationFilter mfaAuthenticationFilter;@AutowiredprivateCustomAuthenticationFailureHandler customFailureHandler;@BeanpublicSecurityFilterChainfilterChain(HttpSecurity http,AuthenticationConfiguration authConfig)throwsException{// 禁用 CSRF 简化示例 http.csrf(csrf -> csrf.disable());// 使用自定义 AuthenticationProvider 替换默认的 DaoAuthenticationProvider http.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. 未授权时返回 401 http.exceptionHandling(ex -> ex.authenticationEntryPoint((request, response, authException)-> response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"Unauthorized")));return http.build();}// 若需手动获取 AuthenticationManager,可使用以下 Bean@BeanpublicAuthenticationManagerauthenticationManager(AuthenticationConfiguration authConfig)throwsException{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,并展示二维码
@ControllerpublicclassAuthController{@AutowiredprivateIUserService userService;/** * 登录页(第一步:用户名 + 密码) */@GetMapping("/login")publicStringloginPage(@RequestParam(required =false)String error,Model model){ model.addAttribute("error", error !=null);return"login";}/** * 注册页(仅示例) */@GetMapping("/register")publicStringregisterPage(){return"register";}@PostMapping("/register")publicStringdoRegister(@RequestParamString username,@RequestParamString password){User u =newUser(); u.setUsername(username); u.setPassword(password); userService.register(u);return"redirect:/login";}/** * MFA 验证页:用户输入动态验证码 */@GetMapping("/mfa")publicStringmfaPage(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")publicStringenableMfa(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";}// 为用户生成秘钥并开启 MFA 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("/")publicStringhomePage(){return"home";}}
说明GET /enable-mfa 用于用户主动绑定 MFA(生成 Secret 并呈现给用户)。若业务要求后台自动开通,可在注册后直接调用 userService.enableMfa(...)

5.8 前端页面示例(Thymeleaf)

为简化,以下示例仅为最基本表单。生产环境可加入更丰富的样式与 JS 验证。

login.html

<!DOCTYPEhtml><htmlxmlns:th="http://www.thymeleaf.org"><head><title>登录</title></head><body><h2>登录</h2><formth:action="@{/login}"method="post"><div><label>用户名:</label><inputtype="text"name="username"required/></div><div><label>密码:</label><inputtype="password"name="password"required/></div><divth:if="${error}"><pstyle="color:red;">用户名或密码错误</p></div><div><buttontype="submit">登录</button></div></form><p>没有账号?<ath:href="@{/register}">注册</a></p></body></html>

register.html

<!DOCTYPEhtml><htmlxmlns:th="http://www.thymeleaf.org"><head><title>注册</title></head><body><h2>注册</h2><formth:action="@{/register}"method="post"><div><label>用户名:</label><inputtype="text"name="username"required/></div><div><label>密码:</label><inputtype="password"name="password"required/></div><div><buttontype="submit">注册</button></div></form><p>已有账号?<ath:href="@{/login}">登录</a></p></body></html>

mfa.html

<!DOCTYPEhtml><htmlxmlns:th="http://www.thymeleaf.org"><head><title>MFA 验证</title></head><body><h2>多因素认证</h2><p>用户 <spanth:text="${username}"></span>,请输入手机应用上的 6 位动态验证码:</p><formth:action="@{/mfa-verify}"method="post"><div><label>动态验证码:</label><inputtype="text"name="code"pattern="\\d{6}"maxlength="6"required/></div><div><buttontype="submit">验证</button></div></form></body></html>

enable-mfa.html

<!DOCTYPEhtml><htmlxmlns: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: <spanth:text="${otpAuthURL}"></span></p><p>Secret Key: <spanth:text="${secret}"></span></p></div><p>设置完成后,请退出重新登录并输入动态验证码。</p></body></html>

home.html

<!DOCTYPEhtml><htmlxmlns:th="http://www.thymeleaf.org"><head><title>首页</title></head><body><h2>欢迎来到系统</h2><p>您已成功登录(且通过 MFA 验证)。</p><p><ath:href="@{/enable-mfa}">启用 MFA(如果尚未启用)</a></p><formth:action="@{/logout}"method="post"><buttontype="submit">退出登录</button></form></body></html>
注意:如果需要在页面中展示二维码,可以使用前端 QRCode.js 等库,将 otpAuthURL 渲染为二维码。

6. 总结与落地建议

本文从“为什么需要多因素认证”入手,讲解了基于 TOTP 的 MFA 核心原理,并详细演示了如何在 Spring Security 6 中分两步完成登录与 MFA 验证的流程。关键点回顾:

  1. 第一步:用户名+密码
    • 自定义 AuthenticationProvider,校验用户名与密码;
    • 若用户启用 MFA,则抛出 MfaRequiredException,并将用户名暂存到 Session。
  2. 第二步:TOTP 验证
    • 自定义 MfaAuthenticationFilter,拦截 /mfa-verify 请求;
    • 从 Session 中获取“待 MFA”用户名,调用服务端 TOTP 校验逻辑;
    • 校验通过后,构建最终 Authentication 并置入 SecurityContext
  3. MySQL + MyBatis-Plus
    • 在数据库 users 表中增加 mfa_enabledmfa_secret 字段;
    • Service 层通过 Google Authenticator 兼容库生成并验证动态验证码;
    • MyBatis-Plus 简化了实体与 Mapper 的开发。

实际生产环境推荐:

  • 二维码展示与绑定:在 /enable-mfa 页面使用前端二维码生成库(如 qrcode.js)将 otpAuthURL 渲染为二维码图片,方便用户扫码。
  • 密钥保护mfa_secret 为敏感数据,建议对其进行数据库加密存储或使用 KMS 等专用系统保护。
  • 备份码与恢复:当用户手机丢失时,可预先生成一组一次性“恢复码”,用户在绑定 MFA 时妥善保存,避免无法登录。
  • 登陆失败锁定:对于连续多次 TOTP 验证失败的账户,可暂时锁定或触发告警,防止暴力破解。
  • SSL/TLS 强制:确保所有页面(尤其是登录与 MFA 页面)使用 HTTPS,防止中间人攻击截获验证码。
  • 会话超时与防并发:可考虑对“待 MFA”状态的会话设置合理的超时时间(如 2 分钟),超时后必须重新进行第一步登录。

通过上面的设计与实现,企业级应用即可在原有用户名+密码的基础上,平滑地接入基于 TOTP 的多因素认证,大幅提升系统安全性,抵御常见的账户破解与钓鱼风险。

如果你在实践过程中有任何疑问或更好的扩展思路,欢迎在评论区留言,最后希望大家一键三连给博主一点点鼓励!


Read more

金仓数据库 MongoDB 兼容:多模融合下的架构之道与实战体验

金仓数据库 MongoDB 兼容:多模融合下的架构之道与实战体验

引言:从“平替”到“超越”的技术跨越 在国产化替代(信创)浪潮下,选择数据库不再只是考量“能否使用”,更多关注其“好用与否”,还要看是否能做到“无缝切换”。提到 MongoDB,想必大家都不生疏,作为 NoSQL 领域的佼佼者,凭借自身灵活的数据架构和飞快的读写效率,斩获诸多互联网及物联网项目,不过须要诚实地表明,一旦关乎到企业核心业务,譬如要确保数据完全一致,执行繁杂的关联查询或者实施统一运作管理时,MongoDB 就常常会有些力不从心。 电科金仓(Kingbase)所给出的多模融合数据库方案颇具趣味,该方案并非仅仅创建一层适配层来博取眼球,其实在架构层面上执行了“降维打击”,经由内核级别的 MongoDB 协议适配 并结合自主研发的 OSON 存储引擎,金仓把“关系型数据库稳定的基础”与“NoSQL 灵活的特性”融合起来,现在,让我们一起探究金仓数据库(KingbaseES,

By Ne0inhk
从千毫秒到亚毫秒:连接条件下推如何让复杂 SQL 飞起来

从千毫秒到亚毫秒:连接条件下推如何让复杂 SQL 飞起来

文章目录 * 前言 * 一、问题背景 * 1.1 客户场景中的典型痛点 * 1.2 业界普遍面临的两大难点 * 1.2.1 语义安全性(Equivalence) * 1.2.2 代价评估(Cost) * 二、传统方案的局限 * 三、金仓数据库基于代价的连接条件下推设计 * 3.1 能不能推:等价性判定(Equivalence) * 3.2 值不值推:代价模型(Cost) * 四、效果验证 * 4.1 最小化用例 * 4.2 复杂场景验证 * 五、总结 前言 在真实的业务系统中,SQL 往往远比教科书示例复杂。随着业务逻辑的不断演进,CTE、

By Ne0inhk
KingbaseES数据库:用 ksql 实现本地库创建 / 查看 / 切换 / 删除(附避坑技巧)

KingbaseES数据库:用 ksql 实现本地库创建 / 查看 / 切换 / 删除(附避坑技巧)

KingbaseES数据库:用 ksql 实现本地库创建 / 查看 / 切换 / 删除(附避坑技巧) 本文围绕本地 KingbaseES 数据库的全生命周期操作展开,先明确操作前的关键前提 —— 根据不同兼容模式确认 “权限库”(普通模式连任意已存库,SQLServer 兼容模式需连 master 库),并通过 \du 命令核查用户是否具备 CREATEDB 权限。核心讲解两种创建方式:推荐用 CREATE DATABASE 语句自定义编码、表空间等配置,也可通过 createdb 工具在系统终端快速创建。后续依次介绍 \l 查看所有库列表、\l + 查单库详情、\c 切换库的方法,强调切换前需提交事务避免数据回滚。删除操作重点提醒需先切换至其他库,建议加 IF EXISTS 选项,并做好数据备份以防丢失。最后针对权限不足、数据库被占用等高频报错,给出具体排查解决步骤,

By Ne0inhk
Flutter 组件 angel3_orm_mysql 的适配 鸿蒙Harmony 实战 - 驾驭专业 ORM 映射引擎、实现鸿蒙端与 MySQL 数据库的透明映射与高性能 SQL 审计方案

Flutter 组件 angel3_orm_mysql 的适配 鸿蒙Harmony 实战 - 驾驭专业 ORM 映射引擎、实现鸿蒙端与 MySQL 数据库的透明映射与高性能 SQL 审计方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 angel3_orm_mysql 的适配 鸿蒙Harmony 实战 - 驾驭专业 ORM 映射引擎、实现鸿蒙端与 MySQL 数据库的透明映射与高性能 SQL 审计方案 前言 在鸿蒙(OpenHarmony)生态向企业级中台应用、大屏数字化面板、以及需要直接操作中心数据库的特定内网管理工具拓展时,“数据库连接与对象关系映射(ORM)”是构建数据闭环的关键桥梁。虽然移动端通常通过 API 与后端交互。但在某些高性能、低延迟的私有云场景下(如:工厂本地监控大屏)。鸿蒙端需要直接与 MySQL 建立高压连接。并实现从 SQL 表结构到 Dart 实体的自动转换。 如果手动编写繁琐的 SELECT * 语句并逐字段进行 Map

By Ne0inhk