服务端身份校验的发展及golang实现

本文将探讨服务身份校验的发展历史和golang具体实现。通过本文的学习,你将收获:

  1. 服务端身份校验的发展历史
  2. session+cookie的会话保持技术原理
  3. JWT校验模式的原理及golang实现
  4. PASETO校验模式的原理及goalng实现

接下来让我们开始学习吧~

背景

session+cookie的会话保持技术

HTTP协议是一种无状态的协议,意味着用户提供账号和密码登录注册之后,下次再请求时,仍需要认证。因为HTTP协议根本无法分辨是哪个用户发送的请求。

为解决这一问题,保持服务器和客户端之间的会话状态,在服务器中为每一个用户分配一块缓存空间,用于存储用户的个人登录信息,且每个存储空间都有一个唯一ID作为自己的身份证。

从此诞生了基于session+cookie的会话保持技术,session是在服务端的一个缓存区,cookie是客户端浏览器的一种数据存储区。处理流程如下:

  1. 客户端浏览器向服务端发出第一次请求

  2. 服务端接收到第一次请求后,在服务端分配一块缓存空间session,并用sessionID来标记该缓存空间。响应第一次请求时,在响应头中设置set-cookie属性的值为sessionID

  3. 客户端浏览器接收到第一次请求的响应后,处理cookie数据并存储。客户端浏览器在后续的每一次请求的请求头中都会设置cookie:sessionID

  4. 客户端浏览器发出第二次请求,并在请求头中设置cookie:sessionID

  5. 服务端接收到第二次请求后,取出sessionID,根据sessionID寻找到特定的session空间,取出用户的个人信息

在这里插入图片描述

session+cookie的会话保持技术存在一下弊端:

  1. 除了浏览器,其他设备对cookie的支持并不友好;

  2. 随着用户量的增加,服务端的空间开销大;

  3. 在处理分布式的场景会相应地限制负载均衡的能力;

  4. cookie存储在客户端,如果被拦截窃取,会很容易受到CSRF跨域伪造请求攻击

Token-based Authentication

针对session+cookie的会话保持技术出现的弊端,提出了基于token的认证技术

在这种验证机制中,用户第一次登录的时候需要POST自己的用户名和密码,随后服务器生成一个access_token。客户端就可以用这个access_token访问服务器上的资源。关于access_token的设置有两种方式:JWT、PASETO

在这里插入图片描述

JWT

JWT-json web token

概述

JWT是一个base64编码的字符串,主要由三部分组成:

  • header:令牌头部,记录令牌的类型和签名算法
  • payload:令牌载荷,记录了保存的主体信息
  • verify signature:令牌签名,按照令牌头部设定的签名算法对令牌进行签名,保证令牌不被篡改和伪造

在这里插入图片描述

header

令牌头部,记录整个令牌的类型和算法,是一个json格式,如下:

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

  • alg:令牌的签名算法

    HSA256:对称性加密算法

    RS256:非对称性加密算法,使用公钥加密,私钥解密

  • typ:整个令牌的类型,固定为"JWT"即可

json格式需通过base64 URL 编码

payload

令牌载荷,记录了需要保存的主体信息,是一个json格式,如下:

{ "ss":"发行者", "iat":"发布时间", "exp":"到期时间", "sub":"主题", "aud":"听众", "nbf":"在此之前不可用", "jti":"JWT ID" }

json格式需通过base64 URL编码

verify signature

令牌签名,按照header中指定的加密方式和指定的密钥,对header.payload的字符串进行加密,后得到verify signature

golang代码实现
目录结构如下
  • jwt_maker.go
  • jwt_maker_test.go
  • maker.go
  • payload.go

在这里插入图片描述

maker.go

定义一个maker接口,有两个方法CreateToken和VerifyToken,在jwt_maker.go文件中对这两个方法进行实现

package token

import "time"

/**
 * @Author: liangyuliang
 * @Description:
 * @File: maker
 * @Version: 1.0.0
 * @Date: 2023/3/12 13:19
 */

// Maker is an interface for managing tokens
type Maker interface {
   // CreateToken creates a new token for a specific username and duration
   CreateToken(username string, duration time.Duration) (string, error)
   // VerifyToken checks if the token is valid or not
   VerifyToken(token string) (*Payload, error)
}
payload.go

设置令牌载荷payload的生成方法,payload记录的内容包括:ID、Username、IssuedAt、ExpiredAt

package token

import (
   "errors"
   "github.com/google/uuid"
   "time"
)

/**
 * @Author: liangyuliang
 * @Description:
 * @File: payload
 * @Version: 1.0.0
 * @Date: 2023/3/12 13:23
 */

var (
   ErrExpiredToken = errors.New("token has expired")
   ErrInvalidToken = errors.New("token is invalided")
)

// Payload contains the payload data of the token
type Payload struct {
   ID        uuid.UUID `json:"id"`
   Username  string    `json:"username"`
   IssuedAt  time.Time `json:"issued_at"`
   ExpiredAt time.Time `json:"expired_at"`
}

func NewPayload(username string, duration time.Duration) (*Payload, error) {
   // NewRandom return a random uuid.UUID
   tokenID, err := uuid.NewRandom()
   if err != nil {
      return nil, err
   }

   payload := &Payload{
      ID:        tokenID,
      Username:  username,
      IssuedAt:  time.Now(),
      ExpiredAt: time.Now().Add(duration),
   }

   return payload, nil
}

/**
 * Valid
 * @Description: checks if the token is valid or not
 * @receiver payload
 * @return error
 */
func (payload *Payload) Valid() error {
   if time.Now().After(payload.ExpiredAt) {
      return ErrExpiredToken
   }
   return nil
}
jwt_maker.go

jwt_maker.go主要用于生成jwt_token,包括以下函数

  • NewJWTMaker:生成一个JWTMaker并指定密钥secretKey
  • CreateToken:生成一个JWT(包括:header、payload、verify signature)
  • VerifyToken:检查JWT是否有效
package token

import (
   "errors"
   "fmt"
   "github.com/golang-jwt/jwt"
   "reflect"
   "time"
)

/**
 * @Author: liangyuliang
 * @Description:
 * @File: jwt_maker
 * @Version: 1.0.0
 * @Date: 2023/3/12 14:04
 */

const minSecretKeySize = 32

// JWTMaker is a JSON Web Token maker
type JWTMaker struct {
   secretKey string
}

/**
 * NewJWTMaker
 * @Description: create a new JWTMaker
 * @param secretKey
 * @return Maker
 * @return error
 */
func NewJWTMaker(secretKey string) (Maker, error) {
   if len(secretKey) < minSecretKeySize {
      return nil, fmt.Errorf("invalid key size: must be at least %d characters", minSecretKeySize)
   }
   return &JWTMaker{secretKey}, nil
}

// CreateToken creates a new token for a specific username and duration
func (maker *JWTMaker) CreateToken(username string, duration time.Duration) (string, error) {
   payload, err := NewPayload(username, duration)
   if err != nil {
      return "", err
   }
   // generate a jwt
   // HS256:一种对称加密算法,使用同一个密钥对signature进行加密解密
   jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
   // get the complete, signed token
   // 令牌签名,对令牌的头部和负荷进行签名,保证令牌不会被伪造或篡改
   return jwtToken.SignedString([]byte(maker.secretKey))
}

// VerifyToken checks if the token is valid or not
func (maker *JWTMaker) VerifyToken(token string) (*Payload, error) {

	//	验证header中的签名是否合法
   keyFunc := func(token *jwt.Token) (interface{}, error) {
      _, ok := token.Method.(*jwt.SigningMethodHMAC)
      if !ok {
         return nil, ErrInvalidToken
      }
      return []byte(maker.secretKey), nil
   }

   // keyFunc函数需要自己实现
   jwtToken, err := jwt.ParseWithClaims(token, &Payload{}, keyFunc)
   if err != nil {
      verr, ok := err.(*jwt.ValidationError)
      if ok && errors.Is(verr.Inner, ErrExpiredToken) {
         return nil, ErrExpiredToken
      }
      return nil, ErrInvalidToken
   }

   payload, ok := jwtToken.Claims.(*Payload)
   if !ok {
      return nil, ErrInvalidToken
   }

   return payload, nil
}
jwt_maker_test.go

jwt_maker_test.go对jwt的相关功能进行测试

  • TestJWTMaker:测试生成JWT的情况
  • TestErrExpiredToken:测试token过期的情况
  • TestErrInvalidToken:测试token无法验证的情况
package token

import (
   "github.com/golang-jwt/jwt"
   "github.com/stretchr/testify/require"
   "simple_bank/util"
   "testing"
   "time"
)

/**
 * @Author: liangyuliang
 * @Description:
 * @File: jwt_maker_test
 * @Version: 1.0.0
 * @Date: 2023/3/12 14:58
 */

func TestJWTMaker(t *testing.T) {
   maker, err := NewJWTMaker(util.RandomString(minSecretKeySize))
   require.NoError(t, err)

   username := util.RandomOwner()
   duration := time.Minute

   issuedAt := time.Now()
   expiredAt := issuedAt.Add(duration)

   token, err := maker.CreateToken(username, duration)
   require.NoError(t, err)
   require.NotEmpty(t, token)

   payload, err := maker.VerifyToken(token)
   require.NoError(t, err)
   require.NotEmpty(t, payload)

   // check the content
   require.NotZero(t, payload.ID)
   require.Equal(t, payload.Username, username)
   require.WithinDuration(t, payload.IssuedAt, issuedAt, time.Second)
   require.WithinDuration(t, payload.ExpiredAt, expiredAt, time.Second)
}

func TestErrExpiredToken(t *testing.T) {
   maker, err := NewJWTMaker(util.RandomString(minSecretKeySize))
   require.NoError(t, err)

   username := util.RandomOwner()
   duration := -time.Minute

   token, err := maker.CreateToken(username, duration)
   require.NoError(t, err)
   require.NotEmpty(t, token)

   verifyToken, err := maker.VerifyToken(token)
   require.Error(t, err, ErrInvalidToken)
   require.Nil(t, verifyToken)
}

func TestErrInvalidToken(t *testing.T) {
   payload, err := NewPayload(util.RandomOwner(), time.Minute)
   require.NoError(t, err)

   jwtToken := jwt.NewWithClaims(jwt.SigningMethodNone, payload)
   token, err := jwtToken.SignedString(jwt.UnsafeAllowNoneSignatureType)
   require.NoError(t, err)

   maker, err := NewJWTMaker(util.RandomString(minSecretKeySize))
   require.NoError(t, err)

   verifyToken, err := maker.VerifyToken(token)
   require.Error(t, err)
   require.Equal(t, err, ErrInvalidToken)
   require.Nil(t, verifyToken)
}
弊端
  1. 不安全的加密算法,虽然JWT提供了许多加密算法,但是包括了许多易受攻击
  2. 在header中包含签名算法的种类,设置alg的字段为none就可以绕过签名验证过程,在知道服务器使用非对称加密算法的基础下,修改alg为对称加密算法

Paseto

PASETO – Platform-Agnostic SEcurity TOkens

概述

每一个版本的PASETO都包含了强大的加密套件,选择对应的加密算法只需要选择PASETO版本即可

最多只能有两个版本同时处于活跃状态

在这里插入图片描述

paseto的令牌结构

在这里插入图片描述

  • Version:版本号,不同版本对应不同的加密套件
  • Purpose:local或public
  • Payload:载荷体
  • Footer:脚部
优势
  • 相比于JWT,PASETO不会向所有用户开放算法
  • PASETO的payload部分可加密,不再是明文的形式
  • PASETO没有alg字段,无法设置为算饭none模式
golang代码实现
  • NewPasetoMaker:生成一个新的PasetoMaker
  • CreateToken:生成token
  • VerityToken:验证token
package token

import (
   "fmt"
   "github.com/aead/chacha20poly1305"
   "github.com/o1egl/paseto"
   "time"
)

/**
 * @Author: liangyuliang
 * @Description:
 * @File: paseto_maker
 * @Version: 1.0.0
 * @Date: 2023/3/13 23:32
 */

type PasetoMaker struct {
   paseto       *paseto.V2
   symmetricKey []byte
}

func NewPasetoMaker(symmetrickKey string) (Maker, error) {
   if len(symmetrickKey) != chacha20poly1305.KeySize {
      return nil, fmt.Errorf("invalid key size: must be %d characters", chacha20poly1305.KeySize)
   }

   maker := &PasetoMaker{
      paseto:       paseto.NewV2(),
      symmetricKey: []byte(symmetrickKey),
   }
   return maker, nil
}

// CreateToken creates a new token for a specific username and duration
func (maker *PasetoMaker) CreateToken(username string, duration time.Duration) (string, error) {
   payload, err := NewPayload(username, duration)
   if err != nil {
      return "", err
   }

   return maker.paseto.Encrypt(maker.symmetricKey, payload, nil)
}

// VerifyToken checks if the token is valid or not
func (maker *PasetoMaker) VerifyToken(token string) (*Payload, error) {
   payload := &Payload{}

   // 验证token是否有效
   err := maker.paseto.Decrypt(token, maker.symmetricKey, payload, nil)
   if err != nil {
      return nil, ErrInvalidToken
   }

   // 验证token是否过期
   err = payload.Valid()
   if err != nil {
      return nil, ErrExpiredToken
   }

   return payload, nil
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,需要安装gRPC和protobuf。可以使用以下命令: ``` go get -u google.golang.org/grpc go get -u github.com/golang/protobuf/proto go get -u github.com/golang/protobuf/protoc-gen-go ``` 接下来,我们需要定义protobuf文件,其中包含服务的RPC方法和消息格式,例如: ``` syntax = "proto3"; package myservice; message Request { string message = 1; } message Response { string message = 1; } service MyService { rpc SayHello(Request) returns (Response) {} } ``` 然后,使用以下命令将protobuf文件编译为Go代码: ``` protoc --go_out=plugins=grpc:. myservice.proto ``` 这将生成myservice.pb.go文件。 接下来,我们需要实现gRPC服务端和客户端。以下是一个简单的示例: 服务端: ``` package main import ( "log" "net" "google.golang.org/grpc" pb "path/to/myservice" ) type myServiceServer struct{} func (s *myServiceServer) SayHello(ctx context.Context, req *pb.Request) (*pb.Response, error) { log.Printf("Received message: %v", req.Message) return &pb.Response{Message: "Hello " + req.Message}, nil } func main() { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterMyServiceServer(s, &myServiceServer{}) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } } ``` 客户端: ``` package main import ( "log" "golang.org/x/net/context" "google.golang.org/grpc" pb "path/to/myservice" ) func main() { conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure()) if err != nil { log.Fatalf("failed to connect: %v", err) } defer conn.Close() c := pb.NewMyServiceClient(conn) res, err := c.SayHello(context.Background(), &pb.Request{Message: "World"}) if err != nil { log.Fatalf("failed to call SayHello: %v", err) } log.Printf("Response message: %v", res.Message) } ``` 运行服务端和客户端,即可进行gRPC通信。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值