谷歌身份验证器 java demo实现 及使用中问题分析

最近公司新进项目需求:谷歌身份验证器来做一个二次因素校验,查了很多网上的例子,也自己写了demo,其中遇到了些问题,记录一下:

1.谷歌身份验证器原理简单说明

2.谷歌身份验证器的使用流程

3.谷歌身份验证器使用需要引入的jar包

4.谷歌身份验证器server端代码

5.谷歌身份验证器test端代码

6.谷歌身份验证器server端校验code要求传long型数据,如果验证码为0023456这种情况的解决

 

1.谷歌身份验证器原理简单说明

引用谷歌身份验证器,可以直接百度就可以下载了,应用商店中不一定会有这个软件,至少我用的华为就没搜索到,直接手机浏览器搜索下载即可。

谷歌身份验证器不需要引入谷歌的API来实现,主要是通过加密算法来实现的身份统一。

手机端需要拿到用户的secret密钥,从而计算得出6位的动态验证码。这个获取密钥的途径可以通过两种场景实现:

谷歌身份验证器 java demo实现 及使用中问题分析_第1张图片

1)服务端产生加密密钥并生成二维码展示给用户,用户直接扫码即可识别密钥并产生动态验证码的服务

2)直接展示给用户密钥串,用户在手机端主动输入

谷歌身份验证器 java demo实现 及使用中问题分析_第2张图片

通过这两种方式都可以将用户的secret记到当前设备上。

具体原理就是:客户端和服务端使用相同的密钥,相同的算法,最终在验证时,因为可能会存在时差的问题,服务端实际上是校验了大约四组数据与当前输入的6位动态验证码,有一组相同即认为是通过了验证。

2.谷歌身份验证器的使用流程

谷歌身份验证器 java demo实现 及使用中问题分析_第3张图片

 

这是我理解的流程;

3.谷歌身份验证器使用需要引入的jar包

        
            commons-codec
            commons-codec
            1.12
        

主要是这个jar包,jdk用的是1.8版本的,低版本的没试过。

4.谷歌身份验证器server端代码

package com.yu.test.controller;

import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

public class GoogleGenerator {

    // 生成的key长度( Generate secret key length)
    public static final int SECRET_SIZE = 10;

    public static final String SEED = "22150146801713967E8g";
    // Java实现随机数算法
    public static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";
    // 最多可偏移的时间
    int window_size = 3; // default 3 - max 17

    public static String generateSecretKey ( ) {
        SecureRandom sr = null;
        try {
            sr = SecureRandom.getInstance ( RANDOM_NUMBER_ALGORITHM );
            sr.setSeed ( Base64.decodeBase64 ( SEED ) );
            byte[] buffer = sr.generateSeed ( SECRET_SIZE );
            Base32 codec = new Base32 ();
            byte[] bEncodedKey = codec.encode ( buffer );
            String encodedKey = new String ( bEncodedKey );
            return encodedKey;
        } catch (NoSuchAlgorithmException e) {
            // should never occur... configuration error
        }
        return null;
    }

    /**
     * 根据user和secret生成二维码的密钥
     *
     * @param user
     * @param host
     * @param secret
     * @return
     */
    public static String getQRBarcodeURL ( String user , String host , String secret ) {
        String format = "http://www.google.com/chart?chs=200x200&chld=M%%7C0&cht=qr&chl=otpauth://totp/%s@%s?secret=%s";
        return String.format ( format , user , host , secret );
    }

    /**
     * 这个format不可以修改,身份验证器无法识别二维码
     *
     * @param user
     * @param secret
     * @return
     */
    public static String getQRBarcode ( String user , String secret ) {
        String format = "otpauth://totp/%s?secret=%s";
        return String.format ( format , user , secret );
    }

    public boolean check_code ( String secret , String code , long timeMsec ) {
        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 = (timeMsec / 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.
        for (int i = -window_size; i <= window_size; ++i) {
            long hash;
            try {
                hash = verify_code ( 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 () );
                // return false;
            }
            System.out.println ( "code=" + code );
            System.out.println ( "hash=" + hash );
            if (code.equals ( addZero ( hash ) )) {
                return true;
            }
/*            if (code==hash ) {
                return true;
            }*/
        }
        // The validation code is invalid.
        return false;
    }

    private static int verify_code ( 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 , "HmacSHA1" );
        Mac mac = Mac.getInstance ( "HmacSHA1" );
        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 (int) truncatedHash;
    }

    /*private String addZero ( long code ) {
        System.out.println ( "addZero code:" + code );
        String codeString = String.valueOf ( code );
        System.out.println ( "addZero codeString" + codeString );
        int codeLength = codeString.length ();
        StringBuffer sb = new StringBuffer ( codeString );
        if (codeLength < 6) {
            for (int i = 0; i < (6 - codeLength); i++) {
                sb.insert ( 0 , "0" );
            }
        }
        return sb.toString ();
    }*/
这部分做了改进

    private String addZero ( long code ) {
        return String.format ( "%06d",code );
    }
}

这部分代码大部分都和网上一样,不过网上的实例都是以long型来传递code值,很可能出现023456,000234等这种情况,这会导致功能的异常,这部分会在第五点详细说明。

server部分主要是3个方法:

1.生成secret的方法

2.生成二维码QRCode的方法

3.校验动态验证码是否正确的verify方法

当然加了一个数据转换的方法

5.谷歌身份验证器test端代码

    private static String secret = "543X2ILASPYIZ2MN";

    @Test
    public void testGenerator(){
        secret = GoogleGenerator.generateSecretKey ();
        String qrCode = GoogleGenerator.getQRBarcode ( "helloz",secret );
        System.out.println ( "qrCode="+qrCode+";key="+secret );
    }

    @Test
    public void testValidCode(){
        String code = "678785";
        long time = System.currentTimeMillis ();
        GoogleGenerator g = new GoogleGenerator ();
        boolean result = g.check_code ( secret,code,time );
        System.out.println ( result );
    }

test使用junit写的测试,第一个是生成secret的方法,同时也输出了二维码页面的字符串,可以找个网页在线生成二维码的生成一个扫描就是了。

再就是验证验证码是否有效的方法了,这部分要记得使用刚刚生成的secret,secret才是校验的灵魂与核心。

6.谷歌身份验证器server端校验code要求传long型数据,如果验证码为0023456这种情况的解决

网上的大多数实例都采用long型传递code参数,在遇到002345这种口令的时候,会有各种问题,这边改造了方法,要求传入参数使用String,这样00的位不会丢失,但是后端校验加密出来的串hash是long型有可能会出现023232这种数据,从而会丢失位数,或校验不准。所以写了个方法用0来补位。试用了一下String.format的方式来补位,还是很好用的。

 

以上大部分来自于网络上共用的东西,在开发中加入了些自己的理解。

有问题可以一起讨论:QQ740273040

你可能感兴趣的:(java后端-技术)