golang使用JWX进行认证和加密

golang,使用,jwx,进行,认证,加密 · 浏览次数 : 430

小编点评

{ v, err := jwk.ReadFile(`private-key.pem`, jwk.WithPEM(true)) if err != nil { // handle error }}{ v, err := jwk.ReadFile(`public-key.pem`, jwk.WithPEM(true)) if err != nil { // handle error }}\tsrv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\t\tw.WriteHeader(http.StatusOK)\t\tfmt.Fprintf(w, `{ \t\t\"keys\": [ {\"kty\":\"EC\", \"crv\":\"P-256\", \"x\":\"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4\", \"y\":\"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM\", \"use\":\"enc\", \"kid\":\"1\"}, {\"kty\":\"RSA\", \"n\": \"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw\", \"e\":\"AQAB\", \"alg\":\"RS256\", \"kid\":\"2011-04-29\"} ] }`)\t}))\tdefer srv.Close()\tset, err := jwk.Fetch(\t\tcontext.Background(),\t\tsrv.URL,\t\t// This is necessary because httptest.Server is using a custom certificate\t\tjwk.WithHTTPClient(srv.Client()),\t)\tif err != nil {\t\tfmt.Printf(\"failed to fetch JWKS: %s\\", err)\t\treturn\t}

正文

golang使用JWX进行认证和加密

image

最近看了一个名为go-auth的库,它将JWT作为HTTP cookie对用户进行验证,但这个例子中缺少了对JWT的保护,由此进行了一些针对JWX的研究。

下面描述来自golang-jwt的官方描述:

概述

JWT是一个签名的JSON对象,通常用作Oauth2的Bearer token,JWT包括三个用.分割的部分:前两部分为使用base64url编码的JSON对象,最后一部分是签名。第一部分称为header,包含用于验证最后一部分签名所需的信息,如使用的签名方式和使用的密钥等,中间的部分是程序最关心的部分,称为ClaimRFC 7519定义了相关的字段,当然也可以添加自己的字段。

签名 vs 加密

一个token是一个签名的json对象,涉及两方面的内容:

  • token的创建者拥有签名的secret
  • 数据一旦被签名就不能修改

需要注意的是,JWT并不支持加密,因此任何人都能读取token 的内容。如果需要加密数据,可以使用配套的规范--JWE,可以参考这两个库:lestrrat-go/jwxgolang-jwt/jwe

选择签名方法

签名方法有很多种,在使用前可能需要花时间挑选合适的签名方法,主要考量点为:对称和非对称。

对称签名方法,如HSA,只需要一个secret即可,这也是最简单的签名方法,可以使用任何[]byte作为有效的secret。对称加密的计算速度也相对快一些。当token的生产者和消费者都可信的前提下,可以考虑使用对称加密。由于对称加密使用相同的secret进行token的签名和验证,因此不能轻易将密钥分发出去。

非对称签名,如RSA,则使用了不同的密钥进行签名和token验证,因此可以使用私钥生成token,并允许消费者使用公钥进行验证。

JWT和OAuth

这里提一下,OAuth和JWT并不是一回事,一个JWT token只是一个简单的被签名的JSON对象,可以用在所需要的地方,最常见的方式是用在OAuth2认证中。

下面描述了二者是如何交互的:

  • OAuth是一种允许身份提供者与用户登录的服务分离的协议。例如,你可以使用Facebook登陆不同的服务(Yelp、Spotify等),此时用的就是OAuth。
  • OAuth定义了几种传递身份验证数据的选项,其中流行的一种方式称为"bearer token",一个bearer token就是一个只能被已认证的用户持有的简单字符串,即通过提供该token来进行身份认证。从这里可以看出JWT可以作为一种bearer token。
  • 由于bearer token用于认证,因此私密性很重要,这也是为什么通常会通过SSL来使用bearer token。

JWT的用法

从上面的官方描述中可以看到JWT其实就是一个字符串,其分为三段:header,主要指定签名方法;claim,用于提供用户身份数据;signature,使用header中指定的签名方法进行签名,签名时主要使用了三个基础数据:

  1. 签名密钥:在对称签名(如HMAC)中作为哈希数据的一部分,在非对称签名(如ECDSA)中则作为私钥。在JWT的签名和验证过程中都需要使用到密钥。

  2. JWT的过期时间:JWT有一个过期时间。在用户登陆服务器之后,服务器会给客户端返回JWT,当客户端服务服务端时会将JWT传递给服务端,服务端除了需要验证客户端的签名之外还需要验证该token是否过期,JWT的过期时间数据位于claims中。

  3. claim:主要包含了JWT相关的信息,用户可以扩展自己的claim信息。签名方法会使用这部分信息进行签名。如下是标准的claims,可以看到这部分信息其实与SSL证书中的字段雷同。

    type StandardClaims struct {
    	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"`
    }
    

另外需要注意的是,JWT是使用明文交互的,其中claim中包含了用户的敏感信息,因此需要使用JWE进行加密。

在了解JWT之前可以看下几个重要的术语:

  • JWS(SignedJWT):经过签名的jwt,为三段式结构:headerclaimssignature

  • JWA:签名算法,即 header中的alg字段值。

  • JWE(EncryptedJWT):用于加密payload,如JWT,主要字段如下:

    const (
    	AgreementPartyUInfoKey    = "apu" #(Algorithm) Header Parameter 
    	AgreementPartyVInfoKey    = "apv"
    	AlgorithmKey              = "alg" #(Algorithm) Header Parameter 
    	CompressionKey            = "zip" #(Compression Algorithm) Header Parameter
    	ContentEncryptionKey      = "enc" #(Encryption Algorithm) Header Parameter
    	ContentTypeKey            = "cty" #(Content Type) Header Parameter
    	CriticalKey               = "crit" #(Critical) Header Parameter
    	EphemeralPublicKeyKey     = "epk"
    	JWKKey                    = "jwk" #(JSON Web Key) Header Parameter
    	JWKSetURLKey              = "jku" #(JWK Set URL) Header Parameter
    	KeyIDKey                  = "kid" #(Key ID) Header Parameter
    	TypeKey                   = "typ" #(Type) Header Parameter
    	X509CertChainKey          = "x5c" #(X.509 Certificate Chain) Header Parameter
    	X509CertThumbprintKey     = "x5t" #(X.509 Certificate SHA-1 Thumbprint) Header Parameter 
    	X509CertThumbprintS256Key = "x5t#S256" #(X.509 Certificate SHA-256 Thumbprint) Header Parameter
    	X509URLKey                = "x5u" #(X.509 URL) Header Parameter
    )
    
  • JWK:是一个JSON数据结构,用于JWS的签名验证以及JWE的加解密,主要字段如下:

    const (
    	KeyTypeKey                = "kty"      #(Key Type) Parameter
    	KeyUsageKey               = "use"      #(Public Key Use) Parameter
    	KeyOpsKey                 = "key_ops"  #(Key Operations) Parameter
    	AlgorithmKey              = "alg"      #(Algorithm) Parameter
    	KeyIDKey                  = "kid"      #(Key ID) Parameter
    	X509URLKey                = "x5u"      #(X.509 URL) Parameter
    	X509CertChainKey          = "x5c"      #(X.509 Certificate Chain) Parameter
    	X509CertThumbprintKey     = "x5t"      #(X.509 Certificate SHA-1 Thumbprint) Parameter 
    	X509CertThumbprintS256Key = "x5t#S256" #(X.509 Certificate SHA-256 Thumbprint) Parameter
    )
    

例子

lestrrat-go库中给出了很多例子。在使用该库之前简单看下主要的函数:

  • jwt.NewBuilder:创建一个表示JWT 的结构体(也可以使用jwt.New创建):

    type stdToken struct {
    	mu            *sync.RWMutex
    	dc            DecodeCtx          // per-object context for decoding
    	options       TokenOptionSet     // per-object option
    	audience      types.StringList   // https://tools.ietf.org/html/rfc7519#section-4.1.3
    	expiration    *types.NumericDate // https://tools.ietf.org/html/rfc7519#section-4.1.4
    	issuedAt      *types.NumericDate // https://tools.ietf.org/html/rfc7519#section-4.1.6
    	issuer        *string            // https://tools.ietf.org/html/rfc7519#section-4.1.1
    	jwtID         *string            // https://tools.ietf.org/html/rfc7519#section-4.1.7
    	notBefore     *types.NumericDate // https://tools.ietf.org/html/rfc7519#section-4.1.5
    	subject       *string            // https://tools.ietf.org/html/rfc7519#section-4.1.2
    	privateClaims map[string]interface{} //用户自定义的claims
    }
    
  • jwt.Sign:用于对JWT 进行签名,输入为表示JWT元素的stdToken,输出为[]byte

  • jwt.Parse:将签名的token解析为stdToken,输入为jwt.Sign的输出。

  • jws.sign:使用字符串来创建JWS消息,入参为[]byte。与jwt.Sign的不同点在于,前者的入参是stdToken标准结构体,而后者是任意字符串。

  • jws.parse:对编码的JWS消息进行解码,输出结构如下:

    type Message struct {
    	dc         DecodeCtx
    	payload    []byte
    	signatures []*Signature
    	b64        bool // true if payload should be base64 encoded
    }
    
  • jwe.Encrypt:加密payload

  • jwe.Decrypt:解密payload

下面看下如何生成JWT,以及如何结合使用JWE和JWK对其进行加密。

Example 1

下面jwt使用对称方式进行签名/解析,jwe使用非对称方式进行加解密

package main

import (
	"crypto/rand"
	"crypto/rsa"
	"fmt"
	"github.com/lestrrat-go/jwx/v2/jwa"
	"github.com/lestrrat-go/jwx/v2/jwe"
	"github.com/lestrrat-go/jwx/v2/jwk"
	"github.com/lestrrat-go/jwx/v2/jws"
	"github.com/lestrrat-go/jwx/v2/jwt"
	"time"
)

func main() {
	// 创建一个jwt token结构体
	tok, err := jwt.NewBuilder().
		Issuer(`github.com/lestrrat-go/jwx`).
		IssuedAt(time.Now()).
		Build()
	if err != nil {
		fmt.Printf("failed to build token: %s\n", err)
		return
	}

	//创建对称签名的key
	key, err := jwk.FromRaw([]byte(`abracadabra`))
	if err != nil {
		fmt.Printf(`failed to create new symmetric key: %s`, err)
		return
	}
	key.Set(jws.KeyIDKey, `secret-key`)
	//使用HS256对称签名方式进行签名,生成JWS
	signed, err := jwt.Sign(tok, jwt.WithKey(jwa.HS256, key))

	//下面使用jwe对JWS进行加密,使用的是非对称加密方式
	//首先生成RSA密钥对
	rawprivkey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		fmt.Printf("failed to create raw private key: %s\n", err)
		return
	}
	//提取私钥,用于解密
	privkey, err := jwk.FromRaw(rawprivkey)
	if err != nil {
		fmt.Printf("failed to create private key: %s\n", err)
		return
	}
	//提取公钥,用于加密
	pubkey, err := privkey.PublicKey()
	if err != nil {
		fmt.Printf("failed to create public key:%s\n", err)
		return
	}
	//使用公钥加密JWS
	encrypted, err := jwe.Encrypt(signed, jwe.WithKey(jwa.RSA_OAEP, pubkey))
	if err != nil {
		fmt.Printf("failed to encrypt payload: %s\n", err)
		return
	}
	//使用私钥解密出JWS
	decrypted, err := jwe.Decrypt(encrypted, jwe.WithKey(jwa.RSA_OAEP, privkey))
	if err != nil {
		fmt.Printf("failed to decrypt payload: %s\n", err)
		return
	}

	//使用对称签名方式解析出token 结构体
	parsedTok, err := jwt.Parse(decrypted, jwt.WithKey(jwa.HS256, key), jwt.WithValidate(true))
	if err != nil {
		fmt.Println("failed to parse signed token")
		return
	}
	fmt.Println(parsedTok)
}

Example 2

下面使用非对称方式进行签名/解析:

package main

import (
	"crypto/rand"
	"crypto/rsa"
	"fmt"
	"github.com/lestrrat-go/jwx/v2/jwa"
	"github.com/lestrrat-go/jwx/v2/jwt"
)

func main() {
	//创建RSA密钥对
	privKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		fmt.Printf("failed to generate private key: %s\n", err)
		return
	}

	var payload []byte
	{	// 创建JWT payload
		token := jwt.New()
		token.Set(`foo`, `bar`)
		//使用RSA私钥进行签名
		payload, err = jwt.Sign(token, jwt.WithKey(jwa.RS256, privKey))
		if err != nil {
			fmt.Printf("failed to generate signed payload: %s\n", err)
			return
		}
	}

	{	// 使用RSA公钥进行解析
		token, err := jwt.Parse(
			payload,
			jwt.WithValidate(true),
			jwt.WithKey(jwa.RS256, &privKey.PublicKey),
		)
		if err != nil {
			fmt.Printf("failed to parse JWT token: %s\n", err)
			return
		}

		fmt.Println(token)
	}
}

Example 3

上面使用的JWK是使用代码生成的,也可以加载本地文件(jwk.ReadFile)或通过JSU的方式从网络上拉取所需的JWK(jwk.Fetch)。

lestrrat-go的官方文档中给出了很多指导。

{
  v, err := jwk.ReadFile(`private-key.pem`, jwk.WithPEM(true))
  if err != nil {
    // handle error
  }
}

{
  v, err := jwk.ReadFile(`public-key.pem`, jwk.WithPEM(true))
  if err != nil {
    // handle error
  }
}
	srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		fmt.Fprintf(w, `{
  		"keys": [
        {"kty":"EC",
         "crv":"P-256",
         "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
         "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
         "use":"enc",
         "kid":"1"},
        {"kty":"RSA",
         "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
         "e":"AQAB",
         "alg":"RS256",
         "kid":"2011-04-29"}
      ]
    }`)
	}))
	defer srv.Close()

	set, err := jwk.Fetch(
		context.Background(),
		srv.URL,
		// This is necessary because httptest.Server is using a custom certificate
		jwk.WithHTTPClient(srv.Client()),
	)
	if err != nil {
		fmt.Printf("failed to fetch JWKS: %s\n", err)
		return
	}

参考

与golang使用JWX进行认证和加密相似的内容:

golang使用JWX进行认证和加密

## golang使用JWX进行认证和加密 ![image](https://img2023.cnblogs.com/blog/1334952/202307/1334952-20230719094038816-1830029005.png) 最近看了一个名为[go-auth](https://git

golang如何使用指针灵活操作内存?unsafe包原理解析

本文将深入探讨Golang中unsafe包的功能和原理。同时,我们学习某种东西,一方面是为了实践运用,另一方面则是出于功利性面试的目的。所以,本文还会为大家介绍unsafe 包的典型应用以及高频面试题。

[转帖]使用 TiUP cluster 在单机上安装TiDB

https://zhuanlan.zhihu.com/p/369414808 TiUP 是 TiDB 4.0 版本引入的集群运维工具,TiUP cluster 是 TiUP 提供的使用 Golang 编写的集群管理组件,通过 TiUP cluster 组件就可以进行日常的运维工作,包括部署、启动、关

[转帖]使用 TiUP 部署 TiDB 集群

https://docs.pingcap.com/zh/tidb/stable/production-deployment-using-tiup TiUP 是 TiDB 4.0 版本引入的集群运维工具,TiUP cluster 是 TiUP 提供的使用 Golang 编写的集群管理组件,通过 TiU

摸鱼快报:golang net/http中的雕虫小技

以后会开一个板块,摸鱼快报,快速记录这几周开发中雕虫小技。 1. 向开发环境localhost:3000种植cookie 前端使用Create React App脚手架,默认以localhost:3000端口启动; 后端使用golang-gin框架,使用8034端口启动。 登录模块走的是sso,前后

[转帖]TiUP Cluster 命令合集

https://docs.pingcap.com/zh/tidb/stable/tiup-component-cluster TiUP Cluster 是 TiUP 提供的使用 Golang 编写的集群管理组件,通过 TiUP Cluster 组件就可以进行日常的运维工作,包括部署、启动、关闭、销毁

golang trace view 视图详解

> 大家好,我是蓝胖子,在golang中可以使用go pprof的工具对golang程序进行性能分析,其中通过go trace 命令生成的trace view视图对于我们分析系统延迟十分有帮助,鉴于当前对trace view视图的介绍还是很少,在粗略的看过trace统计原理后,我将对这部分做比较详细

[转帖]goproxy的设置

goproxy.io 是全球最早的 Go modules 镜像代理服务之一 【大陆地区建议使用 proxy.golang.com.cn】,采用 CDN 加速服务为开发者提供依赖下载, 该服务由一批热爱开源, 热爱 Go 语言的年轻人开发维护。从 Go 1.11 开始 Go 语言开始支持 Go mod

20个Golang片段让我不再健忘

本文使用代码片段的形式来解释在 go 语言开发中经常遇到的小功能点,由于本人主要使用 java 开发,因此会与其作比较,希望对大家有所帮助。

golang sync.Map 与使用普通的 map 的区别

使用sync.Map与普通的Go map主要有以下几点区别: 1. 并发安全性 普通map: 在没有外部同步的情况下,不是并发安全的。在多goroutine访问时,如果没有适当的锁或其他同步机制保护,可能会导致数据竞争和未定义行为。 sync.Map: 是并发安全的。它内部实现了必要的同步机制,允许