这是我参与「第五届青训营」笔记创作活动的第 1 天
快速上手
安装jwt-go依赖:go get github.com/dgrijalva/jwt-go
原理简介
背景
HTTP协议是一种无状态的协议,这意味着用户提供账号和密码进行登录认证后,下次再请求的时候,仍然需要认证,因为服务器并不知道是谁发送的请求,并不知道该用户已经认证过一次。
所以为了解决这一问题,保持客户端与服务端的会话状态,在服务器的缓存中需要为每位用户分配一份存储空间,用于存储用户的个人登录信息等,且每份存储空间有个唯一标识ID作为自己的身份证;
这样在作响应的时候,将该ID返回给浏览器,浏览器存储到本地,以便后续再次请求时都可以携带着这个ID,服务器就能根据这个ID去对应缓存空间中去查找是否存在对应的存储区,能找到则表示该用户之前已经访问过了,存储区存储的登录信息等也可以直接使用,就不用再次登录了。
这就是传统的基于session+cookie的会话保持技术。session是存在于服务端的一个缓存区,相当于一个存储数据的map数据结构,每个session对应着自己的sessionId;而cookie则是存在于客户端浏览器的一种数据存储区。
但是这种基于Session的会话保持技术存在很多弊端,如:
- 随着用户增加服务器的开销也会增大;
- 在处理分布式应用的情境下会相应的限制负载均衡器的能力;
- Cookie存储在客户端,如果被拦截窃取,会很容易受到CSRF跨域伪造请求攻击;
- 除浏览器之外的其他设备对Cookie的支持并不友好,对跨平台支持不好;
概述
JWT全称Json Web Token,翻译为JSON格式的网络令牌,很多时候又简称为Token,它要解决的问题,就是为多种终端设备,提供统一的、安全的令牌格式。因此,JWT只是一个令牌格式而已,你可以把它存储到Cookie,也可以存储到localstorage,没有任何限制!同样的,对于传输,你可以使用任何传输方式来传输JWT,一般来说,我们会使用HTTP消息头来传输它。比如,当登录成功后,服务器可以给客户端响应一个JWT。
组成
为了保证令牌的安全性,JWT令牌由三个部分组成,分别是:
- header:令牌头部,记录了整个令牌的类型和签名算法
- payload:令牌负荷,记录了保存的主体信息,比如你要保存的用户信息就可以放到这里
- signature:令牌签名,按照头部固定的签名算法对整个令牌进行签名,该签名的作用是:保证令牌不被伪造和篡改
Token操作
定义Claim及属性
claim 即 jwt 的载荷(payload)
Userame 属性为我们自定义的载荷,而 jwt.StandardClaims 则为 jwt 自带的属性
type myClaims struct {
Username string `json:"username"`
jwt.StandardClaims
}
而StandardClaims的内容如下
type StandardClaims struct {
Audience string `json:"aud,omitempty"`(受众,即接受 JWT 的一方)
ExpiresAt int64 `json:"exp,omitempty"`(所签发的JWT的过期时间)
Id string `json:"jti,omitempty"`(JWT的Id)
IssuedAt int64 `json:"iat,omitempty"`(签发时间)
Issuer string `json:"iss,omitempty"`(JWT的签发者)
NotBefore int64 `json:"nbf,omitempty"`(JWT的生效时间)
Subject string `json:"sub,omitempty"`(主题)
}
生成Token
在生成Token前需要对Claim实例化,由于需要加密,因此还需要定义一个密钥。
mySigningKey := []byte("1425723960@qq.com")
c := myClaims{
Username: "LiuBing",
StandardClaims: jwt.StandardClaims{
NotBefore: time.Now().Unix() - 5, //(JWT的生效时间)
ExpiresAt: time.Now().Unix() + 5, //(所签发的JWT的过期时间)
Issuer: "BingBing",
},
}
NewWithClaims方法的第一个参数是加密算法的类型,第二个参数是Claim实例
//返回Token(未加密)
token1 := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
//对Token进行加密
secret, err := token1.SignedString(mySigningKey)
if err != nil {
fmt.Printf("%s\n", err)
}
解析Token
//接收到前端传来的secret密文
token2, err := jwt.ParseWithClaims(secret, &myClaims{}, func(token *jwt.Token) (interface{}, error) {
return mySigningKey, nil
})
if err != nil {
fmt.Printf("%s\n", err)
}
fmt.Println(token2)
fmt.Println(token2.Claims)
fmt.Println(token2.Claims.(*myClaims).Username)
运行结果如下