<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      SpringBoot中使用TOTP實現MFA(多因素認證)

      一、MFA簡介

      定義:多因素認證(MFA)要求用戶在登錄時提供??至少兩種不同類別??的身份驗證因子,以提升賬戶安全性

      核心目標:解決單一密碼認證的脆弱性(如暴力破解、釣魚攻擊),將賬戶被盜風險降低??80%以上;通過組合不同的驗證因素,MFA 能夠顯著降低因密碼泄露帶來的風險

      二、核心原理

      MFA通過多步驟驗證構建安全屏障:

      1. ??初始驗證??:用戶輸入用戶名和密碼(知識因子)
      2. ??二次驗證??:系統要求額外因子(如手機接收OTP碼、指紋掃描)
      3. ??動態授權??:高風險操作(如轉賬)可觸發更多驗證(如硬件令牌+生物識別)
      4. ??訪問控制??:所有因子驗證通過后,授予最小必要權限

      ??安全增強邏輯??:

      • 攻擊者即使破解密碼(知識因子),仍需突破所有權或生物因子,難度呈指數級增長
      • 例如:釣魚攻擊中竊取密碼后,因無法獲取動態令牌或生物特征而失敗

      三、主流技術方案與對比

      認證方式 安全性 用戶體驗 實施成本 場景
      TOTP動態碼?? 通用:企業系統、云服務等(推薦首選)
      ??短信驗證碼? 金融支付、社交平臺(需運營商集成)
      生物識別??(如人臉、指紋等) 極高 移動設備、高安全系統
      ??硬件令牌??(如YubiKey) 極高 金融、政府、軍事系統

      四、TOTP簡介

      1. 基于時間的一次性密碼,動態驗證碼每30秒更新,基于共享密鑰(Secret Key)和當前時間戳通過HMAC-SHA1算法生成6位數字。
      2. 優勢??:離線可用、無需短信成本、兼容Google Authenticator等標準應用

      五、SpringBoot集成TOTP

      a.登錄流程圖(這里原系統使用 SA-Token,其他邏輯應該也大差不差)

      Untitled diagram _ Mermaid Chart-2025-07-28-121800

      b.代碼實現

      原系統用戶表添加以下字段

      ALTER TABLE iot_user
      ADD COLUMN mfa_secret VARCHAR(64) NULL COMMENT 'TOTP密鑰(AES加密存儲)',
      ADD COLUMN backup_codes TEXT NULL COMMENT '備用驗證碼(JSON數組,AES加密存儲)',
      ADD COLUMN mfa_enabled TINYINT(1) DEFAULT 0 COMMENT '是否啟用MFA(0-否,1-是)';
      1.添加Maven依賴
          <dependency>
                  <groupId>com.warrenstrange</groupId>
                  <artifactId>googleauth</artifactId>
                  <version>1.5.0</version>
              </dependency>
              <dependency>
                  <groupId>commons-net</groupId>
                  <artifactId>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);
              iotUserService.updateById(user);
              String qr = "otpauth://totp/" + userId + "?secret=" + secret + "&issuer=IOT_Platform";
              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("驗證碼無效");
              }
              String userAgent = request.getHeader("User-Agent");
              String ip = IpUtils.getClientIp(request);
              // 2. 若選擇免認證7天,更新數據庫
              if (Boolean.TRUE.equals(verifyParam.getRemember())) {
                  log.info("MFA驗證請求IP:" +  ip);
                  mfaService.setMfaSkip(userId, userAgent, ip);
              } else {
                  // 未選擇7天免認證,則刪除redis
                  String deviceHash = DigestUtils.sha256Hex(userAgent + ip).substring(0, 8);
                  String key = "mfa_skip:" + userId + ":" + deviceHash;
                  redisUtil.delete(key);
              }
              // 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() {
              return mfaService.setupMfa();
          }
          @Override
          public void openMfa(RecoverMfaParam recoverMfaParam) {
              if (ObjectUtil.isNull(recoverMfaParam.getCode())) {
                  throw new ServiceException("請輸入驗證碼");
              }
              String userId = StpUtil.getLoginIdAsString();
              IotUser iotUser = this.getById(userId);
              if (ObjectUtil.isNull(iotUser)) {
                  throw new ServiceException("用戶不存在");
              }
              if (mfaService.verifyCode(userId, recoverMfaParam.getCode())) {
                  iotUser.setMfaEnabled(1);
                  this.updateById(iotUser);
              } else {
                  throw new ServiceException("驗證碼錯誤");
              }
          }
          @Override
          public void recoverMfa(RecoverMfaParam recoverMfaParam, HttpServletRequest request) {
              if (ObjectUtil.isNull(recoverMfaParam.getCode())) {
                  throw new ServiceException("恢復碼code不能為空");
              }
              String userId = StpUtil.getLoginIdAsString();
              IotUser iotUser = this.getById(userId);
              if (ObjectUtil.isNull(iotUser)) {
                  throw new ServiceException("用戶不存在");
              }
              String encryptedBackupCodes = iotUser.getBackupCodes();
              List<String> backupCodes = new ArrayList<>(
                      Arrays.asList(decrypt(encryptedBackupCodes).split(","))
              );
              if (mfaService.verifyCode(userId, recoverMfaParam.getCode())) {
                  backupCodes.remove(recoverMfaParam.getCode());
                  // 更新數據庫
                  iotUser.setBackupCodes(encrypt(String.join(",", backupCodes)));
                  // 重置MFA,再次登錄時需要重新設置并掃碼綁定
                  iotUser.setMfaEnabled(0);
                  iotUser.setMfaSecret(null);
                  iotUser.setBackupCodes(null);
                  this.updateById(iotUser);
                  String userAgent = request.getHeader("User-Agent");
                  String ip = IpUtils.getClientIp(request);
                  String deviceHash = DigestUtils.sha256Hex(userAgent + ip).substring(0, 8);
                  String key = "mfa_skip:" + userId + ":" + deviceHash;
                  redisUtil.delete(key);
              } else {
                  throw new ServiceException(1, "備用碼或MFA碼錯誤");
              }
          }
          @Override
          public void logout() {
              /** 會話注銷 */
              StpUtil.logoutByTokenValue(StpUtil.getTokenValue());
          }
      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(@RequestBody RecoverMfaParam recoverMfaParam) {
              iotUserService.openMfa(recoverMfaParam);
              return new SuccessResponseData();
          }
          /**
           * @description:  恢復MFA(用戶未掃描二維碼,需要使用備用碼重置并在下次登錄時重新設置MFA)
           * @param: [recoverMfaParam]
           * @return: com.honyar.core.model.response.ResponseData
           * @author: zhouhong
           */
          @PostMapping("/auth/mfa/recoverMfa")
          public ResponseData recoverMfa(@RequestBody RecoverMfaParam recoverMfaParam, HttpServletRequest request) {
              iotUserService.recoverMfa(recoverMfaParam, request);
              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,再使用綁定的code調用接口開啟mfa(數據庫用戶mfaEnable字段置為1即可),然后再調用mfa校驗接口進行mfa校驗,如果用戶選擇不開啟則直接登錄成功進入系統。

      2.調用mfa二維碼、備用一次性code生成接口

      生成二維碼

      說明:調用這個接口后前端根據 qrUrl信息生成一個二維碼,并且同時瀏覽器下載備用code 到本地,用戶使用Authenticator APP進行掃碼添加用戶,然后再使用 Authenticator 里面生成的code調用校驗Mfa接口校驗成功后進入系統;第二次用戶直接從Authenticator獲取code進行二次認證即可

      首頁code

      3.使用code開啟當前登錄用戶的MFA

      image

      4.調用Mfa校驗接口

      校驗1

      說明:校驗成功后進入系統

      5.恢復/解綁MFA

      image

       需要使用MFA或者生成二維碼時的備用碼來解綁

      posted @ 2025-07-28 22:22  Tom-shushu  閱讀(901)  評論(5)    收藏  舉報
      主站蜘蛛池模板: caoporn免费视频公开| 五月婷之久久综合丝袜美腿| 亚洲色欲色欲www| 亚洲爆乳少妇无码激情| 自拍视频一区二区三区四区| 日韩精品国产二区三区| 国产精品福利自产拍久久| 视频专区熟女人妻第二页| 亚洲av本道一区二区| 国产老妇伦国产熟女老妇高清| 久久久久久国产精品美女| 国产精品久久久久7777| 99热久久这里只有精品| 色婷婷久久综合中文久久一本| 人妻内射视频麻豆| 日韩精品 在线一区二区| 国产福利酱国产一区二区| 人妻系列无码专区久久五月天| 国产学生裸体无遮挡免费| 亚洲精品日韩久久精品| 天天干天天日| 日本一级午夜福利免费区| 日韩免费码中文在线观看| 国产精品免费看久久久| 四虎影院176| 欧美经典人人爽人人爽人人片| 国产另类ts人妖一区二区| 国产精品永久免费无遮挡| 亚洲欧美综合精品成人导航 | 亚洲精品国产一区二区在线观看 | 微拍福利一区二区三区| 女人张开腿无遮无挡视频| 日本亚洲色大成网站www久久 | 日韩卡一卡2卡3卡4卡| 综合偷自拍亚洲乱中文字幕 | 综合偷自拍亚洲乱中文字幕 | 人妻另类 专区 欧美 制服| 激情综合网激情五月激情| 亚洲高清aⅴ日本欧美视频| 免费无码中文字幕A级毛片| 中文字幕精品亚洲字幕成|