- 在线解析Token:https://jwt.io/
JWT(JSON Web Token)是为了在网路应用环境之间传递声明而执行的一种基于JSON的开放标准(RFC7519)JWT是一个非常轻巧的规范,该规范允许使用Token在客户端和服务端之间传递安全可靠的信息。
JWT的Token被设计为紧凑且安全的,因此特别适用于分布式站点的单点登录(SSO)场景。JWT的Token可直接被用于认证,也可被加密。
JWT定义了一种紧凑且自包含的方式,用于在各方之间以JSON方式安全地传输信息。信息经过数字签名因此可被验证和信任。可使用HMAC算法或使用RSA/ECDSA的公钥/私钥对JWT进行签名。
JWT的声明一般被用来在身份提供者和服务提供者之间传递被认证的用户身份信息,以便从资源服务器获取资源,也可额外额外业务逻辑所必须的声明信息。
JWT原理类似加盖公章或手写签名的过程,合同上写了很多条款,不是随便一张纸随便写啥都可以的,必须要有一些证明,比如签名、盖章。JWT就是通过附加签名来保证传输过来的信息是真的,而非伪造的。
认证
由于HTTP是一种无状态的协议,因此会涉及到用户访问系统的状态保持问题。
常见的认证方式可分为两种,分别为基于Session和基于Token的。
基于Session的认证方式
传统的授权认证一般会采用Session,由于Session存储在服务端,会加大服务端的计算量。而多台服务器之间存在Session同步的问题。JWT存储在客户端,不仅减少了服务端的计算量,而且天生支持分布式。
另外,由于Session受到单台服务器的限制,一个用户登录过后旧只能分配到这一台服务器上。对于使用负载均衡的服务器来说,使用JWT则是一种更优的选择。
- 客户端向服务端发送用户名和密码
- 服务器验证通过后在当前会话中保存用户数据,比如用户角色、登录时间等。
- 服务端向用户返回一个session_id,写入客户端的Cookie或其它存储中。
- 客户端随后的每一次请求都会通过Cookie将session_id传回到服务端
- 服务端接收到session_id,同时找到前期保存的数据,由此得知客户端身份。
- 客户端退出登录,服务端将对应的session_id的数据清除。
此种方式需将session_id及相关数据保存在服务端(也可存储到Redis中),在接收到客户端请求时进行校验。
用户的Session数据以文件或缓存的方式存储在服务端,客户端浏览器Cookie中只保存session_id,因此服务端Session属于集中存储。当数量不大的情况下没有什么问题,一旦当用户数量增多到一定程度时就会给服务端管理和维护带来巨大的负担。首先Session无法实现跨域,另外集中式管理中服务器性能也是一个问题。
基于Token的认证方式
- 客户端向服务端发送用户名和密码
- 服务端将相关数据,比如用户ID、认证有效期等信息签名后生成Token返还给客户端。
- 客户端将Token写入本地存储
- 客户端随后每一次请求都会将本地Token附加到Header中
- 服务端获取到客户端请求头Header,拿到数据并做签名校验。若校验成功则说明数据没有被篡改,若确认Token在有效期内则认为是有效的。
JWT是基于Token的认证方式,JWT采用JSON传输,无跨域问题,传输数据通过了数据签名相对比较安全。JWT会将用户信息加密到Token中,服务端不保存任何用户信息,服务端通过使用保存的密钥验证Token的正确性,只要正确即通过验证。
场景
授权(Authorization)
JWT是一种用户认证(区别于Session、Cookie)的解决方案,客户端和服务端通过解密Token信息来实现用户认证,无需服务器集中维护Token信息,便于扩展。
一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务、资源。单点登录广泛地使用了JWT,因为开销很小且可轻松地跨域使用。
信息交换(Information Exchange)
对于安全地在各方之间传递的信息,JWT是一种较好的方式。因为JWT可以被签名。比如使用公钥和私钥对可以确定发送人就是它们所说的哪个人。另外由于签名使用Header和有效负载计算过,因此可以验证内容是否被篡改过。
OAuth
JWT和OAuth的区别
- OAuth是一种授权框架,JWT是一种认证协议。
- 均可采用HTTPS来确保数据的安全性
- OAuth使用在第三方登录,JWT使用在前后端分离中对后端API进行保护。
原理
JWT- 服务端生成JWT返回给客户端时放入Cookie,并添加上HttpOnly标记,使Cookie不能被JavaScript获取以防止XSS攻击。
- 客户端传输JWT时Header中加入
Authorization
字段(使用Bearer模式)以避免CORS攻击。
Authorization: Bear eyJhbGciO...
构成
构成 | 名称 | 描述 |
---|---|---|
Header | 头部 | Token类型和签名算法(HMAC SHA256、HS384)。 |
Payload | 载荷(Claim) | 承载的内容或携带的信息,如用户名、过期时间等。 |
Signature | 签名 | 由Header、Payload、自己维护的一个Secret经过加密得来。 |
Signature会将Header和Payload进行Base64编码后,使用Header中声明的加密算法加盐(Serect)生成。由于是Base64编码相当于明文可见,因此不要在Payload中放入敏感信息。
缺点
- 用户无法主动登出,只要Token在有效期内就有效。可考虑Redis设置同Token有效期一致的黑名单解决此问题。
- Token过了有效期无法续签问题,可通过判断旧的Token什么到期,过期的时候刷新Token续签接口来产生新的Token以替代旧的Token。
有效期
JWT的令牌由于是无状态的,任何获取到Token的人都可以访问。为了减少盗用,可将Token有效期设置的短一些。对于重要的操作,尽量再次认证。另外要尽量使用HTTPS以减少Token的泄露。
Token设置有效期是为了增加安全性,Token被黑客截获也只能攻击较短时间,但设计有效期会面临Token续签问题。其解决方案通常会在服务端设置两个Token:
Token | 描述 |
---|---|
Access Token | 添加到HTTP请求的Header中,进行用户认证,请求接口资源。 |
Refresh Token | 当Access Token过期后,客户端传递Refresh Token刷新Access Token续期接口,获取新的Access Token和Refresh Token。其有效期比Access Token有效期更长。 |
jwt-go
下载安装
$ go get -u github.com/dgrijalva/jwt-go
使用
$ vim middleware/jwt.go
定义Signature签名加密的私钥(salt)
//JSW Secret 私钥
var JwtSecret = config.Get("server.JwtSecret", "1a4e5c23k")
继承jwt
提供给载荷,扩展自己所需字段。
//Payload 载荷
type JwtClaims struct{
jwt.StandardClaims
Username string `json:"username"` //账户
Password string `json:"password"` //密码
}
生成JWT令牌
//生成JSON Web Token
func CreateJwt(username, password string, timeout int) (string, bool){
//过期时间
expiresAt := time.Now().Add(time.Hour * time.Duration(timeout)).Unix()
//设置载荷
claims := JwtClaims{}
claims.Username = username
claims.Password = password
claims.ExpiresAt = expiresAt
//claims.Issuer = "ginv"// 非必须,也可以填充用户名
//生成令牌 采用HMAC SHA256算法加密
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
//令牌签名
tokenString,err := token.SignedString([]byte(JwtSecret))
if err!=nil {
return "", false
}
return tokenString, true
}
验证JWT令牌字符串
//验证JSON Web Token
func ValidateJwt(tokenString string) (*JwtClaims, bool){
//解析令牌字符串
token,err := jwt.ParseWithClaims(tokenString, &JwtClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(JwtSecret), nil
})
if err!=nil {
log.Println(err)
return nil, false
}
//获取载荷
claims,ok := token.Claims.(*JwtClaims)
if ok && token.Valid{
return claims, true
}
return nil, false
}
创建中间件方法
//JSON Web Token中间件
func Jwt() gin.HandlerFunc{
return func(ctx *gin.Context){
code := proto.SUCCESS
//获取请求头中的Authorization
authorization := ctx.Request.Header.Get("Authorization")
if authorization == "" {
code = proto.ERROR_AHTHORIZATION_EMPTY
}
//拆分Authorization字段获取token字符串
strSlice := strings.SplitN(authorization, " ", 2)
if len(strSlice)!=2 && strSlice[0]!="Bearer"{
code = proto.ERROR_TOKEN_EMPTY
ctx.Abort()
}
//验证token字符串
claim,ok := ValidateJwt(strSlice[1])
if !ok {
code = proto.ERROR_TOKEN_VALIDATE
ctx.Abort()
}
//过期判断
if time.Now().Unix() > claim.ExpiresAt {
code = proto.ERROR_TOKEN_EXPIRED
ctx.Abort()
}
ctx.JSON(http.StatusOK, gin.H{"code":code, "message":proto.GetCodeMessage(code)})
//设置用户名
ctx.Set("username", claim.Username)
ctx.Next()
}
}
网友评论