系列
- 雲原生 API 閘道器,gRPC-Gateway V2 初探
- Go + gRPC-Gateway(V2) 構建微服務實戰系列,小程式登入鑑權服務:第一篇
- Go + gRPC-Gateway(V2) 構建微服務實戰系列,小程式登入鑑權服務:第二篇
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
。
大家持續關注,未完待續……
Refs
- API Security : API key is dead..Long live Distributed Token by value
- Demo: go-grpc-gateway-v2-microservice
- gRPC-Gateway
- gRPC-Gateway Docs
我是為少
微信:uuhells123
公眾號:黑客下午茶
加我微信(互相學習交流),關注公眾號(獲取更多學習資料~)