zoukankan      html  css  js  c++  java
  • Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务(三):RSA(RS512) 签名 JWT(附demo)

    系列

    1. 云原生 API 网关,gRPC-Gateway V2 初探
    2. Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务:第一篇
    3. Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务:第二篇

    JWT & RS512

    JWTRSA 详解,这里就不科普了。Google 一下,太多文章介绍了。

    我们进入官网:https://jwt.io/

    我们这里看图说话,注意 Decoded 部分:

    1. HEADER:ALGORITHM & TOKEN TYPE
    2. PAYLOAD:DATA
    3. VERIFY 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:表示谁签发,这里就是我们的这个鉴权微服务。
    • iattoken 签发时间
    • exptoken 过期时间
    • 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

    我们增加 TokenExpireTokenGenerator 的相关配置。

    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

    我是为少
    微信:uuhells123
    公众号:黑客下午茶
    加我微信(互相学习交流),关注公众号(获取更多学习资料~)
    
  • 相关阅读:
    springboot的整合篇-springmvc,mybatis,数据库等
    快速搭建springboot工程
    Git常用命令
    SpringBoot整合通用Mapper
    SpringBoot项目部署
    实例化调用和静态方式调用的区别
    CentOS系统安装PHP7.3
    CentOS7部署LNMP环境
    Apache和Nginx的区别
    Linux系统下卸载MySQL
  • 原文地址:https://www.cnblogs.com/hacker-linner/p/14647542.html
Copyright © 2011-2022 走看看