前言
服务器之间要是想确保通信的安全性,需要有一种机制,用于校验双方的身份,符合身份的通过校验,不符合身份的需要进行拦截。可以在拦截器中实现此机制。
Hmac其实是一种消息摘要算法, 以一个密钥以及数据作为输入,输出一个定长消息摘要。
因此这块要实现功能,需要有三个要素:Hmac算法 + 密钥 + 密钥加密之后的数据,密钥是双方约定,保存在本地,第三方不可能知道。加密后的数据,是用密钥计算的一个消息摘要,例如时间戳,还有一些特定的请求头等等。
消息摘要的计算方式
消息摘要的计算主要是通过密钥以及请求url和时间戳计算得出的。其中:
密钥:是双方约定好的一个多位数值。不经过网络传输,也不放在请求中,双方存储在本地。
url:是从协议名称到Http请求第一行中的查询字符串,例如
请求路径 |
url |
POST /some/path.html HTTP/1.1 |
/some/path.html |
HEAD /xyz?a=b HTTP/1.1 |
/xyz |
GET http://foo.bar/a.html HTTP/1.0 |
/a.html |
时间戳:当前传输时间的毫秒值。
计算md5时需要传输进两个参数,分别为key,data,key为上面所说的密钥,data为url和时间戳所拼成的字符串的字节数组。例如:
val urlData = uri + hmacTime
val urlDataByteArray = urlData.toByteArray()
生成密钥的计算方式:
fun encryptHmac(key: ByteArray?, data: ByteArray?): String {
val secretKey = SecretKeySpec(key, encryptMode)
val mac = Mac.getInstance(secretKey.algorithm)
mac.init(secretKey)
val resultBytes = mac.doFinal(data)
return Hex.encodeHexString(resultBytes)
}
Hex.encodeHexString函数用于将字节数组转换为按顺序标识每个字接的十六进制值的字符串。
protected static char[] encodeHex(final byte[] data, final char[] toDigits) {
final int l = data.length;
final char[] out = new char[l << 1];
// two characters form the hex value.
for (int i = 0, j = 0; i < l; i++) {
out[j++] = toDigits[(0xF0 & data[i]) >>> 4];
out[j++] = toDigits[0x0F & data[i]];
}
return out;
}
这块我使用的是apache的hex包,当然可以自己手动转换:
private fun bytesToHexString(byteArray: ByteArray?): String? {
val stringBuilder = StringBuilder()
if (byteArray == null || byteArray.isEmpty()) {
return null
}
for (i in byteArray.indices) {
val intValue = (byteArray[i] and 0xFF.toByte()).toInt()
val stringValue = Integer.toHexString(intValue)
if (stringValue.length < 2) {
stringBuilder.append(0)
}
stringBuilder.append(stringValue)
}
return stringBuilder.toString()
}
注意导入的包:
import org.apache.commons.codec.binary.Hex
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
在Spring拦截器中进行认证校验
@Component
class MyInterceptor : HandlerInterceptor {
@Autowired
lateinit var checker: HmacChecker
override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
// 校验是否为hmac请求
if (valid(request)) {
// 进行hmac校验
if (validAccess(request)) {
return true
}
throw Exception("check error")
}
return true
}
private fun valid(req: HttpServletRequest): Boolean {
return req.headerNames.toList().contains("HMACAuthorization")
}
private fun validAccess(req: HttpServletRequest): Boolean {
return checker.check(req, null, null)
}
}
将MyInterceptor注册进拦截器
@Component
class AuthConfig : WebMvcConfigurer {
@Autowired
lateinit var myInterceptor MyInterceptor;
override fun addInterceptors(registry: InterceptorRegistry) {
//token校验ignores
registry.addInterceptor(myInterceptor)
.addPathPatterns("/**")
}
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**")
.allowedMethods("HEAD", "GET", "PUT", "POST", "DELETE", "PATCH", "OPTIONS", "TRACE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600)
}
}
校验主逻辑:
先定义一个Checker接口,以防后期有别的校验引入。增强其扩展性。
interface Checker {
fun check(request: HttpServletRequest, response: HttpServletResponse?, handler: Any?): Boolean
}
@Component
class HmacChecker : Checker {
override fun check(request: HttpServletRequest, response: HttpServletResponse?, handler: Any?): Boolean {
// 从请求头中获取用于hmac校验的值
val key = request.getHeader("key")
val time = request.getHeader("time")
val auth = request.getHeader("Authorization")
// 读取配置文件的值 获取对应服务的密钥信息
val keyMap = authKeyManager.getAuthKeyMap()
if (!keyMap.containsKey(key)) {
return false
}
val secretKey = keyMap[key]!!.toByteArray()
val url = request.requestURI + time
val data = url.toByteArray()
// 在接收端再次进行加密 并和传输的加密值进行对比
// 如果密钥一致 计算得出的值一定是一样的,否则请求不合法
val authKey = HmacUtils().encryptHmac(secretKey, data)
if (authKey != auth) {
// 请求不合法 校验失败
return false
}
// 校验通过
return true
}
}
go 语言的加密实现
func HmacMD5(key string, data string) []byte {
h := hmac.New(md5.New, []byte(key))
h.Write([]byte(data))
return h.Sum(nil)
}