jwt登录
|字数总计:1.8k|阅读时长:7分钟|阅读量:|
概述
在实际应用中,很多API接口通常都需要在用户登录后才允许访问操作。但是显然不可能每次访问页面都携带用户名密码数据,这样不仅不安全,而且还会对数据库造成较大的压力。所以我们需要另辟蹊径。
实际上,由于http请求是无状态的,所以为了维持登录状态,必须在每次请求时,携带某种能够代表用户信息的字符串,且该字符串唯一,不可被伪造。这个字符串通常称为token。
想象以下,有一个地方,必须需要拿出相关证明才能入内。而该证明需要你通过某些个人信息去办理才能申请的到。为了防止开假证明,通常需要对证明进行签字,按压指纹等操作。
jwt实际上可以采用上述场景做比喻。当你需要访问服务器API时,服务器要求你携带jwt字符串才能访问该api,而jwt字符串需要你先使用用户名密码登录至系统才能申请到。为了防止jwt字符串被伪造,通常还需要使用服务器上存放的密钥来参与最终jwt字符串的构成。
由于计算机中字符串可轻易篡改,故不能将服务器上的密钥直接组合到jwt字符串中,而是需要服务器端存储的密钥数据与半成品jwt字符串做某种加密运算,得到加密后的签名,拼接在半成品jwt字符串之后,中间用.隔开。即实现jwt字符串的生成。之后服务器拿到该字符串只需要验证前半部分半成品jwt经过加密后是否与后面的签名一致即可发现该jwt字符串是否发生篡改。
jwt登录流程
如下图的顺序图即一次正常登录与请求的流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @startuml actor 用户 as user participant 浏览器 as browser participant 服务器 as server database 数据库 as db user -> browser++: 输入用户名,点击登录
browser -> server++: POST /login \n {username,password} server -> db++: 验证username与password是否对应 return 返回验证结果 server -> server: 创建jwt字符串 return jwt字符串
user -> browser: 进行需要登录的操作 browser -> server++: 发送请求且header携带jwt字符串 server -> server: 检查jwt是否合法,解析用户uid server -> server: 进行业务逻辑处理 return 返回响应 @enduml
|
jwt结构
jwt全程为 JSON Web Token,是现行的一种开放标准,不限定具体的编程语言。
JWT由三部分构成:header(头部),payload(载荷)或claim,signature(签名)。
这部分主要有两个字段
-
alg:加密算法的类型
-
typ:token的类型,这里就是jwt
payload载荷
这部分定义了七个标准字段
- iss :token的发行者
- sub :token面向的用户
- aud :受众
- exp :token的过期时间,Unix时间戳
- nbf :not before , 如果当前时间在 nbf 里的时间之前,则Token不被接受
- iat :token的签发时间,Unix时间戳
- jti :当前token的唯一标识
当然我们也可以额外添加一些信息进入,减少服务器查询数据库的压力。
signature签名
由header和payload的json字符串的base64编码中间用.隔开的字符串拼接后经过header.alg算法加密而成
jwt字符串构成
最终的jwt字符串由三部分拼接而成:header的json字符串的base64编码,payload的json字符串的base64编码,signature,中间用.作为分割符拼接而成的总字符串。
生成后的字符串通常为如下形式
代码生成jwt字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| package main
import ( "crypto" "crypto/hmac" _ "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "log" "time" )
type H map[string]interface{}
func main() { secretKey := "abc" headerJson, _ := json.Marshal(H{ "alg": "HS256", "typ": "JWT", })
payloadJson, _ := json.Marshal(H{ "exp": time.Now().Add(time.Hour).Unix(), "uid": 12, })
headerBase64 := base64.StdEncoding.EncodeToString(headerJson) payloadBase64 := base64.StdEncoding.EncodeToString(payloadJson) signatureRaw := fmt.Sprintf("%s.%s", headerBase64, payloadBase64) m := crypto.SHA256 if !m.Available() { log.Fatalln("SHA256不可用") } hasher := hmac.New(m.New, []byte(secretKey)) hasher.Write([]byte(signatureRaw)) signature := base64.RawURLEncoding.EncodeToString(hasher.Sum(nil))
token := fmt.Sprintf("%s.%s", signatureRaw, signature) log.Println(token) }
|
使用上述代码,可生成如下jwt字符串
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NTQ4MzI1NTYsInVpZCI6MTJ9.cPXYeqyUYOHGJ8uXPzA82MqU5iPFLVxFq2YhAvyHm_E
验证代码的正确性
打开网址JSON Web Tokens - jwt.io,可进行在线jwt生成与解析
如上图,我们将jwt的前半部分粘贴进入,再将密钥abc填入输入框中,得到下图
可发现golang生成的jwt字符串与浏览器上生成的相一致,说明jwt生成的代码的正确性,jwt.io网站也是一个常用的jwt解析工具。
工程使用
实际开发时,一般我们会调用现成的jwt库,如golang中常用的
github.com/golang-jwt/jwt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| package main
import ( "crypto" "crypto/hmac" _ "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "github.com/golang-jwt/jwt" "log" "time" )
type H map[string]interface{}
func myJwt(secretKey string, uid int, exp int64) string { headerJson, _ := json.Marshal(H{ "alg": "HS256", "typ": "JWT", })
payloadJson, _ := json.Marshal(H{ "exp": exp, "uid": 12, })
headerBase64 := base64.StdEncoding.EncodeToString(headerJson) payloadBase64 := base64.StdEncoding.EncodeToString(payloadJson) signatureRaw := fmt.Sprintf("%s.%s", headerBase64, payloadBase64) m := crypto.SHA256 if !m.Available() { log.Fatalln("SHA256不可用") } hasher := hmac.New(m.New, []byte(secretKey)) hasher.Write([]byte(signatureRaw)) signature := base64.RawURLEncoding.EncodeToString(hasher.Sum(nil))
token := fmt.Sprintf("%s.%s", signatureRaw, signature) return token }
func libJwt(secretKey string, uid int, exp int64) string { t, _ := jwt.NewWithClaims( jwt.SigningMethodHS256, struct { jwt.StandardClaims Uid int `json:"uid"` }{ jwt.StandardClaims{ ExpiresAt: exp, }, uid, }, ).SignedString([]byte(secretKey)) return t }
func main() { uid := 12 exp := time.Now().Add(time.Hour).Unix() secretKey := "abc" log.Println(myJwt(secretKey, uid, exp)) log.Println(libJwt(secretKey, uid, exp)) }
|
上述代码定义了两个函数,一个是我们自己写的jwt生成函数,一个是调用第三方库的生成函数。
输出结果如下:
2022/06/10 14:06:58 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NTQ4NDQ4MTg
sInVpZCI6MTJ9._QeTrywcIRbEa_Ii1-i3JEyRNc2XQw1GSB_E2EKv5iU
2022/06/10 14:06:58 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NTQ4NDQ4MTg
sInVpZCI6MTJ9._QeTrywcIRbEa_Ii1-i3JEyRNc2XQw1GSB_E2EKv5iU
可发现两个生成结果一样,
实际使用时,我们的claims经常需要定制一些字段,故我们经常需要对jwt进行二次封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| package util
import ( "github.com/golang-jwt/jwt" "time" )
type Jwt struct { secretKey string expiresDuration time.Duration }
func NewJwt(secretKey string, expiresDuration time.Duration) *Jwt { return &Jwt{ secretKey: secretKey, expiresDuration: expiresDuration, } }
type customClaims struct { jwt.StandardClaims Uid int `json:"uid"` }
func (j *Jwt) GenerateToken(uid int) string { t := jwt.NewWithClaims( jwt.SigningMethodHS256, customClaims{ jwt.StandardClaims{ ExpiresAt: time.Now().Add(j.expiresDuration).Unix(), }, uid, }, ) token, _ := t.SignedString([]byte(j.secretKey)) return token }
func (j *Jwt) ParseToken(token string) (*customClaims, error) { var cc customClaims _, err := jwt.ParseWithClaims(token, &cc, func(t *jwt.Token) (interface{}, error) { return []byte(j.secretKey), nil }) if err != nil { return &cc, err } return &cc, nil }
|