go 实现aes加密的自定义token

go 实现aes加密的自定义token,第1张

在web项目中,无状态的token较为通用的是jwt。对他了解了一番之后,发现在普通的web应用中,似乎 也不是那么的好:

jwt token 非常的长性能一般

个人认为除了无状态的优势以外,选择它的理由就是它业界通用,对于目前各种授权登录的方式有好处。

想了一圈,还是决定放弃他,自己实现一个自己的token。

我设计的token包含三部分内容:

token创建的时间,放在最开头,以时间戳的形式,并装为byte数组形式,固定占8个byte中间放用户的信息,这个可以自定义内容。转为字符串即可最后一部分是由前两部分计算得来的hashCode

其实也是参考了jwt的形式,只不过,将内容简化,当然也就只适用于自己的应用,不通用了,也没有refresh token这些。

加密与解密

加密的过程如下:

获取当前时间的时间戳,因为时间戳更方便计算是否过期。然后binary.BigEndian.PutUint64转化为比byte数组,放在token的最开始将时间和用户信息的字符串拼接好将拼接好的字符串进行hash计算,得到HashCode然后将hashCode拼接到尾部然后将整个字符串做AES加密最后base64转码(刚开始不转码直接添加到http请求头,发现会导致response丢失,有点无知了)

解密的过程基本就反过来,毕竟是对称加密,基本就是怎么来的就怎么回去。

hashCode

hashCode的特点是不可逆,长度固定,市面上有很多优秀的hashCode工具,但是他们返回的HashCode都太长了。没有必要

因为是在对称加密的基础上在加一层对数据的保护,其实并不太在意hash碰撞,一般也不容易发生碰撞的。

计算HashCode过程:

设定一个HashCode的长度HashCodeLen计算字符串的长度SrcLen是否是HashCodeLen的整数倍,如果不是,则在末尾补一些数据然后将字符串分为HashCodeLen段,然后将一个段内的所有字符的ascii码相加,求和sum因为常见的字符的ascii码是32-126。共95个字符,所以这里取余95取得数值范围0-94,加32则将范围调整到了32-126于是将sum取余95再加32,便得到这一段的hashCode了。以此计算完每一段,便得到完整的HashCode 代码

代码注释很详细:

package token

import (
	"crypto/aes"
	"crypto/cipher"
	"encoding/binary"
	"encoding/hex"
	"errors"
	"goskeleton/app/global/variable"
	"time"
)

const HashCodeLen = 4 // 生成的HashCode的长度

type ConnAes struct {
	key        string
	keyBlock   cipher.Block
	iv         string // 偏移向量,为了和第一组明文异或。解析见:https://blog.csdn.net/ma_jiang/article/details/111704368
	ExpireTime int64  //过期时间 ,单位秒
	// refreshTime int // 过期刷新token允许的时间
}

func NewConnAes() *ConnAes {
	ca := ConnAes{}
	ca.key = variable.ConfigYml.GetString("AES.key")
	ca.iv = variable.ConfigYml.GetString("AES.iv")
	ca.ExpireTime = variable.ConfigYml.GetInt64("AES.CreatedExpireAt")
	keyBlock, _ := aes.NewCipher([]byte(ca.key))
	ca.keyBlock = keyBlock
	return &ca
}

// 生成token
func GetToken(src string) (string, error) {

	srcByte := make([]byte, len(src)+8+HashCodeLen)                    // 加8是因为时间戳转byte之后占8个byte,加HashCodeLen是因为hashcode占HashCodeLen个字节
	binary.BigEndian.PutUint64(srcByte[:8], uint64(time.Now().Unix())) // 将时间戳添加到开头
	for i, v := range src {                                            // 将字符串字符逐个添加到数组中
		srcByte[i+8] = byte(v)
	}
	getSrcHashCode(srcByte) // 计算HashCode
	return encrypt(srcByte)
}

// 更新token,就是将token的时间更换为当前时间
//返回值:1.用户信息2.token3.错误
func RefreshToken(tokenSrc string) (string, string, error) {
	// base64解码
	tokenByte, err := hex.DecodeString(tokenSrc)
	if err != nil {
		return "", "", errors.New("无效token!")
	}
	ca := NewConnAes()
	tokenMsg, err := ca.decrypt(tokenByte) // 解码
	if err != nil {
		return "", "", errors.New("无效token!")
	}
	if !isEffective(tokenMsg) { // 检查token是否被篡改
		return "", "", errors.New("无效token!")
	}

	isNotExpired, isRefresh := ca.isNotExpired(tokenMsg)

	if !isNotExpired {
		return "", "", errors.New("token已过期,请重新登录!")
	}

	userBody := string(tokenMsg[8 : len(tokenMsg)-HashCodeLen]) // 用户信息字符串,前八位是时间戳,后面是hashCode
	if !isRefresh {
		return userBody, tokenSrc, nil // 还不需要刷新
	}

	timeByte := make([]byte, 8)                                     // 时间戳转byte存储就占8个byte
	binary.BigEndian.PutUint64(timeByte, uint64(time.Now().Unix())) // 将时间戳转化为byte形式大端存储
	for i := 0; i < 8; i++ {
		tokenMsg[i] = timeByte[i]
	}
	getSrcHashCode(tokenMsg)
	newToken, err := encrypt(tokenMsg)
	return userBody, newToken, err // 去掉末尾的hash码
}

//加密
func encrypt(src []byte) (string, error) {

	ca := NewConnAes()
	// 填充数据补齐
	paddinglen := aes.BlockSize - (len(src) % aes.BlockSize)
	for i := 0; i < paddinglen; i++ {
		src = append(src, byte(paddinglen))
	}
	// enbuf := make([]byte, len(src))
	cbce := cipher.NewCBCEncrypter(ca.keyBlock, []byte(ca.iv))
	cbce.CryptBlocks(src, src)
	return hex.EncodeToString(src), nil
}

//解密
func (ca *ConnAes) decrypt(src []byte) ([]byte, error) {

	if (len(src) < aes.BlockSize) || (len(src)%aes.BlockSize != 0) {
		return nil, errors.New("error encrypt data size")
	}

	cbcd := cipher.NewCBCDecrypter(ca.keyBlock, []byte(ca.iv))
	cbcd.CryptBlocks(src, src)
	// 去掉填充的数据
	paddinglen := int(src[len(src)-1])
	if paddinglen > 16 {
		return nil, errors.New("error encrypt data size")
	}
	return src[:len(src)-paddinglen], nil
}

// 验证字符串的hash码,防止被篡改
func isEffective(token []byte) bool {
	oldHashCode := make([]byte, HashCodeLen)

	copy(oldHashCode, token[len(token)-HashCodeLen:]) // 将原始的hashCode备份下来,否则待会会被覆盖
	getSrcHashCode(token)                             // 更新hashCode
	newHashCode := token[len(token)-HashCodeLen:]     //取到更新后的hashCode
	for i := 0; i < len(oldHashCode); i++ {
		if oldHashCode[i] != newHashCode[i] { // 一旦有一个对不上,说明数据被篡改过
			return false
		}
	}
	return true
}

// 验证token是否过期
//返回值表示:1. 是否在有效期内;2.是否需要刷新
func (ca *ConnAes) isNotExpired(token []byte) (bool, bool) {

	// 时间以大端uint64的形式保存在开头,占8个byte
	tokenTimeStamp := int64(binary.BigEndian.Uint64(token[0:8])) // 时间戳形式

	now := time.Now().Unix() //当前时间的时间戳形式

	if now-(tokenTimeStamp+ca.ExpireTime) < 0 { // 检查token是否过期
		if now-(tokenTimeStamp+int64(ca.ExpireTime/2)) < 0 { // 检查token是否有效期过半,以此作为是否需要刷新的标志
			return true, false // 未过有效期,还不需要刷新
		}
		return true, true //未过有效期,但需要刷新了
	} else {
		return false, false // 过了有效期,不需要刷新
	}
}

// 计算字符数组的hash值,这里设置的是哈希值为HashCodeLen个字符,当然也可以设置成别的长度
// 目的是将任意字符串转换为HashCodeLen位字符的哈希值,用于校验字符串是否被修改
// 这里不太在意hash碰撞,因为仅仅是为了一定程度上校验字符串是否被修改
// 工具库有很多已经实现的hash算法,还要去自己实现是因为我希望生成一个很短的hash码,减少字符串的长度,另外自己的hash也会让外人摸不着头脑,而不从破译
// 当然也可以按照自己的思路修改该算法,其余地方都无需修改,保证输出是四个字符即可
func getSrcHashCode(src []byte) {
	realLen := len(src) - HashCodeLen  // 字符串的长度由1.时间2.用户信息3.hashCode三部分组成,其中前两部分是计算hash的内容,而hashCode的长度是HashCodeLen,所以这里去掉HashCodeLen
	remainder := realLen % HashCodeLen // 余数,因为最终的hash值为HashCodeLen个字符,所以取余HashCodeLen
	if remainder != 0 {                // 等于0就刚好是HashCodeLen的倍数,不需要填充数据了
		for i := 0; i < HashCodeLen-remainder; i++ {
			//这里添加字符是临时的,计算完之后会被hashCode覆盖
			src[realLen+i] = byte(HashCodeLen) // 末尾填充字符,以保证长度是HashCodeLen的倍数,当然也可以改成添加别的字符
		}
	}
	var length int
	if remainder == 0 {
		length = realLen / HashCodeLen
	} else {
		length = (realLen + HashCodeLen - remainder) / HashCodeLen // 将字符数组分为HashCodeLen段,每段的长度
	}
	// 这里采用倒叙的方式计算,因为余数小于HashCodeLen,所以填充的数据的量必然小于HashCodeLen
	// 所以最后一位一定是空闲的,所以倒着计算,先将后面几位的hashCode计算出来,填到最后一位上,这样前面的几个空位就释放了
	// 依次倒推,倒着计算出hashCode,也就不需要去new一个新的数组来存放hashCode了
	for i := HashCodeLen - 1; i >= 0; i-- {

		sum := 0
		index := i * length // 每一段字符的起点
		//整体倒着计算,但是当前段的sum还是可以顺着去求和的
		for j := index; j < index+length; j++ { // 每次循环length长度的字符
			sum = sum + int(src[j])
		}
		// 因为常见的字符的ascii码是32-126。共95个字符,所以这里取余95取得数值范围0-94,加32则将范围调整到了32-126
		// 从末尾开始添加:len-1是最后一个字符位,len-HashCodeLen就是hashCode的第一个字符
		// 因为i是从HashCodeLen-1开始计算的,len-HashCodeLen+(HashCodeLen-1)=len-1,所以len-HashCodeLen+i便又从倒数第一个字符位开始了
		src[len(src)-HashCodeLen+i] = byte(sum%95 + 32)
	}
}
使用

当前就两个使用场景:

用户注册和登录,成功之后调用GetToken,获取token需要鉴权的接口,通过中间件(拦截器)的形式调用RefreshToken解析token并鉴权,并更新token

再扯点别的,无状态token的弊端之一就是服务器无法对其进行管理,没法主动踢下线。主流的方案是将token缓存到redis,然后通过redis踢下线,这种方案就丢失了无状态的优势了。因为大量token存到redis,还是挺烦的。
所以网上另一个方案是在redis缓存黑名单,将需要踢下线的token加到redis,匹配到的就不给登录,这样量就下来了。我觉得行。

所以,jwt的token那么长,也不好。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/995257.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存