本文将探讨服务身份校验的发展历史和golang具体实现。通过本文的学习,你将收获:
- 服务端身份校验的发展历史
- session+cookie的会话保持技术原理
- JWT校验模式的原理及golang实现
- PASETO校验模式的原理及goalng实现
接下来让我们开始学习吧~
目录
背景
session+cookie的会话保持技术
HTTP协议是一种无状态的协议,意味着用户提供账号和密码登录注册之后,下次再请求时,仍需要认证。因为HTTP协议根本无法分辨是哪个用户发送的请求。
为解决这一问题,保持服务器和客户端之间的会话状态,在服务器中为每一个用户分配一块缓存空间,用于存储用户的个人登录信息,且每个存储空间都有一个唯一ID作为自己的身份证。
从此诞生了基于session+cookie的会话保持技术,session是在服务端的一个缓存区,cookie是客户端浏览器的一种数据存储区。处理流程如下:
-
客户端浏览器向服务端发出第一次请求
-
服务端接收到第一次请求后,在服务端分配一块缓存空间session,并用sessionID来标记该缓存空间。响应第一次请求时,在响应头中设置set-cookie属性的值为sessionID
-
客户端浏览器接收到第一次请求的响应后,处理cookie数据并存储。客户端浏览器在后续的每一次请求的请求头中都会设置cookie:sessionID
-
客户端浏览器发出第二次请求,并在请求头中设置cookie:sessionID
-
服务端接收到第二次请求后,取出sessionID,根据sessionID寻找到特定的session空间,取出用户的个人信息
session+cookie的会话保持技术存在一下弊端:
-
除了浏览器,其他设备对cookie的支持并不友好;
-
随着用户量的增加,服务端的空间开销大;
-
在处理分布式的场景会相应地限制负载均衡的能力;
-
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)
}
弊端
- 不安全的加密算法,虽然JWT提供了许多加密算法,但是包括了许多易受攻击
- 在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
}