欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringBoot項(xiàng)目如何添加2FA雙因素身份認(rèn)證

 更新時(shí)間:2024年04月25日 11:50:55   作者:會(huì)飛的一棵樹(shù)  
雙因素身份驗(yàn)證2FA是一種安全系統(tǒng),要求用戶提供兩種不同的身份驗(yàn)證方式才能訪問(wèn)某個(gè)系統(tǒng)或服務(wù),國(guó)內(nèi)普遍做短信驗(yàn)證碼這種的用的比較少,不過(guò)在國(guó)外的網(wǎng)站中使用雙因素身份驗(yàn)證的還是很多的,這篇文章主要介紹了SpringBoot項(xiàng)目如何添加2FA雙因素身份認(rèn)證,需要的朋友參考下

什么是 2FA(雙因素身份驗(yàn)證)?

雙因素身份驗(yàn)證(2FA)是一種安全系統(tǒng),要求用戶提供兩種不同的身份驗(yàn)證方式才能訪問(wèn)某個(gè)系統(tǒng)或服務(wù)。國(guó)內(nèi)普遍做短信驗(yàn)證碼這種的用的比較少,不過(guò)在國(guó)外的網(wǎng)站中使用雙因素身份驗(yàn)證的還是很多的。用戶通過(guò)使用驗(yàn)證器掃描二維碼,就能在app上獲取登錄的動(dòng)態(tài)口令,進(jìn)一步加強(qiáng)了賬戶的安全性。

主要步驟

pom.xml中增加依賴

<!-- 用于SecureKey生成 -->
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.15</version>
</dependency>
<!-- 二維碼依賴 -->
<dependency>
    <groupId>org.iherus</groupId>
    <artifactId>qrext4j</artifactId>
    <version>1.3.1</version>
</dependency>

用戶表中增加secretKey列

為用戶綁定secretKey字段,用以生成二維碼及后期校驗(yàn)

image

工具類

谷歌身份驗(yàn)證器工具類

/**
 * 谷歌身份驗(yàn)證器工具類
 */
public class GoogleAuthenticator {
    /**
     * 時(shí)間前后偏移量
     * 用于防止客戶端時(shí)間不精確導(dǎo)致生成的TOTP與服務(wù)器端的TOTP一直不一致
     * 如果為0,當(dāng)前時(shí)間為 10:10:15
     * 則表明在 10:10:00-10:10:30 之間生成的TOTP 能校驗(yàn)通過(guò)
     * 如果為1,則表明在
     * 10:09:30-10:10:00
     * 10:10:00-10:10:30
     * 10:10:30-10:11:00 之間生成的TOTP 能校驗(yàn)通過(guò)
     * 以此類推
     */
    private static int WINDOW_SIZE = 0;
    /**
     * 加密方式,HmacSHA1、HmacSHA256、HmacSHA512
     */
    private static final String CRYPTO = "HmacSHA1";
    /**
     * 生成密鑰,每個(gè)用戶獨(dú)享一份密鑰
     *
     * @return
     */
    public static String getSecretKey() {
        SecureRandom random = new SecureRandom();
        byte[] bytes = new byte[20];
        random.nextBytes(bytes);
        Base32 base32 = new Base32();
        String secretKey = base32.encodeToString(bytes);
        // make the secret key more human-readable by lower-casing and
        // inserting spaces between each group of 4 characters
        return secretKey.toUpperCase();
    }
    /**
     * 生成二維碼內(nèi)容
     *
     * @param secretKey 密鑰
     * @param account   賬戶名
     * @param issuer    網(wǎng)站地址(可不寫)
     * @return
     */
    public static String getQrCodeText(String secretKey, String account, String issuer) {
        String normalizedBase32Key = secretKey.replace(" ", "").toUpperCase();
        try {
            return "otpauth://totp/"
                    + URLEncoder.encode((!StringUtils.isEmpty(issuer) ? (issuer + ":") : "") + account, "UTF-8").replace("+", "%20")
                    + "?secret=" + URLEncoder.encode(normalizedBase32Key, "UTF-8").replace("+", "%20")
                    + (!StringUtils.isEmpty(issuer) ? ("&issuer=" + URLEncoder.encode(issuer, "UTF-8").replace("+", "%20")) : "");
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException(e);
        }
    }
    /**
     * 獲取驗(yàn)證碼
     *
     * @param secretKey
     * @return
     */
    public static String getCode(String secretKey) {
        String normalizedBase32Key = secretKey.replace(" ", "").toUpperCase();
        Base32 base32 = new Base32();
        byte[] bytes = base32.decode(normalizedBase32Key);
        String hexKey = Hex.encodeHexString(bytes);
        long time = (System.currentTimeMillis() / 1000) / 30;
        String hexTime = Long.toHexString(time);
        return TOTP.generateTOTP(hexKey, hexTime, "6", CRYPTO);
    }
    /**
     * 檢驗(yàn) code 是否正確
     *
     * @param secret 密鑰
     * @param code   code
     * @param time   時(shí)間戳
     * @return
     */
    public static boolean checkCode(String secret, long code, long time) {
        Base32 codec = new Base32();
        byte[] decodedKey = codec.decode(secret);
        // convert unix msec time into a 30 second "window"
        // this is per the TOTP spec (see the RFC for details)
        long t = (time / 1000L) / 30L;
        // Window is used to check codes generated in the near past.
        // You can use this value to tune how far you're willing to go.
        long hash;
        for (int i = -WINDOW_SIZE; i <= WINDOW_SIZE; ++i) {
            try {
                hash = verifyCode(decodedKey, t + i);
            } catch (Exception e) {
                // Yes, this is bad form - but
                // the exceptions thrown would be rare and a static
                // configuration problem
                // e.printStackTrace();
                throw new RuntimeException(e.getMessage());
            }
            if (hash == code) {
                return true;
            }
        }
        return false;
    }
    /**
     * 根據(jù)時(shí)間偏移量計(jì)算
     *
     * @param key
     * @param t
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     */
    private static long verifyCode(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException {
        byte[] data = new byte[8];
        long value = t;
        for (int i = 8; i-- > 0; value >>>= 8) {
            data[i] = (byte) value;
        }
        SecretKeySpec signKey = new SecretKeySpec(key, CRYPTO);
        Mac mac = Mac.getInstance(CRYPTO);
        mac.init(signKey);
        byte[] hash = mac.doFinal(data);
        int offset = hash[20 - 1] & 0xF;
        // We're using a long because Java hasn't got unsigned int.
        long truncatedHash = 0;
        for (int i = 0; i < 4; ++i) {
            truncatedHash <<= 8;
            // We are dealing with signed bytes:
            // we just keep the first byte.
            truncatedHash |= (hash[offset + i] & 0xFF);
        }
        truncatedHash &= 0x7FFFFFFF;
        truncatedHash %= 1000000;
        return truncatedHash;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            String secretKey = getSecretKey();
            System.out.println("secretKey:" + secretKey);
            String code = getCode(secretKey);
            System.out.println("code:" + code);
            boolean b = checkCode(secretKey, Long.parseLong(code), System.currentTimeMillis());
            System.out.println("isSuccess:" + b);
        }
    }
}

二維碼工具類

/**
 * 驗(yàn)證碼生成工具類
 */
public class TOTP {
    private static final int[] DIGITS_POWER = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
    /**
     * This method uses the JCE to provide the crypto algorithm. HMAC computes a
     * Hashed Message Authentication Code with the crypto hash algorithm as a
     * parameter.
     *
     * @param crypto   : the crypto algorithm (HmacSHA1, HmacSHA256, HmacSHA512)
     * @param keyBytes : the bytes to use for the HMAC key
     * @param text     : the message or text to be authenticated
     */
    private static byte[] hmac_sha(String crypto, byte[] keyBytes, byte[] text) {
        try {
            Mac hmac;
            hmac = Mac.getInstance(crypto);
            SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
            hmac.init(macKey);
            return hmac.doFinal(text);
        } catch (GeneralSecurityException gse) {
            throw new UndeclaredThrowableException(gse);
        }
    }
    /**
     * This method converts a HEX string to Byte[]
     *
     * @param hex : the HEX string
     * @return: a byte array
     */
    private static byte[] hexStr2Bytes(String hex) {
        // Adding one byte to get the right conversion
        // Values starting with "0" can be converted
        byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();
        // Copy all the REAL bytes, not the "first"
        byte[] ret = new byte[bArray.length - 1];
        System.arraycopy(bArray, 1, ret, 0, ret.length);
        return ret;
    }
    /**
     * This method generates a TOTP value for the given set of parameters.
     *
     * @param key          : the shared secret, HEX encoded
     * @param time         : a value that reflects a time
     * @param returnDigits : number of digits to return
     * @param crypto       : the crypto function to use
     * @return: a numeric String in base 10 that includes
     */
    public static String generateTOTP(String key, String time, String returnDigits, String crypto) {
        int codeDigits = Integer.decode(returnDigits);
        String result = null;
        // Using the counter
        // First 8 bytes are for the movingFactor
        // Compliant with base RFC 4226 (HOTP)
        while (time.length() < 16) {
            time = "0" + time;
        }
        // Get the HEX in a Byte[]
        byte[] msg = hexStr2Bytes(time);
        byte[] k = hexStr2Bytes(key);
        byte[] hash = hmac_sha(crypto, k, msg);
        // put selected bytes into result int
        int offset = hash[hash.length - 1] & 0xf;
        int binary = ((hash[offset] & 0x7f) << 24)
                | ((hash[offset + 1] & 0xff) << 16)
                | ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);
        int otp = binary % DIGITS_POWER[codeDigits];
        result = Integer.toString(otp);
        while (result.length() < codeDigits) {
            result = "0" + result;
        }
        return result;
    }
}

Service

@Transactional(isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED)
@Service
public class TwoFAService {
    @Autowired
    private UserMapper userMapper;
   /**
    * 獲取SecureKey
    */
    public String getSecureKey(Integer userId) {
        User user = userMapper.selectUserById(userId);
        return user.getSecretKey();
    }
    /**
     * 更新secureKey
     */
    public Integer updateSecureKey(Integer userId, String secureKey) {
        return userMapper.updateSecureKeyById(userId, secureKey);
    }
   /**
    * 校驗(yàn)動(dòng)態(tài)碼
    */
    public boolean chek2FACode(User user, String twoFACode) throws Exception {
        String secretKey = user.getSecretKey();
        // 沒(méi)綁定設(shè)備就先驗(yàn)證通過(guò)
        if(secretKey == null || secretKey.isEmpty()) {
            return true;
        } else  {
           if(twoFACode.isEmpty()) { throw new Exception("已綁定設(shè)備,請(qǐng)輸入動(dòng)態(tài)碼"); }
           boolean checkRes = GoogleAuthenticator.checkCode(secretKey, Long.parseLong(twoFACode), System.currentTimeMillis());
           if(!checkRes) {
               throw new Exception("動(dòng)態(tài)碼錯(cuò)誤");
           } else {
               return true;
           }
        }
    }
}

Controller

用戶登錄中增加兩步驗(yàn)證:

@Controller
@RequestMapping(value = "/mgr")
public class UserController {
    @Autowired
    private UserService userService;
    @Autowired
    private LogService logService;
    @Autowired
    private TwoFAService twoFAService;
    /**
     * @Description: 用戶登錄
     */
    @RequestMapping(value = "/user/login", method = RequestMethod.POST)
    @ResponseBody
    public GlobalResult login(String userCode, String userPwd, String twoFACode) {
        try {
            UsernamePasswordToken token = new UsernamePasswordToken(userCode, userPwd);
            Subject subject = SecurityUtils.getSubject();
            subject.login(token);
            // 2FA驗(yàn)證
            User user = (User) subject.getPrincipal();
            twoFAService.chek2FACode(user, twoFACode);
            Log log = new Log();
            .......
        }
    }
}

兩步驗(yàn)證的Controler

@RestController
@RequestMapping(value = "/2fa")
public class TwoFAController {
    @Autowired
    private TwoFAService twoFAService;
    /**
     * 生成二維碼信息對(duì)象
     */
    @GetMapping("/getQrcode")
    public QrCodeResponse getQrcode(@RequestParam("userId") Integer userId, @RequestParam("userCode") String userCode, HttpServletResponse response) throws Exception {
        try {
            String secretKey = twoFAService.getSecureKey(userId);
            QrCodeResponse qrCodeResponse = new QrCodeResponse();
            if(secretKey == null || secretKey.isEmpty()) {
                secretKey = GoogleAuthenticator.getSecretKey();
                qrCodeResponse.setBind(false);
                // userMapper.updateSecureKeyById(userId, secretKey);
            } else {
                qrCodeResponse.setBind(true);
            }
            // 生成二維碼內(nèi)容
            String qrCodeText = GoogleAuthenticator.getQrCodeText(secretKey, userCode, "suggest-mgr");
            // 以流的形式返回生成二維碼輸出
            // new SimpleQrcodeGenerator().generate(qrCodeText).toStream(response.getOutputStream());
            BufferedImage image = new SimpleQrcodeGenerator().generate(qrCodeText).getImage();
            // 將圖片轉(zhuǎn)換為Base64字符串
            String base64Image = convertImageToBase64(image);
            qrCodeResponse.setQrCodeText(secretKey);
            qrCodeResponse.setBase64Image(base64Image);
            return qrCodeResponse;
        } catch (Exception e) {
            // 處理異常
            e.printStackTrace();
            return null; // 或者返回適當(dāng)?shù)腻e(cuò)誤信息
        }
    }
    /**
     * 更新SecretKey
     * @param userId
     * @param secretKey
     */
    @GetMapping("/updateSecretKey")
    public void updateSecretKey(@RequestParam("userId") Integer userId, @RequestParam("secretKey") String secretKey) {
        twoFAService.updateSecureKey(userId, secretKey);
    }
    /**
     * 獲取新的secretKey 重置用
     * @param userId
     * @param userCode
     * @return
     */
    @GetMapping("/getNewSecretKey")
    public QrCodeResponse getNewSecretKey(@RequestParam("userId") Integer userId, @RequestParam("userCode") String userCode, HttpServletResponse response) throws Exception {
        try {
            String secretKey = secretKey = GoogleAuthenticator.getSecretKey();
            QrCodeResponse qrCodeResponse = new QrCodeResponse();
            qrCodeResponse.setBind(false);
            // 生成二維碼內(nèi)容
            String qrCodeText = GoogleAuthenticator.getQrCodeText(secretKey, userCode, "xxx-site");
            BufferedImage image = new SimpleQrcodeGenerator().generate(qrCodeText).getImage();
            // 將圖片轉(zhuǎn)換為Base64字符串
            String base64Image = convertImageToBase64(image);
            qrCodeResponse.setQrCodeText(secretKey);
            qrCodeResponse.setBase64Image(base64Image);
            // 返回包含qrCodeText和Base64編碼圖片的信息
            return qrCodeResponse;
        } catch (Exception e) {
            // 處理異常
            e.printStackTrace();
            return null; // 或者返回適當(dāng)?shù)腻e(cuò)誤信息
        }
    }
    /**
     * 將圖片文件流轉(zhuǎn)為base64
     */
    private String convertImageToBase64(BufferedImage image) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ImageIO.write(image, "png", baos);
            byte[] imageBytes = baos.toByteArray();
            return Base64.getEncoder().encodeToString(imageBytes);
        } catch (Exception e) {
            // 處理異常
            return "";
        }
    }
    static public class QrCodeResponse {
        private String secretKey;
        private String base64Image;
        private boolean isBind;
        public String getSecretKey() {
            return secretKey;
        }
        public void setSecretKeyt(String secretKey) {
            this.secretKey = secretKey;
        }
        public String getBase64Image() {
            return base64Image;
        }
        public void setBase64Image(String base64Image) {
            this.base64Image = base64Image;
        }
        public boolean isBind() {
            return isBind;
        }
        public void setBind(boolean bind) {
            isBind = bind;
        }
    }
}

常用2FA驗(yàn)證工具

到此這篇關(guān)于SpringBoot項(xiàng)目添加2FA雙因素身份認(rèn)證的文章就介紹到這了,更多相關(guān)SpringBoot雙因素身份認(rèn)證內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Java并發(fā)編程:CountDownLatch與CyclicBarrier和Semaphore的實(shí)例詳解

    Java并發(fā)編程:CountDownLatch與CyclicBarrier和Semaphore的實(shí)例詳解

    這篇文章主要介紹了Java并發(fā)編程:CountDownLatch與CyclicBarrier和Semaphore的實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下
    2017-09-09
  • Java的反射機(jī)制---動(dòng)態(tài)調(diào)用對(duì)象的簡(jiǎn)單方法

    Java的反射機(jī)制---動(dòng)態(tài)調(diào)用對(duì)象的簡(jiǎn)單方法

    下面小編就為大家?guī)?lái)一篇Java的反射機(jī)制---動(dòng)態(tài)調(diào)用對(duì)象的簡(jiǎn)單方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2016-07-07
  • Java線程池如何實(shí)現(xiàn)精準(zhǔn)控制每秒API請(qǐng)求

    Java線程池如何實(shí)現(xiàn)精準(zhǔn)控制每秒API請(qǐng)求

    這篇文章主要介紹了Java線程池如何實(shí)現(xiàn)精準(zhǔn)控制每秒API請(qǐng)求問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-08-08
  • Java雙向鏈表倒置功能實(shí)現(xiàn)過(guò)程解析

    Java雙向鏈表倒置功能實(shí)現(xiàn)過(guò)程解析

    這篇文章主要介紹了Java雙向鏈表倒置功能實(shí)現(xiàn)過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-09-09
  • JavaWeb文件上傳入門教程

    JavaWeb文件上傳入門教程

    這篇文章主要為大家詳細(xì)介紹了JavaWeb文件上傳入門教程,分析了文件上傳原理、介紹了第三方上傳組件,感興趣的小伙伴們可以參考一下
    2016-06-06
  • Java圖形界面GUI布局方式(小結(jié))

    Java圖形界面GUI布局方式(小結(jié))

    這篇文章主要介紹了Java圖形界面GUI布局方式(小結(jié)),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2021-04-04
  • MyEclipse2017創(chuàng)建Spring項(xiàng)目的方法

    MyEclipse2017創(chuàng)建Spring項(xiàng)目的方法

    這篇文章主要為大家詳細(xì)介紹了MyEclipse2017創(chuàng)建Spring項(xiàng)目的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-03-03
  • JDK8通過(guò)Stream 對(duì)List,Map操作和互轉(zhuǎn)的實(shí)現(xiàn)

    JDK8通過(guò)Stream 對(duì)List,Map操作和互轉(zhuǎn)的實(shí)現(xiàn)

    這篇文章主要介紹了JDK8通過(guò)Stream 對(duì)List,Map操作和互轉(zhuǎn)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-09-09
  • 深入淺出理解Java泛型的使用

    深入淺出理解Java泛型的使用

    這篇文章主要介紹了深入淺出理解Java泛型的使用,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-09-09
  • Java 8 lambda初試示例詳解

    Java 8 lambda初試示例詳解

    這篇文章主要介紹了Java 8 lambda初試示例詳解,需要的朋友可以參考下
    2017-04-04

最新評(píng)論