在Java中使用Google Authenticator
Google Authenticator(谷歌身份验证器)
现在身份的验证主要是靠 用户名 和 密码。一旦泄露,则任何人都可以登录。谷歌身份验证器,通过一种方式解决了这个问题。动态口令
动态口令不是什么稀奇复杂的东西。在生活中太多地方已经出现过。例如:将军令
它的原理
服务端先为用户生成一个密钥,把这个密钥通过某种方式(扫码,用户手动输入)录入到客户端。
这样一来客户端和服务端都存储着用户相同的一个密钥。
客户端和服务端通过相同的算法,加上相同的密钥,加上相同的的时间戳,每隔30秒可以计算出一个相同的,新的动态密码(6 - 8位长度)。
登录的时候,可以不使用固定的密码。而采用这种动态密码。
这种动态密码不具备连续性,且30秒就失效。密钥存储在客户端,整个计算过程不需要网络传输(客户端离线状态也可以使用)。安全度相对较高(密钥不会泄漏)。
客户端
谷歌已经提供了现成的客户端,也可以考虑在自己的APP中实现客户端。
-
安卓
https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2 -
IOS
https://apps.apple.com/cn/app/google-authenticator/id388497605
Github
Java代码的实现
TOTP
它是核心的TOTP算法实现
Time-Based One-Time Password Algorithm (主要使用TOTP, 因为时间同步并不是太难的事)
import java.lang.reflect.UndeclaredThrowableException;
import java.security.GeneralSecurityException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
import java.util.TimeZone;
/**
* This is an example implementation of the OATH
* TOTP algorithm.
* Visit www.openauthentication.org for more information.
*
* @author Johan Rydell, PortWise, Inc.
*/
public class TOTP {
private TOTP() {}
/**
* 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];
for (int i = 0; i < ret.length; i++)
ret[i] = bArray[i+1];
return ret;
}
private static final int[] DIGITS_POWER
// 0 1 2 3 4 5 6 7 8
= {1,10,100,1000,10000,100000,1000000,10000000,100000000 };
/**
* 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
*
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP(String key,
String time,
String returnDigits){
return generateTOTP(key, time, returnDigits, "HmacSHA1");
}
/**
* 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
*
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP256(String key,
String time,
String returnDigits){
return generateTOTP(key, time, returnDigits, "HmacSHA256");
}
/**
* 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
*
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP512(String key,
String time,
String returnDigits){
return generateTOTP(key, time, returnDigits, "HmacSHA512");
}
/**
* 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
* {@link truncationDigits} digits
*/
public static String generateTOTP(String key,
String time,
String returnDigits,
String crypto){
int codeDigits = Integer.decode(returnDigits).intValue();
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;
}
public static void main(String[] args) {
// Seed for HMAC-SHA1 - 20 bytes
String seed = "3132333435363738393031323334353637383930";
// Seed for HMAC-SHA256 - 32 bytes
String seed32 = "3132333435363738393031323334353637383930" +
"313233343536373839303132";
// Seed for HMAC-SHA512 - 64 bytes
String seed64 = "3132333435363738393031323334353637383930" +
"3132333435363738393031323334353637383930" +
"3132333435363738393031323334353637383930" +
"31323334";
long T0 = 0;
long X = 30;
long testTime[] = {59L, 1111111109L, 1111111111L,
1234567890L, 2000000000L, 20000000000L};
String steps = "0";
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
df.setTimeZone(TimeZone.getTimeZone("UTC"));
try {
System.out.println(
"+---------------+-----------------------+" +
"------------------+--------+--------+");
System.out.println(
"| Time(sec) | Time (UTC format) " +
"| Value of T(Hex) | TOTP | Mode |");
System.out.println(
"+---------------+-----------------------+" +
"------------------+--------+--------+");
for (int i=0; i<testTime.length; i++) {
long T = (testTime[i] - T0)/X;
steps = Long.toHexString(T).toUpperCase();
while (steps.length() < 16) steps = "0" + steps;
String fmtTime = String.format("%1$-11s", testTime[i]);
String utcTime = df.format(new Date(testTime[i]*1000));
System.out.print("| " + fmtTime + " | " + utcTime +
" | " + steps + " |");
System.out.println(generateTOTP(seed, steps, "8",
"HmacSHA1") + "| SHA1 |");
System.out.print("| " + fmtTime + " | " + utcTime +
" | " + steps + " |");
System.out.println(generateTOTP(seed32, steps, "8",
"HmacSHA256") + "| SHA256 |");
System.out.print("| " + fmtTime + " | " + utcTime +
" | " + steps + " |");
System.out.println(generateTOTP(seed64, steps, "8",
"HmacSHA512") + "| SHA512 |");
System.out.println(
"+---------------+-----------------------+" +
"------------------+--------+--------+");
}
}catch (final Exception e){
System.out.println("Error : " + e);
}
}
}
GoogleAuthenticatorUtils
抽象出的工具类
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.SecureRandom;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Hex;
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;
public class GoogleAuthenticatorUtils {
/**
* 生成随机的密钥
* @return
*/
public static String getRandomSecretKey() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[20];
random.nextBytes(bytes);
Base32 base32 = new Base32();
return base32.encodeToString(bytes).toLowerCase();
}
/**
* 根据密钥,计算出当前时间的动态口令 (30s会变化一次)
* @param secretKey
* @return
*/
public static String getTOTPCode(String secretKey) {
Base32 base32 = new Base32();
byte[] bytes = base32.decode(secretKey);
String hexKey = Hex.encodeHexString(bytes);
long time = (System.currentTimeMillis() / 1000) / 30;
String hexTime = Long.toHexString(time);
return TOTP.generateTOTP(hexKey, hexTime, "6");
}
/**
* 根据密钥,生成 TOPT 密钥的 URI 字符串
* @param secretKey
* @param account
* @param issuer
* @return
*/
public static String getGoogleAuthenticatorBarCode(String secretKey, String account, String issuer) {
try {
return "otpauth://totp/"
+ URLEncoder.encode(issuer + ":" + account, "UTF-8").replace("+", "%20")
+ "?secret=" + URLEncoder.encode(secretKey, "UTF-8").replace("+", "%20")
+ "&issuer=" + URLEncoder.encode(issuer, "UTF-8").replace("+", "%20");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
/**
* 根据 TOPT 密钥的 URI 字符串 生成二维码
* @param barCode
* @param outputStream
* @param height
* @param width
* @throws WriterException
* @throws IOException
*/
public static void createQRCode(String barCode, OutputStream outputStream, int height, int width) throws WriterException, IOException {
BitMatrix matrix = new MultiFormatWriter().encode(barCode, BarcodeFormat.QR_CODE, width, height);
MatrixToImageWriter.writeToStream(matrix, "png", outputStream);
}
}
GoogleAuthenticatorTest
演示
import java.io.FileOutputStream;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import com.google.zxing.WriterException;
public class GoogleAuthenticatorTest {
public static void main(String[] args) throws WriterException, IOException {
// 生成随机的密钥
String secretKey = GoogleAuthenticatorUtils.getRandomSecretKey();
System.out.println("随机密钥:" + secretKey);
// 根据验证码,账户,服务商生成 TOPT 密钥的 URI
String uri = GoogleAuthenticatorUtils.getGoogleAuthenticatorBarCode(secretKey, "747692844@qq.com", "springboot");
System.out.println("TOPT密钥URI:" + uri);
// 根据 TOPT 密钥的 URI生成二维码,存储在本地
FileOutputStream fileOutputStream = new FileOutputStream("D:\\google-auth.png");
GoogleAuthenticatorUtils.createQRCode(uri, fileOutputStream, 200, 200);
fileOutputStream.close();
String lastCode = null;
while (true) {
// 根据密钥获取此刻的动态口令
String code = GoogleAuthenticatorUtils.getTOTPCode(secretKey);
if (!code.equals(lastCode)) {
System.out.println("刷新了验证码:" + code + " " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
}
lastCode = code;
try {
Thread.sleep(1000); // 线程暂停1秒
} catch (InterruptedException e) {};
}
}
}
生成的二维码
使用Google Authenticator客户端扫描
控制台输出的日志
每隔30秒就会生成一个新的口令
随机密钥:royydsit3u36dsilk33vc4rtso4pfgme
TOPT密钥URI:otpauth://totp/springboot%3A747692844%40qq.com?secret=royydsit3u36dsilk33vc4rtso4pfgme&issuer=springboot
刷新了验证码:365102 2020-02-12 11:03:52
刷新了验证码:601796 2020-02-12 11:04:00
刷新了验证码:491242 2020-02-12 11:04:30
客户端扫码后得到的动态口令
可以看到,同时间,客户端和日志中的最新的口令是匹配的。都是:491242
关于TOPT密钥URI的格式
otpauth://totp/{issuer}:{account}?secret={secret}&issuer={issuer}
issuer
提供服务的组织/企业(前后要保持一致)
account
账户
secret
密钥
例如,我现在使用 springboot
提供的认证服务,我的的账户是:747692844@qq.com
,随机的安全密钥是:vwdjsowyebnidjjiw5uz3ygwkqjyiha3
otpauth://totp/springboot:747692844@qq.com?secret=vwdjsowyebnidjjiw5uz3ygwkqjyiha3&issuer=springboot