LOADING

正在加载

密码学——JWT介绍与其安全问题

壹 介绍

前些天,被问到JWT的相关问题,原本以为自己了解,但是后来发现只是浅浅的认识,这几天研究了一下,其基本的安全问题和介绍,下面就是自己对JWT的见解!

JWT又叫(Json Web Token),中文翻译又叫json格式的互联网令牌,是一种开放的标准,它定义了一种紧凑且独立的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。此信息可以验证和信任,因为它是经过数字签名的。JWT 可以使用密钥(使用 HMAC 算法)或使用 RSAECDSA 的公钥/私钥对进行签名(这里是百度的)。说白了,这东西就是一个用来校验的信息的准确性的,类似于Session

问题来了,为什么用JWT而不用Session呢?

  • 每个用户在认证登录之后,服务端都会在内存里做一次关于该用户Session认证的记录,当认证用户增多时,就会导致服务端的开销会明显增大
  • 如果认证后,那么该用户Session认证的记录就会在该服务器内保存,而如果使用分布式的应用时,每次都要来这台服务器授权一次,才能访问其他资源,这样会大大限制了负载均衡器的能力

JWT的优势:

  • 其数据量较小,传输快,并且是以JSON加密形式保存在客户端的,所以JWT是跨语言的,而不像Session是将用户登录信息保存在服务器端的
  • 使用Session进行身份认证的话,由于cookie无法跨域,难以实现单点登录。但是,使用token进行认证的话,token可以被保存在客户端的任意位置的内存中,不一定是cookie,所以不依赖cookie,不会存在这些问题
  • 为了防止客户端的数据造假,保存在客户端的登录信息经过了签名处理,而签名的密钥只有服务器端才知道,每次服务器端收到客户端提交的登录信息的时候都要检查一下签名,如果发现数据被篡改,则拒绝接收客户端提交的令牌。这样既保证了一定的安全性,又减少了服务端的开销。

官网地址:传送门

贰 组成

一串完整的JWT主要由三部分组成,每部分用英文句号连接(.)连接,这三部分分别是:HeaderPayloadSignature,既常规的JWT内容格式是这样的:header.payload.signature,并且,这一串内容会进行URL格式的base64加密,也就是说base64解码才可以看到实际传输的内容。下面这个就是完整的JWT

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  • Header:又叫标头,其内容主要由令牌的类型(typ,一般默认是JWT)和所使用的签名算法(algHMACSHA256RSA)这两部分组成,然后进行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
)

官方也提供了在线解密:
bcbf143257faf81b4b7d85f75db64e7b.png

叁 认证过程

JWT的认证过程如下:

  • 首先,客户端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是只涉及到页面的http post请求,不会有JWT产生。
  • 当服务端核对用户名和密码成功后,会将用户的ID等非敏感信息作为JWTPayload,将其与头部分别进行Base64编码拼接后签名,形成一个完整的JWT。格式为: head.payload.singurater
  • 接着服务端将JWT作为登录成功的返回结果返回给客户端,客户端就可以将返回的结果保存在localStoragesessionStorage上,如果退出登录时客户端删除保存的JWT
  • 客户端在每次请求时将JWT放入HTTP Header中的Authorization位,这一步可以有效的解决XSSCSRF问题。
  • 服务端检查JWT是否存在,既验证JWT的有效性。如检查签名是否正确、检查Token是否过期、检查Token的接收方是否是自己。
  • 验证通过后服务端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

7f6c1849756f128c0e72a9c6821fb82a.png

肆 代码实现

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的安全性问题

  • 敏感信息泄露
    由于JWTpayload部分存放了用户的信息,同时可以通过Base64解码,所以如果将敏感的信息存放在payload中,会导致信息泄露。

  • 未校验签名
    有点服务端会取消JWTsignature部分校验,我们可以通过直接修改JWT前面的两部分进行测试,看其是否有效。

  • 签名算法可被修改为none
    有的时候signature部分的加密认证取决与header部分的签名算法,这时候我们如果将header的签名算法改成none形式,那么服务端接收到JWT后会将其认定为无加密算法, 于是对signature的检验也就失效了,那么我们就可以随意修改payload部分伪造用户信息。用none算法生成的JWT只有两部分了,连签名都不存在。

  • 签名密钥爆破
    JWT的安全性很大程度取决于加密的密钥,我们如果可以通过爆破加密密钥,获得正确的密钥,那么我们就可以随意修改JWTpayload部分信息了。JWT爆破脚本:https://github.com/Ch1ngg/JWTPyCrack

  • 修改非对称密码算法为对称密码算法
    JWT的签名加密算法有两种,对称加密算法和非对称加密算法。对称加密算法比如HS256,加解密使用同一个密钥,保存在服务端。非对称加密算法比如RS256,后端加密使用私钥,前端解密使用公钥,公钥是我们可以获取到的。如果我们修改header,将算法从RS256更改为HS256,后端代码会使用RS256的公钥作为HS256算法的密钥。于是我们就可以用RS256的公钥伪造数据。

  • 伪造密钥(CVE-2018-0114
    JWKheader里的一个参数,用于指出密钥,存在被伪造的风险。比如:CVE-2018-0114攻击者可以通过以下方法来伪造JWT:删除原始签名,向标头添加新的公钥,然后使用与该公钥关联的私钥进行签名。(这个安全性问题说实话我不太理解,我在想如果直接不要这个字段不就可以避免这个漏洞了吗?)

对于验证JWT的安全性,可以使用该工具:jwt_tool

JWT的使用建议:

  • 不要存放敏感信息在Token里。
  • Payload中的exp时效不要设定太长
  • 开启Only Http预防XSS攻击
  • 如果担心重播攻击(replay attacks)可以增加jti(JWT ID),exp(有效时间)Claim
  • 在你的应用程序应用层中增加黑名单机制,必要的时候可以进行Block做阻挡(这是针对掉令牌被第三方使用窃取的手动防御)

陆 参考

avatar
小C&天天

修学储能 先博后渊


今日诗句