这是我参与「第三届青训营 -后端场」笔记创作活动的第二篇笔记。
1. 遇到的问题
在项目期间,注册用户在下次登录的时候,经常需要重新输入用户名和密码比较麻烦,于是引入jwt Token。
2. 什么原因导致这个问题
因为http协议本身是一种无状态的协议,而这就导致用户每次请求时,都需要携带用户名和密码来进行用户认证,因为根据http协议,我们不清楚是哪个用户发的请求。
3. 解决方法
我们可以在服务器当中,存一份用户登录的信息,这份登录信息会在响应的时候发送给应用,这样应用就能知道识别是哪个用户请求的。这是传统的session认证
Cookie-Session原理
session缺点
通常session保存在内存当中,这就意味着每次用户认证,都要请求这台服务器,服务器的开销比较大,这在分布式应用上,限制了负载均衡的能力。在分布式当中,你在一台服务器做了认证,并且该服务器有你的session,但是当你的访问被打到另一个服务器时,你还是要重新登录。
为了解决上述问题,项目当中采用的是JWT Token
JWT Token原理
JWT优点
性能方面: Session最大的缺点就是存储在服务器中,所以占用大量服务器内存,token把压力给到客户端,可以明显减轻服务器内存压力。
分布式:例如在一个集群(3台服务器),每次用户登录,访问服务器。如果是传统的Session那么,该Session只会保存在一台服务器上,而Token就无所谓。
安全性: token需要签名,所以安全性相对高。
是否跨域:token可以跨域访问。
JWT缺点
- jwt不适合存放大量信息,因为信息越长,token越长。
- jwt token设置的过期时间是死的,不能修改。
4. 项目中JWT Token的使用
JWT数据结构
eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIi0iIxMjM0NTY30DkwIiwi bmFtZSI6IkpvaG4gRG91IiwiaXNTb2NpYWwi0nRydWV9.
4pcPyMD09olPSyXnrXCj TwXyr4BsezdI1AVTmud2fU4
token是一段很长的字符串,中间用.分割成三部分。
Header (头部)
Payload (负载)
Signature (签名)
//token是一行
Header.Payload.Signature
Header
{
"alg": "HS256",
"typ": "JWT"
}
alg:签名算法,默认是HS256
typ:表示这个令牌(token)的类型,jwt令牌,类型就是jwt
Payload
官方字段:
iss (issuer): 签发人
exp (expiration time):过期时间
sub (subject): 主题
aud (audience): 受众
nbf (Not Before): 生效时间
iat (Issued At): 签发时间
jti (JWT ID): 编号
自定义字段:除了官方字段,还可以加上自定义字段,用于实际需求
{
"sub": "1111111",
"name": "小呆鸟",
"admin": true
}
Signature
Signature主要是对前俩部分的签名,防止数据篡改
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是HS256)产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
Base64URL
Header 和 Payload 串型化的算法是 Base64URL
5. JWT的使用方式
客户端携带Token有三种方式 :
-
放在请求头 (Token放在Header的Authorization中,并使用Bearer开头)
-
放在请求体
-
放在URI
注意:客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。
另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面
6.代码
定义密钥
var (
Secret = []byte("TikTok")
)
生成token
type JWTClaims struct {
UserId int64 `json:"user_id"` // 创建一个我们自己的声明
Username string `json:"user_name"` // 创建一个我们自己的声明
jwt.RegisteredClaims
}
//生成token
func GenToken(userid int64, userName string) (string, error) {
claims := JWTClaims{
UserId: userid, // 自定义字段
Username: userName, // 自定义字段
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "server",
//ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(),可用于设定token过期时间
},
}
//// 使用指定的签名方法创建签名对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
//// 使用指定的secret签名并获得完整的编码后的字符串token
signedToken, err := token.SignedString([]byte("TikTok"))
if err != nil {
return "", err
}
return signedToken, nil
}
解析token
//解析token
func ParsenToken(tokenString string) (*JWTClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (i interface{}, err error) {
return Secret, nil
})
if err != nil {
return nil, err
}
// 对token对象中的Claim进行类型校验
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
验证token
func VerifyToken(tokenString string) (int64, error) {
log.Debugf("tokenString:%v", tokenString)
if tokenString == "" {
return int64(0), nil
}
claims, err := ParsenToken(tokenString)
if err != nil {
return int64(0), err
}
return claims.UserId, nil
}
token鉴权放当做中间件
这样不用频繁的访问服务器,在中间件如果核实不对,就直接返回错误
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := c.PostForm("token")
if tokenString == "" {
tokenString = c.Query("token")
}
//tokenString := c.Query("token")
userId, err := VerifyToken(tokenString)
if err != nil || userId == int64(0) {
response.Fail(c, "auth error", nil)
c.Abort()
}
// 将当前请求的username信息保存到请求的上下文c上
c.Set("UserId", userId)
// 后续的处理函数可以用过c.Get("userId")来获取当前请求的用户信息
c.Next()
}
}
参考文章链接:jwt.io/introductio…