极简抖音项目——密码加盐加密&token生成与验证 | 青训营笔记

625 阅读3分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第4篇笔记

本文描述了在项目中使用的密码加盐和加密操作,以及token的生成方式。

  • 对接收到的用户密码进行加密操作再存入数据库而不是明文存入,减少了用户密码泄漏风险。

  • 使用jwt-go生成token并设置token的有效期,使用token验证用户。

密码加密

  • 本项目初期使用sha256散列加密,不加盐,接收到密码之后进行加密存入数据库,对登录用户的验证采用接收密码使用sha256散列,与数据库所存密码对比,符合则视为密码正确。但是,发现使用解密工具能很容易的破解密码,例如在线sha解密网站www.ttmd5.com/hash.php?ty…
  • 进行调研之后发现pbkdf2加密算法可以一定程度上减缓暴力破解的速度。

这里是初期使用的加密版本,可以看出仅仅接收了passwd进行sha256散列,并转化为十六进制使用string保存在数据库中

func Encryption(passwd string) string {
	h := sha256.New()
	h.Write([]byte(passwd))
	return fmt.Sprintf("%x", h.Sum(nil))

}

这是第二版使用pbkdf2进行加密,接收密码,盐以及迭代次数,在本程序中,salt = username,iter = 10

func Encryption_PBKDF2(passwd, salt string, iter int) string {
	return fmt.Sprintf("%x", pbkdf2.Key([]byte(passwd), []byte(salt), iter, 32, sha256.New))
}

修改之后发现一个问题,像这样中途更改加密算法,之前的用户岂不是不能再次登录了?

所以在这里在user里边添加了一个Encryption字段和迭代次数Iter,用于标记加密手段,对于之前的用户来讲,这个字段是null,对于之后注册的用户来讲,这个字段应该填入"pbkdf2"。

这里也可以新建立一个表,用于存储用户密码的加密手段。

在用户登录界面增加了一个switch判断,如果Encrption字段为空,就使用default,也就是普通不加盐的Encryption算法,如果Encrption == "pbkdf2",那就使用PBKDF2算法。

var password string
	switch user.Encryption {
	case "pbkdf2":
		password = service.Encryption_PBKDF2(c.Query("password"), username, user.Iter)
	default:
		password = service.Encryption(c.Query("password"))
	}

其中Iter参数可以根据当前网络环境和硬件的更新,进行更新,增加或减少次数,由于同样存入数据库中,所以之后的更新不影响之前的参数。

测试

对两种算法的速度进行测试,两种算法的速度差距还是挺大的,在iter=10的时候能够达到十二倍的差距,但是iter=1时候还是有接近五倍的差距。

iter = 1

BenchmarkEncruption-4            2490416               481.6 ns/op
BenchmarkEncruptionPBKDF2-4       662444              1876 ns/op

iter = 10

BenchmarkEncruption-4            2576559               465.2 ns/op
BenchmarkEncruptionPBKDF2-4       189534              5645 ns/op

iter = 20

BenchmarkEncruption-4            2614942               466.3 ns/op
BenchmarkEncruptionPBKDF2-4       116610              9871 ns/op

iter = 100

BenchmarkEncruption-4            2566798               465.6 ns/op
BenchmarkEncruptionPBKDF2-4        26781             44085 ns/op

Token生成

本项目使用用户名和ID生成了token,token不保存,设置token的过期时间是七天,每次接收到token都会进行有效性验证,并解析出对应得用户信息。

package service

import (
	"time"

	"github.com/dgrijalva/jwt-go"
)

var jwtKey = []byte("a_secret_crect")

type Claims struct {
	Username string
	Id       int64
	jwt.StandardClaims
}

//根据用户名和Id生成token
func GetToken(username string, user_id int64) (token string, err error) {
	token, err = ReleaseToken(username, user_id)
	return
}

//验证token的有效性
func TokenValidity(tokenString string) (int64, bool) {
	token, claims, err := ParseToken(tokenString)
	if err == nil && token.Valid {
		return claims.Id, true
	} else {
		return 0, false
	}

}

//token分发
func ReleaseToken(username string, user_id int64) (string, error) {
	expirationTime := time.Now().Add(7 * 24 * time.Hour) //token截至有效时间
	claims := &Claims{
		Username: username,
		Id:       user_id,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: expirationTime.Unix(),
			IssuedAt:  time.Now().Unix(), //发放的时间
			Issuer:    "DouShen",         //谁发放的
			Subject:   "user token",
		},
	}
	//token分三部分,加密协议.储存的信息.前面两部分加上key再哈希得到的值
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString(jwtKey)
	if err != nil {
		return "", err
	}
	return tokenString, nil
}

//token解析
func ParseToken(tokenString string) (*jwt.Token, *Claims, error) {
	claims := &Claims{}
	token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (i interface{}, err error) {
		return jwtKey, nil
	})

	return token, claims, err
}