应用安全: Token | 青训营笔记

207 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 17 天。

1. 加密和解密

加密技术包括两个基本的两个元素:加密算法、密钥。
在现代密码学种,加密算法都是公开的,而密钥是保密的。

1.1. 对称加密

对称加密的特点有:

  • 加密和解密时使用的密钥相同
  • 加密简单快捷
  • 适用于一对一的场景

常见算法:

  • DES: 密钥位数是 56
  • 3DES: 密钥位数是 112
  • IDEA: 密钥位数是 128
  • AES: 密钥位数是 128 或 192 或 256

1.2. 非对称加密

非对称加密的特点有:

  • 加密和解密使用不同的密钥
  • 公钥用于加密,私钥用于解密
  • 适用于一对多的场景

常见算法:

  • RSA
  • DSA
  • ECC
  • DH

1.3. Hash

哈希不是加密算法,其算法是单向的,不可逆的。

  • Hash 函数将任意长的报文 M 映射为定长的 Hash 码 h
  • Hash 函数的目的就是要产生文件、报文或其他数据块的指纹,Hash 码也称报文摘要。
  • Hash 函数可提供保密性、报文认证以及数字签名功能。

1.4. 数字签名

数字签名的作用是证明当事人的身份和数据真实性的一种方式。

  1. 签名者事后不能抵赖自己的签名。
  2. 任何其他人不能伪造签名。
  3. 如果当事的双方关于签名发生争执,能够在公正的仲裁者面前通过验证签名来确认其真伪。

利用非对称加密算法,如 RSA,可以同时实现数字签名。
使用私钥生成数字签名,使用公钥解密验证。

2. Token

Token 需要具备的特性:

  1. 随机性, 不可预测, 防猜测
  2. 失效时间, 可以无感获取 (refresh)
  3. 包含信息, 可以代表用户身份
  4. 防篡改, 保证接收到的 Token 是服务端曾经发放过的
  5. 服务端必须保存些什么, 以便主动失效掉 Token

2.1. jwt-go

安装 JWT 库:使用 go get 命令安装 JWT 库:

go get -u github.com/dgrijalva/jwt-go@v3.2.0

2.1.1. 创建 token

  1. 创建一个 claims 结构体, 用来存储 token 的载荷 (payload) 信息.
    claims 需要组合继承 jwt.StandardClaims, 其中包括失效时间 (ExpiresAt), Token 颁布者 (Issuer)等信息.

    type claims struct {
        jwt.StandardClaims
        UserId int
    }
    
    // Structured version of Claims Section, as referenced at
    // https://tools.ietf.org/html/rfc7519#section-4.1
    // See examples for how to use this with your own claim types
    type StandardClaims struct {
        Audience  string `json:"aud,omitempty"`
        ExpiresAt int64  `json:"exp,omitempty"`
        Id        string `json:"jti,omitempty"`
        IssuedAt  int64  `json:"iat,omitempty"`
        Issuer    string `json:"iss,omitempty"`
        NotBefore int64  `json:"nbf,omitempty"`
        Subject   string `json:"sub,omitempty"`
    }
    
  2. 调用 jwt.NewWithClaims 初始化 Token 生成器并选择算法.

    func NewWithClaims(method SigningMethod, claims Claims) *Token
    

    除了 claims 还需要传入一个参数, SigningMethod 表示签名算法, 可选择的算法有

    • jwt.SigningMethodHMAC
    • jwt.SigningMethodRSAPSS
    • jwt.SigningMethodRSA
    • SigningMethodECDSA 这些算法里面各自还包含了 3 种哈希算法, 分别初始化了一些算法实例. 例如 SigningMethodHS256 就是使用了 HMAC-SHA256 算法:
    type SigningMethodHMAC struct {
        Name string
        Hash crypto.Hash
    }
    // Specific instances for HS256 and company
    var (
        SigningMethodHS256  *SigningMethodHMAC
        SigningMethodHS384  *SigningMethodHMAC
        SigningMethodHS512  *SigningMethodHMAC
        ErrSignatureInvalid = errors.New("signature is invalid")
    )
    
    // Implements the RSAPSS family of signing methods signing methods
    type SigningMethodRSAPSS struct {
        *SigningMethodRSA
        Options *rsa.PSSOptions
    }
    // Specific instances for RS/PS and company
    var (
        SigningMethodPS256 *SigningMethodRSAPSS
        SigningMethodPS384 *SigningMethodRSAPSS
        SigningMethodPS512 *SigningMethodRSAPSS
    )
    
  3. 调用 jwt.SignedString 函数生成 Token.
    并使用一个密钥 (key) 进行数字签名, 用于防止篡改, 保证接收到的 Token 是服务端曾经发放过的.

    // Get the complete, signed token
    func (t *Token) SignedString(key interface{}) (string, error) 
    

完整代码:

type claims struct {
	jwt.StandardClaims
	UserId int
}

func generateToken(userId int) (string, error) {
	token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims{
		StandardClaims: jwt.StandardClaims{
			Issuer:    conf.Hostname,
			ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
		},
		UserId: userId,
	}).SignedString([]byte("密钥,请保密"))

	if err != nil {
		return "", err
	}
	return token, nil
}

2.1.2. 校验 Token

使用 jwt.ParseWithClaims 函数验证 Token, 还可以获取 claims 中的信息.
claims 用于反射获取类型, keyFunc 是获取密钥的函数,

func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error)

完整代码:

func verifyToken(token string) int {
	_token, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) {
		return conf.SecretKey, nil
	})
	if err != nil {
		return 0
	}

	_claims, ok := _token.Claims.(*claims)
	if !ok || !_token.Valid {
		return 0
	}

	return _claims.UserId
}