生成 Google Authenticator 二维码所需信息, 以及验证方法
public class GeogleLoginAuthUtil {
private static final String format = "png";// 默认二维码文件格式
private static final Map<EncodeHintType, Object> hints = new HashMap();// 二维码参数
/** 时间前后偏移量 */
private static final int timeExcursion = 3;
static {
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");// 字符编码
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);// 容错等级 L、M、Q、H 其中 L 为最低, H 为最高
hints.put(EncodeHintType.MARGIN, 2);// 二维码与图片边距
}
/**
* 随机生成一个密钥
*/
public static String createSecretKey() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[20];
random.nextBytes(bytes);
Base32 base32 = new Base32();
String secretKey = base32.encodeToString(bytes);
return secretKey.toLowerCase();
}
/**
* 根据密钥获取验证码
* 返回字符串是因为验证码有可能以 0 开头
* @param secretKey 密钥
* @param time 第几个 30 秒 System.currentTimeMillis() / 1000 / 30
*/
public static String getTOTP(String secretKey, long time) {
Base32 base32 = new Base32();
byte[] bytes = base32.decode(secretKey.toUpperCase());
String hexKey = Hex.encodeHexString(bytes);
String hexTime = Long.toHexString(time);
return TOTP.generateTOTP(hexKey, hexTime, "6");
}
/**
* 生成 Google Authenticator 二维码所需信息
* Google Authenticator 约定的二维码信息格式 : otpauth://totp/{issuer}:{account}?secret={secret}&issuer={issuer}
* 参数需要 url 编码 + 号需要替换成 %20
* @param secret 密钥 使用 createSecretKey 方法生成
* @param account 用户账户 如: example@domain.com 138XXXXXXXX
* @param issuer 服务名称 如: Google Github 印象笔记
*/
public static String createGoogleAuthQRCodeData(String secret, String account, String issuer) {
String qrCodeData = "otpauth://totp/%s?secret=%s&issuer=%s";
try {
return String.format(qrCodeData, URLEncoder.encode(issuer + ":" + account, "UTF-8").replace("+", "%20"), URLEncoder.encode(secret, "UTF-8")
.replace("+", "%20"), URLEncoder.encode(issuer, "UTF-8").replace("+", "%20"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return "";
}
/**
* 返回一个 BufferedImage 对象
* @param content 二维码内容
* @param width 宽
* @param height 高
*/
public static BufferedImage toBufferedImage(String content, int width, int height) throws WriterException, IOException {
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
return MatrixToImageWriter.toBufferedImage(bitMatrix);
}
/**
* 生成二维码图片文件
* @param content 二维码内容
* @param path 文件保存路径
* @param width 宽
* @param height 高
*/
public static void createQRCode(String content, String path, int width, int height) throws WriterException, IOException {
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
//toPath() 方法由 jdk1.7 及以上提供
MatrixToImageWriter.writeToPath(bitMatrix, format, new File(path).toPath());
}
/**
* 将二维码图片输出到一个流中
* @param content 二维码内容
* @param stream 输出流
* @param width 宽
* @param height 高
*/
public static void writeToStream(String content, OutputStream stream, int width, int height) throws WriterException, IOException {
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
MatrixToImageWriter.writeToStream(bitMatrix, format, stream);
}
// 生成二维码输入流
// public void qrcode(String content, @RequestParam(defaultValue = "300", required = false) int width,@RequestParam(defaultValue = "300", required = false) int height, HttpServletResponse response) {
// ServletOutputStream outputStream = null;
// try {
// outputStream = response.getOutputStream();
// writeToStream(content, outputStream, width, height);
// } catch (Exception e) {
// e.printStackTrace();
// } finally {
// if (outputStream != null) {
// try {
// outputStream.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
// }
// }
/**
* <dependency>
* <groupId>net.coobird</groupId>
* <artifactId>thumbnailator</artifactId>
* <version>0.4.8</version>
* </dependency>
* 图片合成
* //Coordinate 存在一个 Coordinate(int x, int y) 构造函数, x 为水印距离底图左边的像素, y 为上边
* BufferedImage bufferedImage = ImageIO.read(new File(logoFilePath));
* Watermark watermark = new Watermark(new Coordinate(100, 100), bufferedImage, 1F);
* 生成带logo二维码输入流
* Thumbnails.of() 可以接收 BufferedImage,InputStream,URL,String,File 的可变 (** 批量处理需要 **) 参数类型
* @param content
* @param logoUrl
* @param width
* @param height
* @param response
*/
// public void qrcode(String content, String logoUrl, @RequestParam(defaultValue = "300") int width, @RequestParam(defaultValue = "300") int height,HttpServletResponse response) {
// ServletOutputStream outputStream = null;
// try {
// outputStream = response.getOutputStream();
// // 根据 QRCodeUtil.toBufferedImage() 返回的 BufferedImage 创建图片构件对象
// Thumbnails.Builder<BufferedImage> builder = Thumbnails.of(toBufferedImage(content, width, height));
// // 将 logo 的尺寸设置为二维码的 30% 大小, 可以自己根据需求调节
// BufferedImage logoImage = Thumbnails.of(new URL(logoUrl)).forceSize((int) (width * 0.3), (int) (height * 0.3)).asBufferedImage();
// // 设置水印位置(居中), 水印图片 BufferedImage, 不透明度(1F 代表不透明)
// builder.watermark(Positions.CENTER, logoImage, 1F).scale(1F);
// // 此处需指定图片格式, 否者报 Output format not specified 错误
// builder.outputFormat("png").toOutputStream(outputStream);
// } catch (Exception e) {
// e.printStackTrace();
// } finally {
// if (outputStream != null) {
// try {
// outputStream.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
// }
// }
/**
* 验证码是使用基于时间的 TOTP 算法, 依赖于客户端与服务端时间的一致性. 如果客户端时间与服务端时间相差过大, 那在用户没有同步时间的情况下,
* 永远与服务端进行匹配. 同时服务端也有可能出现时间偏差的情况, 这样反而导致时间正确的用户校验无法通过
* 为了解决这种情况, 我们可以使用 ** 时间偏移量 ** 来解决该问题
*
* 校验方法
* @param secretKey 密钥
* @param code 用户输入的 TOTP 验证码
*/
public static boolean verify(String secretKey, String code) {
long time = System.currentTimeMillis() / 1000 / 30;
for (int i = -timeExcursion; i <= timeExcursion; i++) {
String totp = getTOTP(secretKey, time + i);
if (code.equals(totp)) {
return true;
}
}
return false;
}
}
TOTP算法:
public class TOTP {
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 include 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 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 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 truncationDigits digits
*/
public static String generateTOTP(String key, String time, String returnDigits, String crypto) {
int codeDigits = Integer.decode(returnDigits);
StringBuilder result;
// Using the counter
// First 8 bytes are for the movingFactor
// Compliant with base RFC 4226 (HOTP)
StringBuilder timeBuilder = new StringBuilder(time);
while (timeBuilder.length() < 16) {timeBuilder.insert(0, "0");}
time = timeBuilder.toString();
// 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 = new StringBuilder(Integer.toString(otp));
while (result.length() < codeDigits) {
result.insert(0, "0");
}
return result.toString();
}
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];
System.arraycopy(bArray, 1, ret, 0, ret.length);
return ret;
}
// public static void main(String[] args) {
// // import org.apache.commons.lang3.RandomStringUtils
// // Seed for HMAC-SHA1 - 20 bytes
// String seed = RandomStringUtils.randomNumeric(40);
// // Seed for HMAC-SHA256 - 32 bytes
// String seed32 = RandomStringUtils.randomNumeric(64);
// // Seed for HMAC-SHA512 - 64 bytes
// String seed64 = RandomStringUtils.randomNumeric(128);
// long T0 = 0;
// long X = 30;
// String returnDigits = "6";
// long[] testTime = {59L, 1011217109L, 1121151171L, 1234567890L, 2008000000L, 20000700000L};
//
// StringBuilder steps;
// DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// df.setTimeZone(TimeZone.getTimeZone("UTC"));
//
// try {
// System.out.println("+------------------seed:" + seed);
// System.out.println("+------------------seed32:" + seed32);
// System.out.println("+------------------seed64:" + seed64);
// System.out.println("+---------------+-----------------------+" + "------------------+--------+--------+");
// System.out.println("| Time(sec) | Time (UTC format) " + "| Value of T(Hex) | TOTP | Mode |");
// System.out.println("+---------------+-----------------------+" + "------------------+--------+--------+");
//
// for (long l : testTime) {
// long T = (l - T0) / X;
// steps = new StringBuilder(Long.toHexString(T).toUpperCase());
// while (steps.length() < 16) {
// steps.insert(0, "0");
// }
// String fmtTime = String.format("%1$-11s", l);
// String utcTime = df.format(new Date(l * 1000));
// System.out.print("| " + fmtTime + " | " + utcTime + " | " + steps + " |");
// System.out.println(generateTOTP(seed, steps.toString(), returnDigits, "HmacSHA1") + "| SHA1 |");
// System.out.print("| " + fmtTime + " | " + utcTime + " | " + steps + " |");
// System.out.println(generateTOTP(seed32, steps.toString(), returnDigits, "HmacSHA256") + "| SHA256 |");
// System.out.print("| " + fmtTime + " | " + utcTime + " | " + steps + " |");
// System.out.println(generateTOTP(seed64, steps.toString(), returnDigits, "HmacSHA512") + "| SHA512 |");
//
// System.out.println("+---------------+-----------------------+" + "------------------+--------+--------+");
// }
// } catch (final Exception e) {
// System.out.println("Error : " + e);
// }
// }
}