Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).
该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9**.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.****UQmqAUhUrpDVV2ST7mZKyLTomVfg7sYkEjmdDI5XF8Q
**
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
header
jwt的头部承载两部分信息:
完整的头部就像下面这样的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
playload
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
标准中注册的声明 (建议但不强制使用) :
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后将其进行base64加密,得到Jwt的第二部分
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串(头部在前),然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
UQmqAUhUrpDVV2ST7mZKyLTomVfg7sYkEjmdDI5XF8Q
密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和验证,所以需要保护好。
最后一步签名的过程,实际上是对头部以及载荷内容进行签名。一般而言,加密算法对于不同的输入产生的输出总是不一样的。对于两个不同的输入,产生同样的输出的概率极其地小(有可能比我成世界首富的概率还小)。所以,我们就把“不一样的输入产生不一样的输出”当做必然事件来看待吧。
所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。
服务器应用在接受到JWT后,会首先对头部和载荷的内容用同一算法再次签名。那么服务器应用是怎么知道我们用的是哪一种算法呢?别忘了,我们在JWT的头部中已经用alg字段指明了我们的加密算法了。
如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们应该拒绝这个Token,返回一个HTTP 401 Unauthorized响应。
注意:在JWT中,不应该在载荷里面加入任何敏感的数据,比如用户的密码。
一般是在请求头里加入Authorization
,并加上Bearer
标注:
fetch('api/user/1', {
headers: {
'Authorization': 'Bearer ' + token
}
})
服务端会验证token,如果验证通过就会返回相应的资源。 token 在http请求的头部咯
我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。
但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来。
基于session认证所显露的问题
Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
基于token的鉴权机制
基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
流程上是这样的:
这个token必须要在每次请求时传递给服务端,保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)
策略,一般我们在服务端这么做就可以了 Access-Control-Allow-Origin:*。
前面说到过session,cookie以及token的区别,在之前传统的做法就是基于存储在服务器上的session来做用户的身份认证,但是通常会有如下问题:
都可以存储用户相关信息,但是session存储在服务端,JWT存储在客户端
3.基于Token的身份认证如何工作
基于Token的身份认证是无状态的,服务器或者session中不会存储任何用户信息.(很好的解决了共享session的问题)
注意:
Access-Control-Allow-Origin: *
4.用Token的好处
5.JWT和OAuth的区别
使用第三方账号登录的情况
(比如使用weibo, qq, github登录某个app),而JWT是用在前后端分离
, 需要简单的对后台API进行保护时使用在Golang语言中,**jwt-go**库提供了一些jwt编码和验证的工具,因此我们很容易使用该库来实现token认证。
另外,我们也知道**gin**框架中支持用户自定义middleware,我们可以很好的将jwt相关的逻辑封装在middleware中,然后对具体的接口进行认证。
在gin框架中,自定义中间件比较容易,只要返回一个gin.HandlerFunc
即完成一个中间件定义。
Header(默认标识, 和加密算法)
Claims(载荷)
jwt.StandardClaims {
Audience string `json:"aud,omitempty"` //token接收者
ExpiresAt int64 `json:"exp,omitempty"` //过期时间
Id string `json:"jti,omitempty"` //自定义id号
IssuedAt int64 `json:"iat,omitempty"` //签名发行时间
Issuer string `json:"iss,omitempty"` //签名发行者
NotBefore int64 `json:"nbf,omitempty"` //token信息生效时间
Subject string `json:"sub,omitempty"` //签名面向的用户
}
Signature(加密签名)
package middleware
import (
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"net/http"
"strings"
"time"
"yang_blog/utils"
"yang_blog/utils/errmsg"
)
var JwtKey = []byte(utils.JwtKey) //用户设置的jwt密钥
var code int //用户定义的返回错误消息请求码
type MyClaims struct {
Username string `json:"username"` //请求的用户名
Password string `json:"password"` //请求的密码
jwt.StandardClaims // 标准结构体
} //接收请求
//生成token 输入参数用户名和密码生成token
func SetToken(username string, password string) (string, int) {
expireTime := time.Now().Add(10 * time.Hour)
setClams := MyClaims{
Username: username,
Password: password,
StandardClaims: jwt.StandardClaims{
IssuedAt: Issuer.Unix(), //传入生成的时间
Issuer: "ginblog",
},
}
//token 方法 SignedString
reqClaim := jwt.NewWithClaims(jwt.SigningMethodHS256, setClams)
token, err := reqClaim.SignedString(JwtKey)
if err != nil {
return "", errmsg.ERROR
}
return token, errmsg.SUCCESS
}
//验证token
func CheckToken(token string) (*MyClaims, int) {
setToken, _ := jwt.ParseWithClaims(token, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
return JwtKey, nil
})
if key, ok := setToken.Claims.(*MyClaims); ok && setToken.Valid {
return key, errmsg.SUCCESS
}
return nil, errmsg.ERROR
}
//jwt 中间件
func JwtToken() gin.HandlerFunc {
return func(c *gin.Context) {
tokenHerder := c.Request.Header.Get("Authorization")
code = errmsg.SUCCESS
if tokenHerder == "" { //令牌不存在
code = errmsg.ERROR_TOKEN_NOT_EXIST //令牌不存在
c.JSON(http.StatusOK, gin.H{
"code": code,
"message": errmsg.GetErrMsg(code),
})
c.Abort()
return
}
//checkToken := strings.SplitN(tokenHerder, "", 2)
checkToken := strings.Split(tokenHerder, " ") //令牌格式错误
if len(checkToken) != 2 && checkToken[0] != "Bearer" {
code = errmsg.ERROR_TOKEN_TPYE_WRONG
c.JSON(http.StatusOK, gin.H{
"code": code,
"message": errmsg.GetErrMsg(code),
})
c.Abort()
return
}
key, tCode := CheckToken(checkToken[1]) //令牌错误
if tCode == errmsg.ERROR {
code = errmsg.ERROR_TOKEN_WRONG
c.JSON(http.StatusOK, gin.H{
"code": code,
"message": errmsg.GetErrMsg(code),
})
c.Abort()
return
}
if time.Now().Unix() > key.ExpiresAt { //令牌过期
code = errmsg.ERROR_TOKEN_RUNTIME
c.JSON(http.StatusOK, gin.H{
"code": code,
"message": errmsg.GetErrMsg(code),
})
c.Abort()
return
}
c.Set("username", key.Username)
c.Next() // 继续访问后续/api/v1
}
}
": errmsg.GetErrMsg(code),
})
c.Abort()
return
}
if time.Now().Unix() > key.ExpiresAt { //令牌过期
code = errmsg.ERROR_TOKEN_RUNTIME
c.JSON(http.StatusOK, gin.H{
“code”: code,
“message”: errmsg.GetErrMsg(code),
})
c.Abort()
return
}
c.Set("username", key.Username)
c.Next() // 继续访问后续/api/v1
}
}