【sduoj】生成和解析 JWT

2021SC@SDUSC

引言

在sduoj项目中,我们有四种角色:管理员、教师、学生、普通用户。如果仅仅是通过调用的接口路径的不同来区分这些角色的话,容易引发一些危险的行为。比如,当一个普通用户知道了该项目的管理员接口,那他的行为就有可能造成系统的混乱。因此,我们需要给项目加一点防御,对接口进行访问控制。

JWT 的全称是 JSON WEB TOKEN,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。

JWT 的安装比较简单,输入以下命令即可:

go get -u github.com/dgrijalva/jwt-go

虽然jwt-go库能够对 JWT 令牌的相关行为进行比较快捷的处理,但是为了方便调用,我们还需要对它进一步封装。

源码分析

Claims

Chaims结构体中的AppKeyAppSecret是我们自定义的认证信息,而jwt.StandardClaims结构体则是jwt-go中定义的。

type Claims struct {
    
    
	AppKey    string `json:"app_key"`
	AppSecret string `json:"app_secret"`
	jwt.StandardClaims
}

我们知道,JWT 由三部分构成,第一部分是 header,第二部分为 payload,第三部分是 signature。

header 中存放着令牌类型和令牌使用的加密算法。

payload 存放有效信息,这些有效信息包含三个部分:标准中注册的声明、共有的声明和私有的声明。jwt.StandardClaims定义的就是标准中注册的声明。

signature存放签证信息,用于校验消息在整个过程中有没有被篡改。

jwt.SandardClaims结构体中,Audience是受众,即接受 JWT 的一方,ExpiresAt是所签发的 JWT 过期时间,Id是 JWT 的唯一标识,IssueAt是签发时间,Issuer是 JWT 的签发者,NotBefore是 JWT 的生效时间,Subject是主题。

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"`
}

GetJWTSecret

GetJWTSecret用于从配置文件中获取该项目的 JWT 密钥,并将它转换成byte数组。

func GetJWTSecret() []byte {
    
    
	return []byte(global.JWTSetting.Secret)
}

GenerateToken

GenerateToken方法用于生成 JWT Token,它利用参数中传入的appKeyappSecret,以及配置文件中的Issuer(签发者)、Expire(有效时间),根据指定的算法生成签名后的 Token。

func GenerateToken(appKey, appSecret string) (string, error) {
    
    
	nowTime := time.Now()
	expireTime := nowTime.Add(global.JWTSetting.Expire)
	claims := Claims{
    
    
		AppKey:    util.EncodeMD5(appKey),
		AppSecret: util.EncodeMD5(appSecret),
		StandardClaims: jwt.StandardClaims{
    
    
			ExpiresAt: expireTime.Unix(),
			Issuer:    global.JWTSetting.Issuer,
		},
	}

	tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	token, err := tokenClaims.SignedString(GetJWTSecret())
	return token, err
}

time.Now可以获取当前时间,用这个时间加上 Token 的有效时间Expire,得到过期时间expireTime,再利用Unix方法,得到一个int64类型的、从时间点 January 1, 1970 UTC 到时间点t所经过的时间(单位 s)。

func (t Time) Unix() int64 {
    
    
	return t.unixSec()
}

参数中的appKeyappSecret并没有直接传入Claims结构体,而是经过了 MD5 加密。

func EncodeMD5(value string) string {
    
    
	m := md5.New()
	m.Write([]byte(value))

	return hex.EncodeToString(m.Sum(nil))
}

NewWithClaims会根据加密算法和Claims对象来创建Token实例,这个实例中的Header就是之前提到的 JWT 三部分之一。

func NewWithClaims(method SigningMethod, claims Claims) *Token {
    
    
	return &Token{
    
    
		Header: map[string]interface{
    
    }{
    
    
			"typ": "JWT",
			"alg": method.Alg(),
		},
		Claims: claims,
		Method: method,
	}
}

signedString方法会利用传入的密钥生成签名字符串。它利用t.SigningStringt.Method.Sign返回的字符串以.为分隔符拼装在一起并返回。

func (t *Token) SignedString(key interface{
    
    }) (string, error) {
    
    
	var sig, sstr string
	var err error
	if sstr, err = t.SigningString(); err != nil {
    
    
		return "", err
	}
	if sig, err = t.Method.Sign(sstr, key); err != nil {
    
    
		return "", err
	}
	return strings.Join([]string{
    
    sstr, sig}, "."), nil
}

SigningString会将 header(头部)和 payload(荷载)部分做一次 base64Url 编码,在下面的代码中,parts用于盛放编码后的字符串,最后利用strings.Join将这两个字符串以.为分隔符连接在一起。

func (t *Token) SigningString() (string, error) {
    
    
	var err error
	parts := make([]string, 2)
	for i, _ := range parts {
    
    
		var jsonValue []byte
		if i == 0 {
    
    
			if jsonValue, err = json.Marshal(t.Header); err != nil {
    
    
				return "", err
			}
		} else {
    
    
			if jsonValue, err = json.Marshal(t.Claims); err != nil {
    
    
				return "", err
			}
		}

		parts[i] = EncodeSegment(jsonValue)
	}
	return strings.Join(parts, "."), nil
}

t.Method.Sign利用它的签名算法(这里是jwt.SigningMethodHS256)、t.SigningString得到的字符串、密钥secret,生成一个签名字符串,即 JWT 三部分之一的 signature(签名)。

由此可以看出,签名是由头部、荷载、密钥、加密算法共同生成的,因此可以用来校验消息是否被篡改,一旦被篡改,签名就无法对上。

func (m *SigningMethodHMAC) Sign(signingString string, key interface{
    
    }) (string, error) {
    
    
	if keyBytes, ok := key.([]byte); ok {
    
    
		if !m.Hash.Available() {
    
    
			return "", ErrHashUnavailable
		}

		hasher := hmac.New(m.Hash.New, keyBytes)
		hasher.Write([]byte(signingString))

		return EncodeSegment(hasher.Sum(nil)), nil
	}

	return "", ErrInvalidKeyType
}

ParseToken

ParseTokenGenerateToken的反过程,它用来解析和校验 Token。该方法调用jwt.ParseWithClaims获取tokenClaims,然后对它进行格式的校验,并检查它是否有效,最终将Claims返回。

func ParseToken(token string) (*Claims, error) {
    
    
	tokenClaims, err := jwt.ParseWithClaims(token, &Claims{
    
    }, func(token *jwt.Token) (interface{
    
    }, error) {
    
    
		return GetJWTSecret(), nil
	})

	if tokenClaims != nil {
    
    
		claims, ok := tokenClaims.Claims.(*Claims)
		if ok && tokenClaims.Valid {
    
    
			return claims, nil
		}
	}

	return nil, err
}

ParseWithClaims用于解析鉴权的声明,它调用Parser.ParseWithClaims进行解码和校验,并返回*Token

func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {
    
    
	return new(Parser).ParseWithClaims(tokenString, claims, keyFunc)
}

Token结构体如下图所示,其中的Valid用于表示该 Token 是否有效,它的值与ExpiresAtIssuerNot Before有关。

type Token struct {
    
    
	Raw       string
	Method    SigningMethod
	Header    map[string]interface{
    
    } 
	Claims    Claims
	Signature string
	Valid     bool
}

猜你喜欢

转载自blog.csdn.net/weixin_45922876/article/details/120852629
jwt