微服务框架 Kratos 使用 JWT 认证与 RSA 签名验证

332 阅读5分钟

JWT 介绍和作用

JSON Web Token (JWT) 是一种开放标准 ( RFC 7519 ),它定义了一种紧凑且独立的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。该信息可以被验证和信任,因为它是经过数字签名的。 JWT 可以使用密钥(使用HMAC算法)或使用RSAECDSA 的公钥/私钥对进行签名

尽管 JWT 可以加密以在各方之间提供保密性,但JWT重点关注的是签名令牌。签名令牌可以验证其中包含的声明的完整性,而加密令牌则向其他方隐藏这些声明。当使用公钥/私钥对对令牌进行签名时,签名还证明只有持有私钥的一方才是对其进行签名的一方。

什么时候应该使用 JSON Web 令牌?

授权

这是使用 JWT 最常见的场景。用户登录后,每个后续请求都将包含 JWT,从而允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小并且能够轻松地跨不同域使用

信息交换

JSON Web 令牌是在各方之间安全传输信息的好方法。因为 JWT 可以进行签名(例如,使用公钥/私钥对),所以您可以确定发送者就是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。

JWT 的组成部分

在其紧凑形式中,JSON Web 令牌由用点 ( . ) 分隔的三个部分组成,它们是:

  • Header 标头
  • Payload 有效载荷
  • Signature 签名

因此,JWT 通常如下所示。

xxxxx.yyyyy.zzzzz

Header:包含令牌的类型(通常是 JWT)和所使用的签名算法(如 HMAC SHA256 或 RSA)。例如:

{
  "alg": "RS256",
  "typ": "JWT"
}

Payload:包含声明(claims),即实际要传输的数据。声明可以是预定义的,也可以是自定义的。例如:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

Signature:用于验证消息在传输过程中没有被更改,并且可以验证发送者的身份。签名是通过对头部和载荷进行 Base64 编码,然后使用指定的算法(如 HMAC SHA256 或 RSA)进行签名生成的。例如:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

JWT 的作用

身份验证:用户登录后,服务器生成一个 JWT 并返回给客户端。客户端在后续请求中携带此 JWT,服务器通过验证 JWT 来确认用户的身份。 信息交换:由于 JWT 是自包含的,可以在不同系统之间传递信息,而无需查询数据库。 2. RSA 算法 RSA 是一种非对称加密算法,由 Ron Rivest、Adi Shamir 和 Leonard Adleman 于 1977 年提出。RSA 算法基于大数分解的困难性,广泛应用于数字签名和加密通信。

2.1 RSA 的工作原理

  • 密钥生成:
  1. 选择两个大素数 𝑝 和 𝑞

  2. 计算 𝑛 = 𝑝 × 𝑞

  3. 计算 𝜙 ( 𝑛 ) = ( 𝑝 − 1 ) ( 𝑞 − 1 )

  4. 选择一个整数 𝑒 ,使得 1 < 𝑒 < 𝜙 ( 𝑛 ) 1<e<ϕ(n) 且 𝑒 与 𝜙 ( 𝑛 ) 互质。

  5. 计算 𝑑,使得 𝑑 × 𝑒 ≡ 1 mod𝜙(𝑛)

  6. 公钥为 ( 𝑒 , 𝑛 ) ,私钥为 ( 𝑑 , 𝑛 )

  • 加密:

    • 使用公钥 ( 𝑒 , 𝑛 ) 对明文 𝑚 进行加密: 𝑐 = 𝑚𝑒 mod 𝑛
  • 解密:

    • 使用私钥 ( 𝑑 , 𝑛 ) 对密文 𝑐 进行解密: 𝑚=𝑐𝑑 mod 𝑛

RSA 与 JWT 在 Kratos 中的应用

在 JWT 中,RSA 算法通常用于生成和验证签名。生成 JWT 时,使用私钥对头部和载荷的组合进行签名;验证 JWT 时,使用公钥对签名进行验证,确保 JWT 的完整性和来源的真实性。

  1. Go 代码详解 3.1 配置文件 在 configs/auth.yml 文件中,我们定义了 JWT 的服务密钥和服务公钥:
jwt:
  service_key: service_key
  api_key: |
    -----BEGIN CERTIFICATE-----
    MIIE2TCCAsGgAwIBAgIDAeJAMA0GCSqGSIb3DQEBCwUAMCYxDjAMBgNVBAoTBWFk
    ...
    -----END CERTIFICATE-----

3.2 配置结构体 在 conf/conf.proto 文件中,定义了 JWT 配置的 Protobuf 消息结构:


message Auth {
  message JWT {
    string service_key = 1;
    string api_key = 2;
  }
  JWT jwt = 1;
}

3.3 注入配置 在 cmd/server.go 文件中,读取配置并注入到应用程序中:

...
var ac conf.Auth
if err := c.Scan(&ac); err != nil {
    panic(err)
}

app, cleanup, err := wireApp(bc.Server, bc.Data, &ac, logger)
...

3.4 注入依赖 在 wire.go 文件中,定义了依赖注入函数:

...
func wireApp(*conf.Server, *conf.Data, *conf.Auth, log.Logger) (*kratos.App, func(), error) {
    panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))
}

3.5 HTTP 中间件 在 server/http.go 文件中,实现了 JWT 认证的 HTTP 中间件:

package server

import (
	"context"
	v1 "credit_cards/api/credit_cards/v1"
	"credit_cards/internal/conf"
	"credit_cards/internal/service"
	"crypto/rsa"
	"fmt"
	"github.com/go-kratos/kratos/v2/log"
	"github.com/go-kratos/kratos/v2/middleware/auth/jwt"
	"github.com/go-kratos/kratos/v2/middleware/recovery"
	"github.com/go-kratos/kratos/v2/middleware/selector"
	"github.com/go-kratos/kratos/v2/transport/http"
	jwtV5 "github.com/golang-jwt/jwt/v5"
	"github.com/gorilla/handlers"
)

func NewWhiteListMatcher() selector.MatchFunc {
	whiteList := make(map[string]struct{})
	// whiteList["/admin.v1.AdminService/Login"] = struct{}{}
	return func(ctx context.Context, operation string) bool {
		if _, ok := whiteList[operation]; ok {
			return false
		}
		return true
	}
}

func parseRSAPublicKeyFromPEM(pemBytes []byte) (*rsa.PublicKey, error) {
	publicKey, err := jwtV5.ParseRSAPublicKeyFromPEM(pemBytes)
	if err != nil {
		return nil, fmt.Errorf("failed to parse RSA public key: %w", err)
	}
	return publicKey, nil
}

func NewHTTPServer(c *conf.Server, ac *conf.Auth, creditCards *service.CreditCardsServiceService, logger log.Logger) *http.Server {
	publicKey, err := parseRSAPublicKeyFromPEM([]byte(ac.Jwt.ApiKey))
	if err != nil {
		panic("failed to parse public key")
	}
	var opts = []http.ServerOption{
		http.Middleware(
			recovery.Recovery(),
			selector.Server(
				jwt.Server(
					func(token *jwtV5.Token) (interface{}, error) {
						if _, ok := token.Method.(*jwtV5.SigningMethodRSA); !ok {
							return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
						}
						return publicKey, nil
					},
					jwt.WithSigningMethod(jwtV5.SigningMethodRS256),
				),
			).
			Match(NewWhiteListMatcher()).Build(),
		),
		http.Filter(handlers.CORS(
			handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization", "Cookie"}),
			handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}),
			handlers.AllowedOrigins([]string{"*"}),
		)),
	}
	if c.Http.Network != "" {
		opts = append(opts, http.Network(c.Http.Network))
	}
	if c.Http.Addr != "" {
		opts = append(opts, http.Address(c.Http.Addr))
	}
	if c.Http.Timeout != nil {
		opts = append(opts, http.Timeout(c.Http.Timeout.AsDuration()))
	}
	srv := http.NewServer(opts...)
	v1.RegisterCreditCardsServiceHTTPServer(srv, creditCards)
	return srv
}

3.6 使用 JWT 的 Payload 在 data/example.go 文件中,展示了如何从上下文中获取 JWT 的 payload:

func (c *creditCardsRepo) GetCreditCard(ctx context.Context, req *biz.GetCreditCardsRequest) ([]*biz.CreditCards, error) {
    // 通过 jwt.FromContext(ctx) 获取 token 的 payload 内容
	token, ok := jwt.FromContext(ctx)
        // 将jwt的payload转成特定的负载
        user := payload.(*token.Payload)
	if !ok {
		return nil, grpc.Errorf(codes.Unauthenticated, "token not found")
	}
	
	fmt.Printf("token %v", token)
}