在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丢失,有点无知了)解密的过程基本就反过来,毕竟是对称加密,基本就是怎么来的就怎么回去。
hashCodehashCode的特点是不可逆,长度固定,市面上有很多优秀的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那么长,也不好。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)