JWT 介绍和作用
JSON Web Token (JWT) 是一种开放标准 ( RFC 7519 ),它定义了一种紧凑且独立的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。该信息可以被验证和信任,因为它是经过数字签名的。 JWT 可以使用密钥(使用HMAC算法)或使用RSA或ECDSA 的公钥/私钥对进行签名
尽管 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 ) ( 𝑞 − 1 )
-
选择一个整数 𝑒 ,使得 1 < 𝑒 < 𝜙 ( 𝑛 ) 1<e<ϕ(n) 且 𝑒 与 𝜙 ( 𝑛 ) 互质。
-
计算 𝑑,使得 𝑑 × 𝑒 ≡ 1 mod𝜙(𝑛)
-
公钥为 ( 𝑒 , 𝑛 ) ,私钥为 ( 𝑑 , 𝑛 )
-
加密:
- 使用公钥 ( 𝑒 , 𝑛 ) 对明文 𝑚 进行加密: 𝑐 = 𝑚𝑒 mod 𝑛
-
解密:
- 使用私钥 ( 𝑑 , 𝑛 ) 对密文 𝑐 进行解密: 𝑚=𝑐𝑑 mod 𝑛
RSA 与 JWT 在 Kratos 中的应用
在 JWT 中,RSA 算法通常用于生成和验证签名。生成 JWT 时,使用私钥对头部和载荷的组合进行签名;验证 JWT 时,使用公钥对签名进行验证,确保 JWT 的完整性和来源的真实性。
- 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)
}