一、MFA简介
定义:多因素认证(MFA)要求用户在登录时提供至少两种不同类别的身份验证因子,以提升账户安全性
核心目标:解决单一密码认证的脆弱性(如暴力破解、钓鱼攻击),将账户被盗风险降低80%以上;通过组合不同的验证因素,MFA 能够显著降低因密码泄露带来的风险
二、核心原理
MFA通过多步骤验证构建安全屏障:
- 初始验证:用户输入用户名和密码(知识因子)
- 二次验证:系统要求额外因子(如手机接收OTP码、指纹扫描)
- 动态授权:高风险操作(如转账)可触发更多验证(如硬件令牌+生物识别)
- 访问控制:所有因子验证通过后,授予最小必要权限
安全增强逻辑:
- 攻击者即使破解密码(知识因子),仍需突破所有权或生物因子,难度呈指数级增长
- 例如:钓鱼攻击中窃取密码后,因无法获取动态令牌或生物特征而失败
三、主流技术方案与对比
认证方式 | 安全性 | 用户体验 | 实施成本 | 场景 | TOTP动态码 | 高 | 优 | 低 | 通用:企业系统、云服务等(推荐首选) | 短信验证码 | 中 | 中 | 中 | 金融支付、社交平台(需运营商集成) | 生物识别(如人脸、指纹等) | 极高 | 优 | 高 | 移动设备、高安全系统 | 硬件令牌(如YubiKey) | 极高 | 中 | 高 | 金融、政府、军事系统 | 四、TOTP简介
- 基于时间的一次性密码,动态验证码每30秒更新,基于共享密钥(Secret Key)和当前时间戳通过HMAC-SHA1算法生成6位数字。
- 优势:离线可用、无需短信成本、兼容Google Authenticator等标准应用
五、SpringBoot集成TOTP
a.登录流程图(这里原系统使用 SA-Token,其他逻辑应该也大差不差)
b.代码实现
1.添加Maven依赖
- <dependency>
- <groupId>com.warrenstrange</groupId>
- googleauth</artifactId>
- <version>1.5.0</version>
- </dependency>
- <dependency>
- <groupId>commons-net</groupId>
- commons-net</artifactId>
- <version>3.9.0</version>
- </dependency>
复制代码 2.Mfz服务类
- @Log4j2
- @Service
- public class MfaService {
- @Lazy
- @Resource
- private IotUserService iotUserService;
- @Resource
- private RedisUtil redisUtil;
- private final GoogleAuthenticator gAuth = new GoogleAuthenticator();
- /**
- * 为用户启用MFA,生成密钥和备用码
- */
- public MfaSetupResult setupMfa(String userId) {
- GoogleAuthenticatorKey key = gAuth.createCredentials();
- String secret = key.getKey();
- List<String> backupCodes = generateBackupCodes();
- // 加密存储(生产环境需替换为KMS加密)
- String encryptedSecret = encrypt(secret);
- log.info(secret + "====二维码生成===" + encryptedSecret);
- String encryptedBackupCodes = encrypt(String.join(",", backupCodes));
- IotUser user = iotUserService.getById(userId);
- if (user == null) {
- throw new RuntimeException("用户不存在");
- }
- // 更新数据库
- user.setMfaSecret(encryptedSecret);
- user.setBackupCodes(encryptedBackupCodes);
- user.setMfaEnabled(1);
- iotUserService.updateById(user);
- String qr = "otpauth://totp/" + userId + "?secret=" + secret + "&issuer=IOT_Platform"
- + "&image=https://iot-dev.xxxxxx.cn/static/img/logo.34793a79.png";
- return new MfaSetupResult(qr, backupCodes);
- }
- /**
- * 生成10个备用验证码(一次性使用)
- */
- private List<String> generateBackupCodes() {
- return new Random().ints(10, 100000, 999999)
- .mapToObj(code -> String.format("%06d", code))
- .collect(Collectors.toList());
- }
- /**
- * 验证TOTP或备用码
- */
- public boolean verifyCode(String userId, String code) {
- IotUser user = iotUserService.getById(userId);
- if (user == null) {
- throw new RuntimeException("用户不存在");
- }
- // 1. 获取加密的密钥和备用码
- String encryptedSecret = user.getMfaSecret();
- String encryptedBackupCodes = user.getBackupCodes();
- String secret = decrypt(encryptedSecret);
- log.info(secret + "校验" + encryptedSecret);
- List<String> backupCodes = new ArrayList<>(
- Arrays.asList(decrypt(encryptedBackupCodes).split(","))
- );
- // 2. 验证TOTP(允许时间偏差)
- if (gAuth.authorize(secret, Integer.parseInt(code))) {
- return true;
- }
- // 3. 验证备用码
- if (backupCodes.contains(code)) {
- backupCodes.remove(code);
- // 更新数据库
- user.setBackupCodes(encrypt(String.join(",", backupCodes)));
- iotUserService.updateById(user);
- return true;
- }
- return false;
- }
- /**
- * 开启7天免MFA认证
- */
- public void setMfaSkip(String userId, String userAgent, String ip) {
- String deviceHash = DigestUtils.sha256Hex(userAgent + ip).substring(0, 8);
- String key = "mfa_skip:" + userId + ":" + deviceHash;
- long expireAt = System.currentTimeMillis() + 7 * 86_400_000L;
- String value = expireAt + "|" + userAgent;
- redisUtil.setEx(key, value, 7, TimeUnit.DAYS);
- }
- /**
- * 验证是否已开启免MFA认证
- */
- public boolean isMfaSkipped(String userId, String userAgent, String ip) {
- String deviceHash = DigestUtils.sha256Hex(userAgent + ip).substring(0, 8);
- String key = "mfa_skip:" + userId + ":" + deviceHash;
- String value = redisUtil.get(key);
- if (value == null) {
- return false;
- }
- // 验证设备信息一致性(防盗用)
- String[] parts = value.split("\\|");
- long expireAt = Long.parseLong(parts[0]);
- String storedUserAgent = parts[1];
- return expireAt > System.currentTimeMillis()
- && storedUserAgent.equals(userAgent);
- }
- // --- AES加密工具方法 ---
- private String encrypt(String data) {
- // 实际实现需使用AES-GCM(此处简化)
- return Base64.getEncoder().encodeToString(data.getBytes());
- }
- private String decrypt(String encrypted) {
- return new String(Base64.getDecoder().decode(encrypted));
- }
- }
复制代码 3. IP获取工具IpUtils
- public class IpUtils {
- public static String getClientIp(HttpServletRequest request) {
- // 1. 优先级解析代理头部
- String[] headers = {"X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"};
- for (String header : headers) {
- String ip = request.getHeader(header);
- if (isValidIp(ip)) {
- return parseFirstIp(ip);
- }
- }
- // 2. 直接获取远程地址
- String ip = request.getRemoteAddr();
- // 3. 处理本地环回地址(开发环境)
- if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
- try {
- return InetAddress.getLocalHost().getHostAddress();
- } catch (Exception e) {
- return "127.0.0.1";
- }
- }
- return ip;
- }
- private static boolean isValidIp(String ip) {
- return ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip);
- }
- private static String parseFirstIp(String ip) {
- // 处理多IP场景(如:X-Forwarded-For: client, proxy1, proxy2)
- return ip.contains(",") ? ip.split(",")[0].trim() : ip;
- }
- }
复制代码 4.登录、Mfa开启、Mfa校验、Mfa二维码以及10个备用一次性code生成(服务类省略)
- @Override
- public LoginResult login(LoginParam loginParam, HttpServletRequest request) {
- IotUser iotUser = this.getOne(new LambdaQueryWrapper<IotUser>().eq(IotUser::getAccount, loginParam.getAccount())
- .eq(IotUser::getStatus, 0));
- // 校验用户是否存在
- if (ObjectUtil.isNull(iotUser)) {
- throw new ServiceException(IotUserExceptionEnum.LOGIN_ERROR);
- }
- // 验证账号密码是否正常
- String requestMd5 = SaltUtil.md5Encrypt(loginParam.getPassword(), iotUser.getSalt());
- String dbMd5 = iotUser.getPassword();
- if (dbMd5 == null || !dbMd5.equalsIgnoreCase(requestMd5)) {
- throw new ServiceException(IotUserExceptionEnum.LOGIN_ERROR);
- }
- // 账号被冻结
- if (iotUser.getStatus().equals(1)) {
- throw new ServiceException(IotUserExceptionEnum.ACCOUNT_FREEZE_ERROR);
- }
- // 密码校验成功后登录,一行代码实现登录
- StpUtil.login(iotUser.getUserId());
- StpUtil.getSession().set(Constants.USER_INFO_KEY, userDto(iotUser));
- /** 获取当前登录用户的Token信息 */
- SaTokenInfo saTokenInfo = StpUtil.getTokenInfo();
- LoginResult loginResult = new LoginResult();
- loginResult.setToken(saTokenInfo.getTokenValue());
- loginResult.setMfaEnabled(iotUser.getMfaEnabled());
- // 开启了MFA认证
- if (iotUser.getMfaEnabled() == 1) {
- String ua = request.getHeader("User-Agent");
- String ip = IpUtils.getClientIp(request);
- log.info("登录请求IP:" + ip);
- if (mfaService.isMfaSkipped(iotUser.getUserId(), ua, ip)) {
- // 触发免验证:激活安全会话
- StpUtil.openSafe( 7 * 24 * 60 * 60);
- } else {
- loginResult.setNeedMfa(true);
- }
- }
- return loginResult;
- }
- @Override
- public VerifyResult verify(MfaVerifyParam verifyParam, HttpServletRequest request) {
- if (ObjectUtil.isNull(verifyParam.getCode())) {
- throw new ServiceException("验证码不能为空");
- }
- if (ObjectUtil.isNull(verifyParam.getRemember())) {
- verifyParam.setRemember(false);
- }
- String userId = StpUtil.getLoginIdAsString();
- // 1. 验证TOTP/备用码
- if (!mfaService.verifyCode(userId, verifyParam.getCode())) {
- throw new ServiceException("验证码无效");
- }
- // 2. 若选择免认证7天,更新数据库
- if (Boolean.TRUE.equals(verifyParam.getRemember())) {
- String userAgent = request.getHeader("User-Agent");
- String ip = IpUtils.getClientIp(request);
- log.info("MFA验证请求IP:" + ip);
- mfaService.setMfaSkip(userId, userAgent, ip);
- }
- // 3. 激活SA-Token安全会话(7天或一次性)
- StpUtil.openSafe(verifyParam.getRemember() ? 7 * 24 * 60 * 60 : 120);
- VerifyResult verifyResult = new VerifyResult();
- verifyResult.setToken(StpUtil.getTokenValue());
- verifyResult.setMsg("验证成功");
- return verifyResult;
- }
- @Override
- public MfaSetupResult qrCode() {
- String userId = StpUtil.getLoginIdAsString();
- return mfaService.setupMfa(userId);
- }
- @Override
- public void openMfa() {
- String userId = StpUtil.getLoginIdAsString();
- IotUser iotUser = this.getById(userId);
- if (ObjectUtil.isNull(iotUser)) {
- throw new ServiceException("用户不存在");
- }
- iotUser.setMfaEnabled(1);
- this.updateById(iotUser);
- }
复制代码 5.Mfa校验入参类
- @Data
- public class MfaVerifyParam {
- /**
- * Mfa动态、一次性备用代码
- */
- private String code;
- /**
- * 当前机器近7天是否跳过Mfa校验
- */
- private Boolean remember;
- }
复制代码 6.控制类
- @RestController
- public class IotPlatFormAuthController {
- @Resource
- private IotUserService iotUserService;
- /**
- * @description: 登录
- * @param: [loginParam]
- * @return: com.honyar.core.model.response.ResponseData
- * @author: zhouhong
- */
- @PostMapping("/auth/login")
- public ResponseData login(@RequestBody LoginParam loginParam, HttpServletRequest request) {
- return new SuccessResponseData(iotUserService.login(loginParam, request));
- }
- /**
- * @description: 开启MFA
- * @param: []
- * @return: com.honyar.core.model.response.ResponseData
- * @author: zhouhong
- */
- @PostMapping("/auth/mfa/openMfa")
- public ResponseData openMfa() {
- iotUserService.openMfa();
- return new SuccessResponseData();
- }
- /**
- * @description: 获取MFA二维码
- * @param: []
- * @return: com.honyar.core.model.response.ResponseData
- * @author: zhouhong
- */
- @PostMapping("/auth/mfa/qrcode")
- public ResponseData qrCode() {
- return new SuccessResponseData(iotUserService.qrCode());
- }
- /**
- * @description: MFA验证
- * @param: [verifyParam]
- * @return: com.honyar.core.model.response.ResponseData
- * @author: zhouhong
- */
- @PostMapping("/auth/mfa/verify")
- public ResponseData verify(@RequestBody MfaVerifyParam verifyParam, HttpServletRequest request) {
- return new SuccessResponseData(iotUserService.verify(verifyParam, request));
- }
- /**
- * @description: 登出
- * @param: []
- * @return: com.honyar.core.model.response.ResponseData
- * @author: zhouhong
- */
- @PostMapping("/auth/logout")
- public ResponseData logout() {
- iotUserService.logout();
- return new SuccessResponseData();
- }
- }
复制代码 c.演示
1.调用登录接口
说明:登录返回当前用户是否已经开启Mfa,当用户已经开启mfa(mfaEnable=1)并且needMfa(需要进行mfa)时需要前端拉起mfa校验页面调用mfa校验接口进行二次校验;当mfaEnable=1并且needMfa=false时,说明当前设备已经开启7天面mfa校验,直接登录成功进入系统;当mfaEnable=0时,说明用户为开启mfa,则引导用户调用接口先开启mfa(数据库用户mfaEnable字段置为1即可),然后再调用mfa校验接口进行mfa校验,如果用户选择不开启则直接登录成功进入系统。
2.调用mfa二维码、备用一次性code生成接口
说明:调用这个接口后前端根据 qrUrl信息生成一个二维码,并且同时浏览器下载备用code 到本地,用户使用Authenticator APP进行扫码添加用户,然后再使用 Authenticator 里面生成的code调用校验Mfa接口校验成功后进入系统;第二次用户直接从Authenticator获取code进行二次认证即可
3.调用Mfa校验接口
说明:校验成功后进入系统
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |