SpringBoot项目添加2FA双因素身份认证

这篇具有很好参考价值的文章主要介绍了SpringBoot项目添加2FA双因素身份认证。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

什么是 2FA(双因素身份验证)?

双因素身份验证(2FA)是一种安全系统,要求用户提供两种不同的身份验证方式才能访问某个系统或服务。国内普遍做短信验证码这种的用的比较少,不过在国外的网站中使用双因素身份验证的还是很多的。用户通过使用验证器扫描二维码,就能在app上获取登录的动态口令,进一步加强了账户的安全性。

主要步骤

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字段,用以生成二维码及后期校验
SpringBoot项目添加2FA双因素身份认证

工具类

谷歌身份验证器工具类


/**
 * 谷歌身份验证器工具类
 */
public class GoogleAuthenticator {

    /**
     * 时间前后偏移量
     * 用于防止客户端时间不精确导致生成的TOTP与服务器端的TOTP一直不一致
     * 如果为0,当前时间为 10:10:15
     * 则表明在 10:10:00-10:10:30 之间生成的TOTP 能校验通过
     * 如果为1,则表明在
     * 10:09:30-10:10:00
     * 10:10:00-10:10:30
     * 10:10:30-10:11:00 之间生成的TOTP 能校验通过
     * 以此类推
     */
    private static int WINDOW_SIZE = 0;

    /**
     * 加密方式,HmacSHA1、HmacSHA256、HmacSHA512
     */
    private static final String CRYPTO = "HmacSHA1";

    /**
     * 生成密钥,每个用户独享一份密钥
     *
     * @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();
    }

    /**
     * 生成二维码内容
     *
     * @param secretKey 密钥
     * @param account   账户名
     * @param issuer    网站地址(可不写)
     * @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);
        }
    }

    /**
     * 获取验证码
     *
     * @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);
    }

    /**
     * 检验 code 是否正确
     *
     * @param secret 密钥
     * @param code   code
     * @param time   时间戳
     * @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;
    }

    /**
     * 根据时间偏移量计算
     *
     * @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);
        }
    }
}

二维码工具类

/**
 * 验证码生成工具类
 */
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);
    }

   /**
    * 校验动态码
    */
    public boolean chek2FACode(User user, String twoFACode) throws Exception {
        String secretKey = user.getSecretKey();
        // 没绑定设备就先验证通过
        if(secretKey == null || secretKey.isEmpty()) {
            return true;
        } else  {
           if(twoFACode.isEmpty()) { throw new Exception("已绑定设备,请输入动态码"); }
           boolean checkRes = GoogleAuthenticator.checkCode(secretKey, Long.parseLong(twoFACode), System.currentTimeMillis());
           if(!checkRes) {
               throw new Exception("动态码错误");
           } else {
               return true;
           }
        }
    }
}

Controller

用户登录中增加两步验证:

@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验证
            User user = (User) subject.getPrincipal();
            twoFAService.chek2FACode(user, twoFACode);

            Log log = new Log();
            .......
        }
    }
}

两步验证的Controler文章来源地址https://www.toymoban.com/news/detail-857589.html

@RestController
@RequestMapping(value = "/2fa")
public class TwoFAController {
    @Autowired
    private TwoFAService twoFAService;

    /**
     * 生成二维码信息对象
     */
    @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);
            }

            // 生成二维码内容
            String qrCodeText = GoogleAuthenticator.getQrCodeText(secretKey, userCode, "suggest-mgr");
            // 以流的形式返回生成二维码输出
            // new SimpleQrcodeGenerator().generate(qrCodeText).toStream(response.getOutputStream());
            BufferedImage image = new SimpleQrcodeGenerator().generate(qrCodeText).getImage();
            // 将图片转换为Base64字符串
            String base64Image = convertImageToBase64(image);
            qrCodeResponse.setQrCodeText(secretKey);
            qrCodeResponse.setBase64Image(base64Image);

            return qrCodeResponse;
        } catch (Exception e) {
            // 处理异常
            e.printStackTrace();
            return null; // 或者返回适当的错误信息
        }
    }

    /**
     * 更新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);

            // 生成二维码内容
            String qrCodeText = GoogleAuthenticator.getQrCodeText(secretKey, userCode, "xxx-site");
            BufferedImage image = new SimpleQrcodeGenerator().generate(qrCodeText).getImage();
            // 将图片转换为Base64字符串
            String base64Image = convertImageToBase64(image);
            qrCodeResponse.setQrCodeText(secretKey);
            qrCodeResponse.setBase64Image(base64Image);

            // 返回包含qrCodeText和Base64编码图片的信息
            return qrCodeResponse;
        } catch (Exception e) {
            // 处理异常
            e.printStackTrace();
            return null; // 或者返回适当的错误信息
        }
    }

    /**
     * 将图片文件流转为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验证工具

  1. Google Authenticator: google play, apple store
  2. Microsoft Authenticator: google play , apple store
  3. AuthenticatorPro(开源):https://github.com/jamie-mh/AuthenticatorPro

到了这里,关于SpringBoot项目添加2FA双因素身份认证的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 3-2. SpringBoot项目集成【用户身份认证】实战 【实战核心篇】基于JWT生成和校验Token

    书接上文 技术选型篇,我们做了【用户身份认证】的技术选型说明,对基于 Session、Token、JWT 的方案进行了详细的对比分析,详细说明了它们都是什么和各自的优缺点!这些是实战的基础,还没看过的同学,建议先看上文。最终采用的是目前流行的 基于JWT的Token用户身份认证

    2023年04月08日
    浏览(38)
  • 3-1. SpringBoot项目集成【用户身份认证】实战 【技术选型篇】基于Session、Token、JWT怎么选?

    通过第二章2-2. SpringBoot API开发详解 --SpringMVC注解+封装结果+支持跨域+打包,我们实现了基于SpringBoot项目的 API接口开发 ,并实现 API结果统一封装、支持跨域请求 等等功能,接下来开始第三章,主要做用户身份认证,主要实现一套 统一鉴权的用户身份认证的机制 。 我已经提

    2024年01月22日
    浏览(41)
  • 3-3. SpringBoot项目集成【用户身份认证】实战 【全流程篇】基于JWT+双重检查的登录+登出+拦截器

    书接上文 实战核心篇,我们已经 把JWT的核心代码实现了! 文中不止是代码实现,更是使用到了设计原则,提升大家的内功心法。并且抛转引玉的实现了RSA和HMAC两种算法,还没看过的同学,建议先看上文。所以对于 基于JWT的Token用户身份认证机制 来说,剩下的就是与接口结

    2023年04月16日
    浏览(37)
  • SpringBoot 基于 OAuth2 统一身份认证流程详解

    了解OAUTH2统一认证基本概念 了解OAUTH2协议流程 了解OAUTH2各种模式类型 了解Spring Security OAuth设计 2. 分析 传统登陆认证介绍 单点登陆认证介绍 OAuth2简介 OAuth2角色 OAuth2协议流程介绍 OAuth2授权类型 OAuth2授权码模式流程 OAuth2简化模式 OAuth2密码模式 OAuth2客户端模式 Spring Security

    2024年02月15日
    浏览(44)
  • GitHub 开启 2FA 双重身份验证的方法

    自2023年3月13日起,我们登录 GitHub 都会看到一个要求 Enable 2FA 的重要提示,具体如下: GitHub users are now required to enable two-factor authentication as an additional security measure. Your activity on GitHub includes you in this requirement. You will need to enable two-factor authentication on your account before May 04, 2023,

    2024年02月15日
    浏览(45)
  • 部署开源项目 Casdoor 身份认证管理系统到本地

    Casdoor是一个基于OAuth 2.0、OIDC、SAML 和 CAS 的,UI-first的身份和访问管理(IAM)/单点登录(SSO)平台。使用 Go 和react开发,前后端分离,内置第三方应用登录服务。 Casdoor 有四个核心概念,分别是 组织(Organization) , 用户(User) , 应用(Application) 和 提供商(Provider) 。组织承载用户和应用

    2024年02月16日
    浏览(42)
  • 学习node之——如何在项目中使用MySQL、前后端的身份认证

    上一篇文章只写了一丢丢,这篇才是正片,look look look 这里连接数据库的用户和密码都是我们在安装mysql时配置的密码。每个人的users表格里面数据不同,结果也会不一样哟! 巧2小黑板啦!这里有两个知识点: 1、可以通过英文?符号占位符指定具体的值 2、用insert into插入数

    2024年02月10日
    浏览(32)
  • 在springBoot中使用JWT实现1.生成token,2.接收前端token进行身份认证,3.通过token获取对象信息

    第一步:引入依赖 第二步:创建工具类 在until包下创建TokenUntil类,用于生成token 利用id,和password作为参数生成token JWt为这个包下的对象 第三步:token使用 在向前端返回的数据对象中添加token属性  是serve层中调用工具类方法将生成的token放到返回的数据中 注意:这里获取到

    2024年02月04日
    浏览(46)
  • GitHub如何验证2FA,烦人的认证,看完几分钟解锁

    今天需要使用GitHub,还是不能用,需要2FA认证,没办法,还是让2FA认证流程来,一一解决,在解决这认证问题之前,先说说2FA认证是什么? 2FA 是指两步验证(Two-Factor Authentication)的缩写。它是一种用于增强账户安全性的身份验证方法。传统的身份验证通常只需要输入用户名

    2024年04月23日
    浏览(34)
  • 统一身份认证,构建数字时代的安全壁垒——统一身份认证介绍、原理和实现方法

    随着数字化时代的来临,个人和机构在互联网上的活动越来越频繁,对于身份认证的需求也愈发迫切。为了有效应对身份欺诈、数据泄露等问题,统一身份认证(Unified Identity Authentication)应运而生。 在本文博主将介绍统一身份认证的概念、原理以及其具体的实现方案。 统一

    2024年02月03日
    浏览(33)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包