项目学习之JWT验证|青训营笔记

241 阅读4分钟

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

一、Token 是什么?

  • 从一般的角度上说: Token 在计算机身份认证中是临时令牌的意思。一般作为邀请、登录系统使用。
  • 在前后端分离项目中:
  1. 产生:Token 是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token 便应运而生。(建议不要在 token 里面存密码,有危险性)
  2. 效果:Token 是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个 Token 便将此 Token 返回给客户端,以后客户端只需带上这个 Token 前来请求数据即可,无需再次带上用户信息。
  3. 目的:Token 的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。

二、使用 JWT 实现 Token 验证

1、JWT 是什么?

JWT 全称 JSON web Token,是一种跨域认证解决方案,属于一个开放的标准,它规定了一种 Token 实现方式,目前多用于前后端分离项目和 OAuth2.0 业务场景下。

2、JWT 的构成

JWT 由三部分构成,第一部分是 header,第二部分为 payload,第三部分是 signature。

  • 在 header 中存放着令牌类型和令牌使用的加密算法。
  • 在 payload 存放有效信息,这些有效信息包含三个部分:标准中注册的声明、共有的声明和私有的声明。jwt.StandardClaims 定义的就是标准中注册的声明。
  • 在 signature 存放签证信息,用于校验消息在整个过程中有没有被篡改。

3、以 jwt-go 为例,我们的信息应该放在哪里?

定义自己的 Claim 结构体,以我的当前项目为例

type MyClaims struct {
	UserId   int64  `json:"user_id"`
	UserName string `json:"username"`
	jwt.StandardClaims
}

分别有用户ID、用户账号、和 jwt-go 中定义的结构体。

  • jwt.StandardClaims 结构体
type StandardClaims struct {
	Audience  string `json:"aud,omitempty"`
	ExpiresAt int64  `json:"exp,omitempty"`
	Id        string `json:"jti,omitempty"`
	IssuedAt  int64  `json:"iat,omitempty"`
	Issuer    string `json:"iss,omitempty"`
	NotBefore int64  `json:"nbf,omitempty"`
	Subject   string `json:"sub,omitempty"`
}
  • 字段解释 Audience是受众,即接受 JWT 的一方,ExpiresAt是所签发的 JWT 过期时间,Id是 JWT 的唯一标识,IssueAt是签发时间,Issuer是 JWT 的签发者,NotBefore是 JWT 的生效时间,Subject是主题。

三、jwt-go demo 可供参考

var JwtKey = []byte("my_jwt_key")

type MyClaims struct {
	UserId   int64  `json:"user_id"`
	UserName string `json:"username"`
	jwt.StandardClaims
}

// CreateToken 生成token
func CreateToken(userId int64, userName string) (string, error) {
	expireTime := time.Now().Add(720 * time.Hour) //过期时间 720 / 24 = 30 天
	nowTime := time.Now()                         //当前时间
	claims := MyClaims{
		UserId:   userId,
		UserName: userName,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: expireTime.Unix(), //过期时间戳
			IssuedAt:  nowTime.Unix(),    //当前时间戳
			Issuer:    "HJZ",             //颁发者签名
			Subject:   "userToken",       //签名主题
		},
	}
	// 使用指定的签名方法(加密算法)和 claims 实例创建签名对象,返回的 token 对象中 SigningString 被填充
	tokenStruct := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	// 使用自己定制的密钥对这个对象签名,获取到最终的token
	// 返回 SigningString 和 tokenStruct.Method.Sign(这个方法需要你传入你的密钥得到签名) 返回的字符串(使用.分隔两个字符串)
	return tokenStruct.SignedString(JwtKey)
}

// CheckToken 验证token
func CheckToken(token string) (*MyClaims, bool) {
	// 解析 token
	// 传入 token 和你自定义的 claims 结构
	// ParseWithClaims对claims进行格式的解码和校验,并检查它是否有效,最终将Claims返回(回调函数)
	tokenObj, err := jwt.ParseWithClaims(token, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
		return JwtKey, nil
	})
	if err != nil {
		return nil, false
	}

	// 校验 token,token 中判断是否有效的的字段 Valid,它的值与 ExpiresAt、Issuer 有关
	if key, _ := tokenObj.Claims.(*MyClaims); tokenObj.Valid && time.Now().Unix() < key.ExpiresAt {
		return key, true
	} else {
		return nil, false
	}
}

测试

func main() {
	token, err := CreateToken(10, "记忆")
	if err != nil {
		panic(err)
	}
	fmt.Printf("生成的token是:\n%v\n", token)

	str, success := CheckToken(token)
	if !success {
		fmt.Println("token err")
		return
	}
	fmt.Printf("token解析结果:\n%+v\n", str)
}

测试结果

  • 生成的token是: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMCwidXNlcm5hbWUiOiLorrDlv4YiLCJleHAiOjE2NTYzODM5OTYsImlhdCI6MTY1Mzc5MTk5NiwiaXNzIjoiSEpaIiwic3ViIjoidXNlclRva2VuIn0.QhGw0zh-RiE6qzuki8EYDjyVqTuPEOY11WsRb9Gz_Jk
  • token解析结果: &{UserId:10 UserName:记忆 StandardClaims:{Audience: ExpiresAt:1656383996 Id: IssuedAt:1653791996 Issuer:HJZ NotBefore:0 Subject:userToken}}
  • 解释: 可以看到 token 解析出来的信息和我们输入的信息是完全一致的。

四、总结

使用 JWT 可以解决前后端应用中后端辨识用户的问题,保证了后端不用重复的去查找数据库就可以知道是哪一个用户在进行操作,但有存在一定的风险,比如重放攻击,因为某个用户的 token 是在网络中传输的,如果有别人截取到这个数据包的数据,可以重新发送这个数据包,服务端因为接收到了正确的 token,身份验证也就通过了,后面的操作就变成跟正常用户访问一样。