壹 介绍
前些天,被问到
JWT
的相关问题,原本以为自己了解,但是后来发现只是浅浅的认识,这几天研究了一下,其基本的安全问题和介绍,下面就是自己对JWT
的见解!
JWT
又叫(Json Web Token
),中文翻译又叫json
格式的互联网令牌,是一种开放的标准,它定义了一种紧凑且独立的方式,用于在各方之间以 JSON
对象的形式安全地传输信息。此信息可以验证和信任,因为它是经过数字签名的。JWT
可以使用密钥(使用 HMAC
算法)或使用 RSA
或 ECDSA
的公钥/私钥对进行签名(这里是百度的)。说白了,这东西就是一个用来校验的信息的准确性的,类似于Session
。
问题来了,为什么用
JWT
而不用Session
呢?
- 每个用户在认证登录之后,服务端都会在内存里做一次关于该用户
Session
认证的记录,当认证用户增多时,就会导致服务端的开销会明显增大- 如果认证后,那么该用户
Session
认证的记录就会在该服务器内保存,而如果使用分布式的应用时,每次都要来这台服务器授权一次,才能访问其他资源,这样会大大限制了负载均衡器的能力
JWT
的优势:
- 其数据量较小,传输快,并且是以
JSON
加密形式保存在客户端的,所以JWT
是跨语言的,而不像Session
是将用户登录信息保存在服务器端的 - 使用
Session
进行身份认证的话,由于cookie
无法跨域,难以实现单点登录。但是,使用token
进行认证的话,token
可以被保存在客户端的任意位置的内存中,不一定是cookie
,所以不依赖cookie
,不会存在这些问题 - 为了防止客户端的数据造假,保存在客户端的登录信息经过了签名处理,而签名的密钥只有服务器端才知道,每次服务器端收到客户端提交的登录信息的时候都要检查一下签名,如果发现数据被篡改,则拒绝接收客户端提交的令牌。这样既保证了一定的安全性,又减少了服务端的开销。
官网地址:传送门
贰 组成
一串完整的JWT
主要由三部分组成,每部分用英文句号连接(.
)连接,这三部分分别是:Header
、Payload
、Signature
,既常规的JWT
内容格式是这样的:header.payload.signature
,并且,这一串内容会进行URL
格式的base64
加密,也就是说base64
解码才可以看到实际传输的内容。下面这个就是完整的JWT
:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header
:又叫标头,其内容主要由令牌的类型(typ
,一般默认是JWT
)和所使用的签名算法(alg
,HMAC
、SHA256
或RSA
)这两部分组成,然后进行Base64
编码组成JWT
结构的第一部分。
{
"alg": "HS256",
"typ": "JWT"
}
Payload
:该部分又叫有效载荷,其内容主要保存了需要传递给服务器的实际信息内容,但是注意不应该在该部分存放敏感信息,因为客户端也可对称解密该部分信息,它相当于是暴露的
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
Signature
:该部分主要用来对前面两部分进行签名,保证JWT
不被篡改。之所以可以用来做签名,是因为该部门存在一段密钥,主要用户不知道该密钥,就破解不出来这部分内容。下面代码中的your-256-bit-secret
就是需要服务端保存的密钥。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
官方也提供了在线解密:
叁 认证过程
JWT
的认证过程如下:
- 首先,客户端通过
Web
表单将自己的用户名和密码发送到后端的接口。这一过程一般是只涉及到页面的http post
请求,不会有JWT
产生。 - 当服务端核对用户名和密码成功后,会将用户的
ID
等非敏感信息作为JWT
的Payload
,将其与头部分别进行Base64
编码拼接后签名,形成一个完整的JWT
。格式为:head.payload.singurater
。 - 接着服务端将
JWT
作为登录成功的返回结果返回给客户端,客户端就可以将返回的结果保存在localStorage
或sessionStorage
上,如果退出登录时客户端删除保存的JWT
。 - 客户端在每次请求时将
JWT
放入HTTP Header
中的Authorization
位,这一步可以有效的解决XSS
和CSRF
问题。 - 服务端检查
JWT
是否存在,既验证JWT
的有效性。如检查签名是否正确、检查Token
是否过期、检查Token
的接收方是否是自己。 - 验证通过后服务端使用
JWT
中包含的用户信息进行其他逻辑操作,返回相应结果。
肆 代码实现
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"strings"
)
// JWT数据类型
type JWT struct {
Header string
Payload string
Signature string
Data JWTData
Type JWTType
}
// 注意Alg与Typ顺序
type JWTType struct {
Alg string `json:"alg"`
Typ string `json:"typ"`
}
// 用户输入的信息
type JWTData struct {
Data string `json:"data"`
}
// 处理错误方法
func (data *JWT) handlingErrors(err error, funname string) {
if err != nil {
fmt.Println("[\033[31;1m-\033[0m]", funname, "Error:", err)
log.Panic(err)
}
}
// 签名,key值一般在服务端,不会给客户端公布
func (data *JWT) jWTSignature(key string) string {
h := hmac.New(sha256.New, []byte(key))
h.Write([]byte(data.Header + "." + data.Payload))
return data.Base64UrlSafeEncode([]byte(h.Sum(nil)))
}
// 将string转换为url_safe base64编码
func (data *JWT) Base64UrlSafeEncode(source []byte) string {
// Base64 Url Safe is the same as Base64 but does not contain '/' and '+' (replaced by '_' and '-') and trailing '=' are removed.
// 进行base64编码
bytearr := base64.StdEncoding.EncodeToString(source)
// 将/号改为_
safeurl := strings.Replace(string(bytearr), "/", "_", -1)
// 将+号改为-
safeurl = strings.Replace(safeurl, "+", "-", -1)
// 将=号去掉
safeurl = strings.Replace(safeurl, "=", "", -1)
return safeurl
}
// 将url_safe base64编码转换为string
func Base64URLDecode(data string) ([]byte, error) {
var missing = (4 - len(data)%4) % 4
data += strings.Repeat("=", missing)
return base64.URLEncoding.DecodeString(data)
}
// JWT加密函数
func (data *JWT) jWTencode(val string) string {
// 赋值
data.Data = JWTData{
Data: val,
}
// 创建JWT的Header
// 赋值
data.Type = JWTType{
Typ: "JWT",
Alg: "HS256",
}
// 反序列化
header, err := json.Marshal(data.Type)
data.handlingErrors(err, "json.Marshal")
// 进行base64编码,JWT的Header都需要进行base64
data.Header = data.Base64UrlSafeEncode(header)
// 创建JWT的Payload
// 反序列化
payload, err := json.Marshal(data.Data)
data.handlingErrors(err, "json.Marshal")
// 进行base64编码,JWT的Header都需要进行base64
data.Payload = data.Base64UrlSafeEncode(payload)
// key是key值,cry是加密算法
data.Signature = data.jWTSignature("your-256-bit-secret")
return data.Header + "." + data.Payload + "." + data.Signature
}
// JWT解密函数
func (data *JWT) jWTdecode(token string) string {
// JWT切片
jwt := strings.Split(token, ".")
if len(jwt) != 3 {
return "JWT格式不正确!"
}
// 赋值给变量
data.Header = jwt[0]
data.Payload = jwt[1]
data.Signature = jwt[2]
// 判断Signature是否正确
Signature := data.jWTSignature("your-256-bit-secret")
if Signature != data.Signature {
return "JWT签名不正确!"
}
// 转换header
header, err := Base64URLDecode(data.Header)
data.handlingErrors(err, "Base64URLDecode")
err = json.Unmarshal(header, &data.Type)
data.handlingErrors(err, "json.Unmarshal")
// 转换payload
payload, err := Base64URLDecode(data.Payload)
data.handlingErrors(err, "Base64URLDecode")
data.Payload = string(payload)
return data.Payload
}
// 主函数
func main() {
j := new(JWT)
s := j.jWTencode("11111111121")
fmt.Println(s)
fmt.Println(j.jWTdecode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiMTExMTExMTExMjEifQ.7tymjdzP5e7LnDYGNhJ1icMDRZaIf8i_B-ody5tKX_I"))
}
伍 JWT的安全性问题
敏感信息泄露
由于JWT
的payload
部分存放了用户的信息,同时可以通过Base64
解码,所以如果将敏感的信息存放在payload
中,会导致信息泄露。未校验签名
有点服务端会取消JWT
的signature
部分校验,我们可以通过直接修改JWT
前面的两部分进行测试,看其是否有效。签名算法可被修改为
none
有的时候signature
部分的加密认证取决与header
部分的签名算法,这时候我们如果将header
的签名算法改成none
形式,那么服务端接收到JWT
后会将其认定为无加密算法, 于是对signature
的检验也就失效了,那么我们就可以随意修改payload
部分伪造用户信息。用none
算法生成的JWT
只有两部分了,连签名都不存在。签名密钥爆破
JWT
的安全性很大程度取决于加密的密钥,我们如果可以通过爆破加密密钥,获得正确的密钥,那么我们就可以随意修改JWT
中payload
部分信息了。JWT
爆破脚本:https://github.com/Ch1ngg/JWTPyCrack修改非对称密码算法为对称密码算法
JWT
的签名加密算法有两种,对称加密算法和非对称加密算法。对称加密算法比如HS256
,加解密使用同一个密钥,保存在服务端。非对称加密算法比如RS256
,后端加密使用私钥,前端解密使用公钥,公钥是我们可以获取到的。如果我们修改header
,将算法从RS256
更改为HS256
,后端代码会使用RS256
的公钥作为HS256
算法的密钥。于是我们就可以用RS256
的公钥伪造数据。伪造密钥(
CVE-2018-0114
)JWK
是header
里的一个参数,用于指出密钥,存在被伪造的风险。比如:CVE-2018-0114攻击者可以通过以下方法来伪造JWT
:删除原始签名,向标头添加新的公钥,然后使用与该公钥关联的私钥进行签名。(这个安全性问题说实话我不太理解,我在想如果直接不要这个字段不就可以避免这个漏洞了吗?)
对于验证JWT
的安全性,可以使用该工具:jwt_tool
JWT的使用建议:
- 不要存放敏感信息在Token里。
- Payload中的exp时效不要设定太长
- 开启Only Http预防XSS攻击
- 如果担心重播攻击(replay attacks)可以增加jti(JWT ID),exp(有效时间)Claim
- 在你的应用程序应用层中增加黑名单机制,必要的时候可以进行Block做阻挡(这是针对掉令牌被第三方使用窃取的手动防御)