kotlin
复制代码
import org.apache.commons.codec.binary.Base32
import org.apache.commons.codec.binary.Hex
import org.springframework.stereotype.Component
import java.io.UnsupportedEncodingException
import java.net.URLEncoder
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import kotlin.experimental.and
@Component
class GoogleAuthenticatorUtil {
/**
* 时间前后偏移量
* 用于防止客户端时间不精确导致生成的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 var WINDOW_SIZE = 0
/**
* 加密方式,HmacSHA1、HmacSHA256、HmacSHA512
*/
private var CRYPTO = "HmacSHA1"
/**
* 生成密钥,每个用户独享一份密钥
*
* @return
*/
fun getSecretKey(): String {
val random = SecureRandom()
val bytes = ByteArray(20)
random.nextBytes(bytes)
val base32 = Base32()
val 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
*/
fun getQrCodeText(secretKey: String, account: String, issuer: String): String {
val normalizedBase32Key = secretKey.replace(" ", "").toUpperCase()
return try {
"otpauth://totp/" + URLEncoder.encode((if (issuer.isNotEmpty()) "$issuer:" else "") + account, "UTF-8")
.replace("+", "%20") +
"?secret=" + URLEncoder.encode(normalizedBase32Key, "UTF-8").replace("+", "%20") +
if (issuer.isNotEmpty()) "&issuer=" + URLEncoder.encode(issuer, "UTF-8").replace("+", "%20") else ""
} catch (e: UnsupportedEncodingException) {
throw IllegalStateException(e)
}
}
/**
* 获取验证码
*
* @param secretKey
* @return
*/
fun getCode(secretKey: String): String {
val normalizedBase32Key = secretKey.replace(" ", "").toUpperCase()
val base32 = Base32()
val bytes = base32.decode(normalizedBase32Key)
val hexKey = Hex.encodeHexString(bytes)
val time = System.currentTimeMillis() / 1000 / 30
val hexTime = java.lang.Long.toHexString(time)
return generateTOTP(hexKey, hexTime, "6", CRYPTO)
}
/**
* 检验 code 是否正确
*
* @param secret 密钥
* @param code code
* @param time 时间戳
* @return
*/
fun checkCode(secret: String, code: Long, time: Long): Boolean {
val codec = Base32()
val decodedKey = codec.decode(secret)
// convert unix msec time into a 30 second "window"
// this is per the TOTP spec (see the RFC for details)
var 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.
var hash: Long
for (i in -WINDOW_SIZE..WINDOW_SIZE) {
try {
hash = verifyCode(decodedKey, t + i)
} catch (e: Exception) {
// Yes, this is bad form - but
// the exceptions thrown would be rare and a static
// configuration problem
// e.printStackTrace();
throw RuntimeException(e.message)
}
if (hash == code) {
return true
}
}
return false
}
/**
* 根据时间偏移量计算
*
* @param key
* @param t
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
*/
private fun verifyCode(key: ByteArray, t: Long): Long {
val data = ByteArray(8)
var value = t
for (i in 7 downTo 0) {
data[i] = value.toByte()
value = value shr 8
}
val signKey = SecretKeySpec(key, CRYPTO)
val mac = Mac.getInstance(CRYPTO)
mac.init(signKey)
val hash = mac.doFinal(data)
var offset = hash[20 - 1] and 0xF.toByte()
// We're using a long because Java hasn't got unsigned int.
var truncatedHash = 0L
for (i in 0..3) {
truncatedHash = truncatedHash shl 8
// We are dealing with signed bytes:
// we just keep the first byte.
truncatedHash = truncatedHash or (hash[offset + i].toInt() and 0xFF).toLong()
}
truncatedHash = truncatedHash and 0x7FFFFFFF
truncatedHash %= 1000000
return truncatedHash
}
private fun generateTOTP(
key: String,
time: String?,
returnDigits: String,
crypto: String
): String {
var time = time
var code = ""
val tm = time!!.toLong(16)
val hash = hmacSha(crypto, hexStr2Bytes(key), longToByteArray(tm))
var offset = hash[hash.size - 1] and 0xf
val binary =
((hash[offset.toInt()].toInt() and 0x7f) shl 24 or (hash[offset + 1].toInt() and 0xff) shl 16 or (hash[offset + 2].toInt() and 0xff) shl 8 or (hash[offset + 3].toInt() and 0xff))
.toLong()
val otp = binary % DIGITS_POWER[returnDigits.toInt()]
code = otp.toString()
while (code.length < returnDigits.toInt()) {
code = "0$code"
}
return code
}
private fun hmacSha(crypto: String, keyBytes: ByteArray, text: ByteArray?): ByteArray {
var mac: Mac? = null
try {
mac = Mac.getInstance(crypto)
val secretKeySpec = SecretKeySpec(keyBytes, "RAW")
mac!!.init(secretKeySpec)
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
} catch (e: InvalidKeyException) {
e.printStackTrace()
}
return mac!!.doFinal(text)
}
private fun hexStr2Bytes(hex: String): ByteArray {
// Adding one byte to get the right conversion
// Values starting with "0" can be converted
val bArray = ByteArray(hex.length / 2 + 1)
var j = 0
for (i in 0 until hex.length) {
var c = hex[i]
if (c.isWhitespace()) {
continue
}
// Gather high nibble
val nibble: Int = when (c) {
in '0'..'9' -> {
c - '0'
}
in 'a'..'f' -> {
c - 'a' + 10
}
in 'A'..'F' -> {
c - 'A' + 10
}
else -> {
throw IllegalArgumentException("Invalid hex character: $c")
}
}
// Shift high nibble four bits to the left
val shiftedNibble = nibble shl 4
// Get next char from string
val nextChar = hex[i + 1]
// Gather low nibble
val nextNibble: Int = when (nextChar) {
in '0'..'9' -> {
nextChar - '0'
}
in 'a'..'f' -> {
nextChar - 'a' + 10
}
in 'A'..'F' -> {
nextChar - 'A' + 10
}
else -> {
throw IllegalArgumentException("Invalid hex character: $nextChar")
}
}
// Combine high nibble with low nibble
val b = shiftedNibble or nextNibble
// Store byte in array
bArray[j++] = b.toByte()
}
// Remove trailing zeros
return if (bArray.size == j) {
bArray
} else {
bArray.copyOf(j)
}
}
private fun longToByteArray(value: Long): ByteArray {
val buffer = ByteArray(8)
for (i in 7 downTo 0) {
buffer[i] = (value shr 8 * (7 - i)).toByte()
}
return buffer
}
private val DIGITS_POWER = intArrayOf(
1,
10,
100,
1000,
10000,
100000,
1000000,
10000000,
100000000
)
}
kotlin
复制代码
/**
3. 绑定谷歌验证
*/
@RestfulPack
@SaCheckLogin
@PostMapping("bingGoogleAuth")
fun bingGoogleAuth(@RequestBody bindGoogleAuthCodeDto: BindGoogleAuthCodeDto): Boolean {
if (StringUtils.isNotBlank(user.googleAuth)) {
throw ProgramException(i18nUtils.getMessage("UNALLOWED_OPERATIONS")!!)
}
return if (googleAuthenticator.checkCode(
bindGoogleAuthCodeDto.secretKey,
bindGoogleAuthCodeDto.code,
System.currentTimeMillis()
)
) {
val encryptedMessage = TripleDesUtils.encrypt(bindGoogleAuthCodeDto.secretKey, user.salt)
user.googleAuth = encryptedMessage
user.bindGoogleAuth=true
true
} else {
throw ProgramException(i18nUtils.getMessage("INVALID_GOOGLE_VERIFICATION_CODE")!!)
}
}
/**
* 验证 code 是否正确
*/
@RestfulPack
@GetMapping("checkCode")
fun checkCode(@RequestParam("code") code: Long): Boolean {
val user = UserUtil.get()
if (StringUtils.isBlank(user.googleAuth)){
throw ProgramException(i18nUtils.getMessage("UNALLOWED_OPERATIONS")!!)
}
return googleAuthenticator.checkCode(TripleDesUtils.decrypt(user.googleAuth!!,user.salt), code, System.currentTimeMillis());
}
/**
* 验证 code 是否正确
*/
@RestfulPack
@PostMapping("removeGoogleAuth")
fun removeGoogleAuth(@RequestBody bindGoogleAuthCodeDto: BindGoogleAuthCodeDto): Boolean {
val user = UserUtil.get()
if (StringUtils.isBlank(user.googleAuth)){
throw ProgramException(i18nUtils.getMessage("UNALLOWED_OPERATIONS")!!)
}
return if (googleAuthenticator.checkCode(TripleDesUtils.decrypt(user.googleAuth!!,user.salt), bindGoogleAuthCodeDto.code, System.currentTimeMillis())==true){
user.googleAuth = null
user.bindGoogleAuth=false
true
}else{
throw ProgramException(i18nUtils.getMessage("INVALID_GOOGLE_VERIFICATION_CODE")!!)
}
}