JWT & RS512
JWT
与 RSA
详解,这里就不科普了。Google 一下,太多文章介绍了。
我们进入官网:https://jwt.io/
我们这里看图说话,注意 Decoded
部分:
HEADER
:ALGORITHM & TOKEN TYPEPAYLOAD
:DATAVERIFY SIGNATURE
没错,JWT
就这三部分组成 HEADER.PAYLOAD.SIGNATURE(头部.负载.签名)
,我们这里选择非对称加密算法(RS512
)。
简单来说(编码时),鉴权微服务使用 Private Key
用来生成签名,其它微服务使用 Public Key
验证签名是不是 Auth
微服务签发的(过期、数据有无篡改等)。Private Key
& Public Key
是成对出现的。
这里编码测试直接用 jwt.io
官网提供的,我们复制出来就好。
RSA PRIVATE KEY
-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV 3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM +bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC 0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X +jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC 2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz 5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j 8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma 3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= -----END RSA PRIVATE KEY-----
我们将其放在 microsvcs/auth/private.key
PUBLIC KEY
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 MwIDAQAB -----END PUBLIC KEY-----
我们将其放在 microsvcs/auth/public.key
Payload
这里我们要用的测试 Data
如下:
{ "exp": 1516246222, "iat": 1516239022, "iss": "server/auth", "sub": "607266aa512e006d58b79d22" }
iss
:表示谁签发,这里就是我们的这个鉴权微服务。iat
:token
签发时间exp
:token
过期时间sub
:谁被签发,这里就是我们的账号ID
编码实战
定义 TokenGenerator 接口 & 使用
我们进入到 microsvcs/auth/auth/auth.go
,延续我们之前的逻辑,因为生成 Token
是个独立的实现,所以按套路我们定义一个 TokenGenerator
接口。
type TokenGenerator interface { GenerateToken(accountID string, expire time.Duration) (string, error) }
使用者定义接口
!使用者定义接口
!使用者定义接口
!重要事说三遍!!!
改造 type Service struct
如下:
type Service struct { Mongo *dao.Mongo Logger *zap.Logger OpenIDResolver OpenIDResolver TokenGenerator TokenGenerator // 生成器 TokenExpire time.Duration // 过期时间 authpb.UnimplementedAuthServiceServer }
我们让 TokenGenerator
& TokenExpire
从外面传进来,通通可配,随时替换相关实现。
用的时候就非常简单了:
... ... tkn, err := s.TokenGenerator.GenerateToken(accountID, s.TokenExpire) if err != nil { s.Logger.Error("cannot generate token", zap.Error(err)) return nil, status.Error(codes.Internal, "") } return &authpb.LoginResponse{ AccessToken: tkn, ExpiresIn: int32(s.TokenExpire.Seconds()), }, nil
实现 TokenGenerator 接口
我们编写如下代码(文件位于microsvcs/auth/token/jwt.go
):
type JWTTokenGen struct { privateKey *rsa.PrivateKey issuer string nowFunc func() time.Time } func NewJWTTokenGen(issuer string, privateKey *rsa.PrivateKey) *JWTTokenGen { return &JWTTokenGen{ issuer: issuer, nowFunc: time.Now, privateKey: privateKey, } } func (t *JWTTokenGen) GenerateToken(accountID string, expire time.Duration) (string, error) { nowSec := t.nowFunc().Unix() token := jwt.NewWithClaims(jwt.SigningMethodRS512, jwt.StandardClaims{ Issuer: t.issuer, // 签发者 IssuedAt: nowSec, // 签发时间 ExpiresAt: nowSec + int64(expire.Seconds()), // 过期时间 Subject: accountID, // 签发给谁 }) return token.SignedString(t.privateKey) }
写代码的套路都是一样的:
- 定义一个
struct
(JWTTokenGen
) - 外部可初始化该
struct
(NewJWTTokenGen
),让一些公共私密的资源可配置化(如:rsa.PrivateKey
) TokenGenerator
接口的具体实现(GenerateToken
)
注意:这里定义 nowFunc
,方便 Unit Tests
固定某一个时间点去做签发工作。
编写测试
具体代码位于 microsvcs/auth/token/jwt_test.go
:
func TestGenerateToken(t *testing.T) { pkFile, err := os.Open("../private.key") if err != nil { logger.Fatal("cannot open private key", zap.Error(err)) } pkBytes, err := ioutil.ReadAll(pkFile) if err != nil { logger.Fatal("cannot read private key", zap.Error(err)) } key, err := jwt.ParseRSAPrivateKeyFromPEM(pkBytes) if err != nil { logger.Fatal("cannot parse private key", zap.Error(err)) } g := NewJWTTokenGen("server/auth", key) g.nowFunc = func() time.Time { return time.Unix(1516239022, 0) } tkn, err := g.GenerateToken("607266aa512e006d58b79d22", 2*time.Hour) if err != nil { t.Errorf("cannot generate token: %v", err) } want := "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTYyNDYyMjIsImlhdCI6MTUxNjIzOTAyMiwiaXNzIjoic2VydmVyL2F1dGgiLCJzdWIiOiI2MDcyNjZhYTUxMmUwMDZkNThiNzlkMjIifQ.nwhaGZ0dozftexVfr9KM9ZVAzsPudhLs-n-yyrrjkbFTYA69rsEd35M0vc1gJ1DNMJk_v-1yUhkgRpxzP2Jiy1Lw8fqIlAk8l9EpDE77oJ9Dal6Rl26GERYZOkCvbq02fKSVj4drlSr75fIce9EnQq2xIVyvvNNty-QvHXTX29QQv-6c8vVYIrCFxtooARN9p8OSpg0hzc-YzsXo64lbUvbLIws27TJNwhctbqrOYQuX9XU3UhJ4Ik0Yt2cLc4LjuqI52Grvf89mJMmM5jnHQv0tKI2guvxNwlC3WN50dCIcuo1zjO-_eSje5OvqP7FKR1eSwnEcZiZQ8qwDDGi8pA" if tkn != want { t.Errorf("wrong token generated. want: %q, got: %q", want, tkn) } }
联调
更新 microsvcs/auth/main.go
我们增加 TokenExpire
与 TokenGenerator
的相关配置。
authpb.RegisterAuthServiceServer(s, &auth.Service{ OpenIDResolver: &wechat.Service{ AppID: "your-app-id", AppSecret: "your-app-secret", }, Mongo: dao.NewMongo(mongoClient.Database("grpc-gateway-auth")), Logger: logger, TokenExpire: 2 * time.Hour, TokenGenerator: token.NewJWTTokenGen("server/auth", privKey), })
小程序联调
完美签发 Token
。
大家持续关注,未完待续……