golang之JWT

什么是JSON Web Token?

JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以JSON方式安全地传输信息。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用HMAC算法)或使用RSAECDSA的公钥/私钥对对JWT进行签名

直白的讲jwt就是一种用户认证(区别于session、cookie)的解决方案。

出现的背景

众所周知,在jwt出现之前,我们已经有session、cookie来解决用户登录等认证问题,为什么还要jwt呢?

这里我们先了解一下session,cookie。

session

熟悉session运行机制的同学都知道,用户的session数据以file或缓存(redis、memcached)等方式存储在服务器端,客户端浏览器cookie中只保存sessionid。服务器端session属于集中存储,数量不大的情况下,没什么问题,当用户数据逐渐增多到一程度,就会给服务端管理和维护带来大的负担。

session有两个弊端:

1、无法实现跨域。

2、由于session数据属于集中管理里,量大的时候服务器性能是个问题。

优点:

1、session存在服务端,数据相对比较安全。

2、session集中管理也有好处,就是用户登录、注销服务端可控。

cookie

cookie也是一种解决网站用户认证的实现方式,用户登录时,服务器会发送包含登录凭据的Cookie到用户浏览器客户端,浏览器会将Cookie的key/value保存用户本地(内存或硬盘),用户再访问网站,浏览器会发送cookie信息到服务器端,服务器端接收cookie并解析来维护用户的登录状态。

cookie避免session集中管理的问题,但也存在弊端:

1、跨域问题。

2、数据存储在浏览器端,数据容易被窃取及被csrf攻击,安全性差。

优点:

1、相对于session简单,不用服务端维护用户认证信息。

2、数据持久性。

jwt

jwt通过json传输,php、java、golang等很多语言支持,通用性比较好,不存在跨域问题。传输数据通过数据签名相对比较安全。客户端与服务端通过jwt交互,服务端通过解密token信息,来实现用户认证。不需要服务端集中维护token信息,便于扩展。当然jwt也有其缺点。

缺点:

1、用户无法主动登出,只要token在有效期内就有效。这里可以考虑redis设置同token有效期一直的黑名单解决此问题。

2、token过了有效期,无法续签问题。可以考虑通过判断旧的token什么时候到期,过期的时候刷新token续签接口产生新token代替旧token。

jwt设置有效期

可以设置有效期,加入有效期是为了增加安全性,即token被黑客截获,也只能攻击较短时间。设置有效期就会面临token续签问题,解决方案如下

通常服务端设置两个token

  • Access Token:添加到 HTTP 请求的 header 中,进行用户认证,请求接口资源。
  • refresh token:用于当 Access Token过期后,客户端传递refresh token刷新 Access Token续期接口,获取新的Access Token和refresh token。其有效期比 Access Token有效期长。

jwt构成:

  • Header:TOKEN 的类型,就是JWT,签名的算法,如 HMAC SHA256、HS384
  • Payload:载荷又称为Claim,携带的信息,比如用户名、过期时间等,一般叫做 Claim
  • Signature:签名,是由header、payload 和你自己维护的一个 secret 经过加密得来的

jwt使用

这里推荐个使用比较多的开源项目github.com/dgrijalva/jwt-go更多文档

示例:

package main

import (
    "fmt"
    "github.com/dgrijalva/jwt-go"
    "time"
)
const (
    SECRETKEY = "243223ffslsfsldfl412fdsfsdf"//私钥
)
//自定义Claims
type CustomClaims struct {
    UserId int64
    jwt.StandardClaims
}
func main() {
    //生成token
    maxAge:=60*60*24
    customClaims :=&CustomClaims{
        UserId: 11,//用户id
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Add(time.Duration(maxAge)*time.Second).Unix(), // 过期时间,必须设置
            Issuer:"jerry",   // 非必须,也可以填充用户名,
        },
    }
    //采用HMAC SHA256加密算法
    token:=jwt.NewWithClaims(jwt.SigningMethodHS256, customClaims)
    tokenString,err:= token.SignedString([]byte(SECRETKEY))
    if err!=nil {
        fmt.Println(err)
    }
    fmt.Printf("token: %v\n", tokenString)

    //解析token
    ret,err :=ParseToken(tokenString)
    if err!=nil {
        fmt.Println(err)
    }
    fmt.Printf("userinfo: %v\n", ret)
}

//解析token
func ParseToken(tokenString string)(*CustomClaims,error)  {
    token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }
        return []byte(SECRETKEY), nil
    })
    if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
        return claims,nil
    } else {
        return nil,err
    }
}

运行结果:

token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjExLCJleHAiOjE1OTA5MTk1NDAsImlzcyI6ImplcnJ5In0.FppmbbHRrS4wd5wen73vYPOvtzycOrn2JZlK6JRjEGk userinfo: &{11 { 1590919540 0 jerry 0 }}

Claims

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"`

aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
iat: jwt的签发时间
iss: jwt签发者
nbf: 定义在什么时间之前,该jwt都是不可用的.就是这条token信息生效时间.这个值可以不设置,但是设定后,一定要大于当前Unix UTC,否则token将会延迟生效.
sub: jwt所面向的用户

以上用到了CustomClaims,也可以用简单的方法

示例

package main

import (
    "fmt"
    "github.com/dgrijalva/jwt-go"
    "time"
)
const (
    SECRETKEY = "243223ffslsfsldfl412fdsfsdf"//私钥
)
//自定义Claims
type CustomClaims struct {
    UserId int64
    jwt.StandardClaims
}
func main() {
    //生成token
    maxAge:=60*60*24
    // Create the Claims
    //claims := &jwt.StandardClaims{
    //  //  ExpiresAt: time.Now().Add(time.Duration(maxAge)*time.Second).Unix(), // 过期时间,必须设置,
    //  //  Issuer:    "jerry",// 非必须,也可以填充用户名,
    //  //}

    //或者用下面自定义claim
    claims := jwt.MapClaims{
        "id":       11,
        "name":       "jerry",
        "exp": time.Now().Add(time.Duration(maxAge)*time.Second).Unix(), // 过期时间,必须设置,
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, err := token.SignedString([]byte(SECRETKEY))
    if err!=nil {
        fmt.Println(err)
    }
    fmt.Printf("token: %v\n", tokenString)

    //解析token
    ret,err :=ParseToken(tokenString)
    if err!=nil {
        fmt.Println(err)
    }
    fmt.Printf("userinfo: %v\n", ret)
}

//解析token
func ParseToken(tokenString string)(jwt.MapClaims,error)  {
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        // Don't forget to validate the alg is what you expect:
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }

        // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
        return []byte(SECRETKEY), nil
    })
    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
        return claims,nil
    } else {
        return nil,err
    }
}

运行结果类似

token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTA5MzUzMDUsImlkIjoxMSwibmFtZSI6ImplcnJ5In0.fapE0IiOEe_TqoMCThbNTHUvgWiHPEk0rm-9uPIcvPU userinfo: map[exp:1.590935305e+09 id:11 name:jerry]

小结:

  • 服务端生成的jwt返回客户端可以存到cookie也可以存到localStorage中(相比cookie容量大),存在cookie中需加上 HttpOnly 的标记,可以防止 XSS 攻击。
  • 尽量用https带证书网址访问。
  • session和jwt没有绝对好与不好,各有其擅长的应用环境,请根据实际情况选择。

参考资料

https://github.com/guyan0319/golang_development_notes

https://godoc.org/github.com/dgrijalva/jwt-go

https://blog.csdn.net/weixin_43613053/article/details/84642140

https://www.cnblogs.com/flipped/p/12973557.html

一 什么是鉴权

在网站中,有些页面是登录后的用户才能访问的,由于http是无状态的协议,我们无法确认用户的状态(如是否登录)。这时候浏览器在访问这些页面时,需要额外传输一些用户的账户信息给后台,让后台知道该用户是否登录、是哪个用户在访问。

二 cookie

cookie是浏览器实现的技术,在浏览器中可以存储用户是否登录的凭证,每次请求都会将该凭证发送给服务器。

cookie实现鉴权步骤:

  • 用户登录成功后,后端向浏览器设置一个cookie:username=lisi
  • 每次请求,浏览器会自动把该cookie发送给服务端
  • 服务端处理请求时,从cookie中取出username,就知道是哪个用户了
  • 如果没过期,则鉴权通过,过期了,则重定向到登录页

Go中使用Cookie:

// 登录时设置cookie
expiration := time.Now()
expiration = expiration.AddDate(1, 0, 0)
cookie := http.Cookie{Name: "username", Value: "张三", Expires: expiration}
http.SetCookie(w, &cookie)

// 再次访问时,获取浏览器传递的cookie
// 获取cookie方式一
username, _ := r.Cookie("username")
// 获取cookie方式二
for _, cookie := range r.Cookies() {
    fmt.Println(cookie.Username)
}

但是这样做,风险很大,黑客很容易知道cookie中传递的内容,即用户的真实账户信息。

二 session

2.1 session原理

为了解决cookie的安全问题,基于cookie,衍生了session技术。session技术将用户的信息存储在了服务端,保证了安全,其实现步骤为:

  • 服务端设置cookie时,不再存储username,而是存储一个随机生成的字符串,比如32位的uuid,服务端额外存储一个uuid与用户名的映射
  • 用户再次请求时,会自动把cookie中的uuid带入给服务器
  • 服务器使用uuid进行鉴权

一般上述的uuid在cookie中存储的键都是sid(session_id),也就是常说的session方案,服务端此时需要额外开辟空间存储sid与用户真实信息的对应映射。

2.2 session实现

如果要手动实现session,需要注意以下方面:

  • 全局session管理器:
  • 保证sessionid 的全局唯一性
  • 为每个客户关联一个session
  • session 过期处理
  • session 的存储(可以存储到内存、文件、数据库等)

关于session数据(sid与真实用户的映射)的存储,可以存放在服务端的一个文件中,比如该session第三方库:https://github.com/gorilla/sessions

使用示例:

package main

import(
    "fmt"
    "net/http"
    "github.com/gorilla/sessions"
)

// 利用cookie方式创建session,秘钥为 mykey
var store = sessions.NewCookieStore([]byte("mykey"))

func setSession(w http.ResponseWriter, r *http.Request){
    session, _ := store.Get(r, "sid")
    session.Values["username"] = "张三"
    session.Save(r, w)
}

func profile(w http.ResponseWriter, r *http.Request){

    session, _ := store.Get(r, "sid")

    if session.Values["username"] == nil {
        fmt.Fprintf(w, `未登录,请前往 localhost:8080/setSession`)
        return
    }

    fmt.Fprintf(w, `已登录,用户是:%s`, session.Values["username"])
    return
}

func main() {

    // 访问隐私页面
    http.HandleFunc("/profile", profile)

    // 设置session
    http.HandleFunc("/setSession", setSession)

    server := http.Server{
        Addr: ":8080",
    }
    server.ListenAndServe()

}

在企业级开发中,经常使用额外的数据库redis来存储session数据。

2.3 禁用cookie时候session方案

以上方式中,生成的sid都存储在cookie中,如果用户禁用了cookie,则每次请求服务端无法收到sid!我们需要想别的办法来让浏览器的每次请求都携带上sid,常用方式是URL重写:在返回给用户的页面里的所有的URL后面追加session标识符,这样用户在收到响应之后,无论点击响应页面里的哪个链接或提交表单,都会自动带上session标识符,从而就实现了会话的保持。

三 JWT

3.1 jwt介绍

session将数据存储在了服务端,无端造成了服务端空间浪费,可否像cookie那样将用户数据存储在客户端,而不被黑客破解到呢?

JWT是json web token缩写,它将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证。
JWT和session有所不同,session需要在服务器端生成,服务器保存session,只返回给客户端sessionid,客户端下次请求时带上sessionid即可。因session是储存在服务器中,有多台服务器时会出现一些麻烦,需要同步多台主机的信息,不然会出现在请求A服务器时能获取信息,但是请求B服务器身份信息无法通过。JWT能很好的解决这个问题,服务器端不用保存jwt,只需要保存加密用的secret,在用户登录时将jwt加密生成并发送给客户端,由客户端存储,以后客户端的请求带上,由服务器解析jwt并验证。这样服务器不用浪费空间去存储登录信息,也不用浪费时间去做同步。

3.2 jwt构成

一个 JWT token包含3部分:

  • header: 告诉我们使用的算法和 token 类型
  • Payload: 必须使用 sub key 来指定用户 ID, 还可以包括其他信息比如 email, username 等.
  • Signature: 用来保证 JWT 的真实性. 可以使用不同算法

header:

// base64编码的字符串`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9`
// 这里规定了加密算法,hash256
{
  "alg": "HS256",
  "typ": "JWT"
}

payload:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
// base64编码的字符串`eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9`

这里的内容没有强制要求,因为 Payload 就是为了承载内容而存在的,不过想用规范的话也可以参考下面的:

* iss: jwt签发者
* sub: jwt所面向的用户
* aud: 接收jwt的一方
* exp: jwt的过期时间,这个过期时间必须要大于签发时间
* nbf: 定义在什么时间之前,该jwt都是不可用的.
* iat: jwt的签发时间
* jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

signature是用 header + payload + secret组合起来加密的,公式是:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

这里 secret就是自己定义的一个随机字符串,这一个过程只能发生在 server 端,会随机生成一个 hash 值,这样组合起来之后就是一个完整的 jwt 了:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.4c9540f793ab33b13670169bdf444c1eb1c37047f18e861981e14e34587b1e04

3.3 jwt执行流程

[图片上传失败...(image-c11c01-1604914142022)]

3.4 Go使用jwt

整体操作步骤:

  • 1.从request获取tokenstring
  • 2.将tokenstring转化为未解密的token对象
  • 3.将未解密的token对象解密得到解密后的token对象
  • 4.从解密后的token对象里取参数

使用第三方包:go get github.com/dgrijalva/jwt-go 示例:


// 生成Token:
// SecretKey 是一个 const 常量
func CreateToken(SecretKey []byte, issuer string, Uid uint, isAdmin bool) (tokenString string, err error) {  
    claims := &jwtCustomClaims{  
       jwt.StandardClaims{
           ExpiresAt: int64(time.Now().Add(time.Hour * 72).Unix()),  
           Issuer:    issuer,  
       },  
       Uid,  
       isAdmin,  
   }  
   token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)  
   tokenString, err = token.SignedString(SecretKey)  
   return  
}

// 解析Token
func ParseToken(tokenSrt string, SecretKey []byte) (claims jwt.Claims, err error) {  
   var token *jwt.Token  
   token, err = jwt.Parse(tokenSrt, func(*jwt.Token) (interface{}, error) {  
      return SecretKey, nil  
   })  
   claims = token.Claims  
   return  
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354