SpringBoot中使用谷歌身份验证器(Google Authenticator)实现二步身份验证纯干货一文看懂

二步身份验证(2FA)是提高应用安全性的重要方法之一。本教程将教你如何在Spring Boot项目中集成Google Authenticator来实现二步身份验证。

  1. 添加pom依赖
 <!--google两步认证相关-->
  <dependency>
      <groupId>de.taimos</groupId>
      <artifactId>totp</artifactId>
      <version>1.0</version>
  </dependency>
  <dependency>
      <groupId>com.google.zxing</groupId>
      <artifactId>javase</artifactId>
      <version>3.2.1</version>
  </dependency>
  1. 创建 Google Authenticator 工具类
package xyz.infomon.utils;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import de.taimos.totp.TOTP;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Hex;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URLEncoder;
import java.security.SecureRandom;
import java.util.Base64;


public class GoogleAuthenticationTool {

    public static String generateSecretKey() {
        SecureRandom random = new SecureRandom();
        byte[] bytes = new byte[20];
        random.nextBytes(bytes);
        Base32 base32 = new Base32();
        return base32.encodeToString(bytes);
    }

    /**
     * 根据32位随机码获得正确的6位数字
     *
     * @param secretKey
     * @return
     */
    public static String getTOTPCode(String secretKey) {
        Base32 base32 = new Base32();
        byte[] bytes = base32.decode(secretKey);
        String hexKey = Hex.encodeHexString(bytes);
        return TOTP.getOTP(hexKey);
    }


    /**
     * 生成绑定二维码(字符串)
     *
     * @param account   账户信息(展示在Google Authenticator App中的)
     * @param secretKey 密钥
     * @param title     标题 (展示在Google Authenticator App中的)
     * @return
     */
    public static String spawnScanQRString(String account, String secretKey, String title) {
        try {
            return "otpauth://totp/"
                    + URLEncoder.encode(title + ":" + account, "UTF-8").replace("+", "%20")
                    + "?secret=" + URLEncoder.encode(secretKey, "UTF-8").replace("+", "%20")
                    + "&issuer=" + URLEncoder.encode(title, "UTF-8").replace("+", "%20");
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * 生成二维码(文件)【返回图片的base64,若指定输出路径则同步输出到文件中】
     *
     * @param barCodeData 二维码字符串信息
     * @param outPath     输出地址
     * @param height
     * @param width
     * @throws WriterException
     * @throws IOException
     */
    public static String createQRCode(String barCodeData, String outPath, int height, int width)
            throws WriterException, IOException {
        BitMatrix matrix = new MultiFormatWriter().encode(barCodeData, BarcodeFormat.QR_CODE,
                width, height);
        BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(matrix);

        ByteArrayOutputStream bof = new ByteArrayOutputStream();
        ImageIO.write(bufferedImage, "png", bof);
        String base64 = imageToBase64(bof.toByteArray());
        if(outPath!=null&&!outPath.equals("")) {
            try (FileOutputStream out = new FileOutputStream(outPath)) {
                MatrixToImageWriter.writeToStream(matrix, "png", out);
            }
        }
        return base64;
    }

    /**
     * 将图片文件转换成base64字符串,参数为该图片的路径
     *
     * @param dataBytes 图片的字节数组
     * @return java.lang.String Base64编码后的字符串
     */
    private static String imageToBase64(byte[] dataBytes) {
        if (dataBytes != null) {
            // 使用 Base64.Encoder 对字节数组进行编码
            String encodedString = Base64.getEncoder().encodeToString(dataBytes);
            return "data:image/jpeg;base64," + encodedString; // 返回Base64编码过的字节数组字符串
        }
        return null;
    }

}

3.编写业务逻辑

/**
     * 获得两步验证二维码
     */
    @Operation(summary = "获得两步验证二维码", description = "生成谷歌两步验证的二维码,供用户扫描")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "成功生成二维码",
                    content = @Content(mediaType = "application/json",
                            schema = @Schema(implementation = CheersResponse.class))),
            @ApiResponse(responseCode = "500", description = "服务器内部错误")
    })
    @GetMapping("/getTwoStepVerificationQRCode")
    public CheersResponse toBindingGoogleTwoFactorValidate(HttpServletRequest request) {
        long twitterUid = getTwitterUid(request);
        String randomSecretKey = GoogleAuthenticationTool.generateSecretKey();
        //此步设置的参数就是App扫码后展示出来的参数
        String qrCodeString = GoogleAuthenticationTool.spawnScanQRString(String.valueOf(twitterUid), randomSecretKey, "应用信息");
        String qrCodeImageBase64 = null;
        try {
            qrCodeImageBase64 = GoogleAuthenticationTool.createQRCode(qrCodeString, null, 512, 512);
        } catch (WriterException | IOException e) {
            e.printStackTrace();
        }
        redisUtils.set(INFOMON_TWOFACTOR_RANDOMSECRETKEY + twitterUid, randomSecretKey);
        return CheersResponse.success(qrCodeImageBase64);
    }

    /**
     * 执行谷歌两步验证绑定
     *
     * @return
     */
    @Operation(summary = "执行谷歌两步验证绑定", description = "验证用户输入的谷歌两步验证码")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "绑定成功",
                    content = @Content(mediaType = "application/json",
                            schema = @Schema(implementation = CheersResponse.class))),
            @ApiResponse(responseCode = "400", description = "验证码无效"),
            @ApiResponse(responseCode = "500", description = "服务器内部错误")
    })
    @PostMapping("/bindingTwoFactorValidate")
    public CheersResponse bindingTwoFactorValidate(HttpServletRequest request, @RequestParam("inputGoogleCode") String inputGoogleCode) {
        long twitterUid = getTwitterUid(request);
        UserDomain userInfo = userService.getUserInfo(twitterUid);
        if (userInfo.isTwoFactorValidate()) {
            return CheersResponse.error(NON_REPEATABLE_BINDING);
        }
        String key = INFOMON_TWOFACTOR_RANDOMSECRETKEY + twitterUid;
        String randomSecretKey = String.valueOf(redisUtils.get(key));
        String rightCode = GoogleAuthenticationTool.getTOTPCode(randomSecretKey);
        if (!rightCode.equals(inputGoogleCode)) {
            return CheersResponse.error(THE_VERIFICATION_CODE_IS_INVALID);
        }
        UpdateWrapper<UserInfo> updateWrapper = new UpdateWrapper<>();
        updateWrapper.eq("twitterUid", twitterUid);
        updateWrapper.set("twoFactorCode", randomSecretKey);
        boolean k = userService.update(null, updateWrapper);
        if (!k) {
            return CheersResponse.error(INTERNAL_SERVER_ERROR);
        }
        return CheersResponse.success();
    }

    /**
     * 验证谷歌两步验证码的接口
     *
     * @param request         HttpServletRequest对象,用于获取用户信息
     * @param inputGoogleCode 用户输入的谷歌验证码
     * @return CheersResponse 包含验证结果
     */
    @Operation(summary = "验证谷歌两步验证码", description = "验证用户输入的谷歌两步验证码是否正确")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "验证成功",
                    content = @Content(mediaType = "application/json",
                            schema = @Schema(implementation = CheersResponse.class))),
            @ApiResponse(responseCode = "400", description = "验证码无效"),
            @ApiResponse(responseCode = "500", description = "服务器内部错误")
    })
    @PostMapping("/validateGoogleTwoFactor")
    public CheersResponse validateGoogleTwoFactor(HttpServletRequest request, @RequestParam("inputGoogleCode") String inputGoogleCode) {
        long twitterUid = getTwitterUid(request);
        String key = INFOMON_TWOFACTOR_RANDOMSECRETKEY + twitterUid;
        String randomSecretKey = String.valueOf(redisUtils.get(key));
        String rightCode = GoogleAuthenticationTool.getTOTPCode(randomSecretKey);

        // 验证用户输入的谷歌验证码是否正确
        if (!rightCode.equals(inputGoogleCode)) {
            return CheersResponse.error(THE_VERIFICATION_CODE_IS_INVALID);
        }
        return CheersResponse.success();
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值