前言
最近在学web编程的途中,经过学长提醒,在进行登陆(Login)
操作之后,识别是否登陆的标识应该要放入authorization
中,正好最近也在学鉴权,就顺便来看看源码了。
正文
1. 代码示例
在进行分析之前,先给予一段JWT生成和解析的代码示例,这样可以随着代码一步一步深入到源码层面来帮助我们进一步理解JWT这一部分知识。
package api//关于jwt的所有操作都在这里
import ("errors""fmt""github.com/dgrijalva/jwt-go""time"
)var mySigningKey = []byte("Bang dream Girls Band Part!114514Girls Band Cry!114514,It's My Go!!!!!")func GenerateJWT(username string) (string, error) {token := jwt.New(jwt.SigningMethodHS256)claims := token.Claims.(jwt.MapClaims)claims["username"] = usernameclaims["exp"] = time.Now().Add(time.Second * 30).Unix()ToKenSting, err := token.SignedString(mySigningKey)if err != nil {return "", err}return ToKenSting, nil
}func VerifyJWT(tokenString string) (string, error) {token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])}return mySigningKey, nil})if err != nil {return "", err}if claims, ok1 := token.Claims.(jwt.MapClaims); ok1 && token.Valid {username := claims["username"].(string)return username, nil}return "", errors.New("token invalid")
}
2. GenerateJWT
2.1 新建token
token := jwt.New(jwt.SigningMethodHS256)
在函数开头,我们可以发现,我们使用了jwt包中的new,并传递了一个签名方法来新建了一个token
这个new函数又是如何实现的呢?
我们可以发现,new就是返回了一个token类型的指针,而进一步向下查看NewWithClaims的实现
则是直接返回了Token结构体的指针,其中的参数method.Alg()是直接返回了这个方法的Name:
claims则是这个token的声明,也就是我们接下来需要写入数据的部分,
2.2 签名方法类型
刚刚我们提到了我们在创建token对象的时候需要传递一个签名方法,可以理解为我们的随后会以什么样的方式生成jwt字符串,很多人可能会对这个签名方法是什么东西感到疑惑,没有兴趣了解的可以跳过这一部分。
此处我们以SigningMethodHMAC为例子
这个所谓的签名方法实际上就是一个结构体,源代码中,我们可以看见,代码为这个结构体创建了256/384/512的实例对象,这就是我们真正可以访问的签名方法了,但是这几个实例对象作为结构体实例的参数又是什么呢?
如图所示,在这个签名方法结构体所在的包中,利用init函数为这些实例对象赋予了值,而RegisterSigningMethod
如图所示,RegisterSigningMethod的作用则是利用哈希表,将签名方法的名字映射到这一结构体的实体对象,相当于将签名方法注册到了实体中,从逻辑上来说,我觉得更美吧。
2.3 Claims
claims := token.Claims.(jwt.MapClaims)claims["username"] = usernameclaims["exp"] = time.Now().Add(time.Second * 30).Unix()
这一步,对token中的Claims成员进行类型断言,并作为MapClaims类型赋予claims变量,此处由于Claims是一个接口,并且MapClaims实际上是一个哈希表,并且实现了Claims接口,所以可以转换,并且具有引用的作用,意思是修改claims的值就相当于修改了token.Claims的值。
这里先不对Raw以及没讲到的成员变量做说明,后面会提到。
我们可以看见MapClaims实际上是一个哈希表,而这个类型实现了Valid方法,所以就实现了Claims接口。
2.4 jwt字符串生成
ToKenSting, err := token.SignedString(mySigningKey)
此处,则是我们生成jwt字符串的一步了
此处,除了两个需要注意的方法,其他都很简单,下面我们来看看这两个方法具体做了什么事情吧!
func (t *Token) SigningString() (string, error) {var err errorparts := make([]string, 2)for i, _ := range parts {var jsonValue []byteif 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
}
首先是第一部分,创建了一个长度为2的string切片,随后我们对part进行遍历,当遍历到第一个部分时,代码将 Token 结构体中的头部信息(t.Header)序列化为 JSON 格式的字节切片。随后将其存储在parts[0]之中,当遍历到part第二部分则是将Claims部分进行序列化,并存入part[1],最后将这个切片用“.”连接起来。
随后来看看第二部分
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
}
在这里首先对key进行类型断言,由于此处我们传入的是字节切片,所以不会出错,随后再进行判断这个签名方法的hash值是否合法,也就是看这个方法是否是正确的,这样理解便好,随后利用hmac包(对应了签名方法)中的New,并传入我们的Key,新建了一个哈希对象,随后将之前我们在外部生成的签名写入这个哈希对象,最后,通过调用 hasher.Sum(nil) 计算并返回结果,这个结果一般会是一个字符串形式的签名,用于生成最终的 JWT,而关于hash计算和生成其中的源码有点过于跑题了,就不在这里展开讲了。
return strings.Join([]string{sstr, sig}, "."), nil
在生成签名的最后,将之前生成的字符串利用“.”连接在一起,这一步对于我们后面解析JWT字符串有大用,到这一步,就已经得到我们的JWT字符串了。
3. VerifyJWT
3.1 Parse
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])}return mySigningKey, nil})
我们解析JWT字符串需要创建一个token来存储我们的解析后的结果,而parse函数,就是解析我们的JWT字符串的大杀器了,在这个函数中,我们需要传入原始的JWT字符串和一个匿名函数,用来在Parse函数内部判断解析的方法时候正确。
func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {return new(Parser).Parse(tokenString, keyFunc)
}
在进入到函数内部之后,我们发现又是一层套娃,但是这一层新建了一个parser解析器,以此来进行调用解析
func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {return p.ParseWithClaims(tokenString, MapClaims{}, keyFunc)
}
继续进入下一步
func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {token, parts, err := p.ParseUnverified(tokenString, claims)if err != nil {return token, err}// Verify signing method is in the required setif p.ValidMethods != nil {var signingMethodValid = falsevar alg = token.Method.Alg()for _, m := range p.ValidMethods {if m == alg {signingMethodValid = truebreak}}if !signingMethodValid {// signing method is not in the listed setreturn token, NewValidationError(fmt.Sprintf("signing method %v is invalid", alg), ValidationErrorSignatureInvalid)}}// Lookup keyvar key interface{}if keyFunc == nil {// keyFunc was not provided. short circuiting validationreturn token, NewValidationError("no Keyfunc was provided.", ValidationErrorUnverifiable)}if key, err = keyFunc(token); err != nil {// keyFunc returned an errorif ve, ok := err.(*ValidationError); ok {return token, ve}return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable}}vErr := &ValidationError{}// Validate Claimsif !p.SkipClaimsValidation {if err := token.Claims.Valid(); err != nil {// If the Claims Valid returned an error, check if it is a validation error,// If it was another error type, create a ValidationError with a generic ClaimsInvalid flag setif e, ok := err.(*ValidationError); !ok {vErr = &ValidationError{Inner: err, Errors: ValidationErrorClaimsInvalid}} else {vErr = e}}}// Perform validationtoken.Signature = parts[2]if err = token.Method.Verify(strings.Join(parts[0:2], "."), token.Signature, key); err != nil {vErr.Inner = errvErr.Errors |= ValidationErrorSignatureInvalid}if vErr.valid() {token.Valid = truereturn token, nil}return token, vErr
}
终于来到了我们的源代码分析环节,
token, parts, err := p.ParseUnverified(tokenString, claims)if err != nil {return token, err}
首先第一步,是对我们的token进行生成,我们需要进入函数内部来进行分析。
func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Token, parts []string, err error) {parts = strings.Split(tokenString, ".")if len(parts) != 3 {return nil, parts, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed)}token = &Token{Raw: tokenString}// parse Headervar headerBytes []byteif headerBytes, err = DecodeSegment(parts[0]); err != nil {if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") {return token, parts, NewValidationError("tokenstring should not contain 'bearer '", ValidationErrorMalformed)}return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}}if err = json.Unmarshal(headerBytes, &token.Header); err != nil {return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}}// parse Claimsvar claimBytes []bytetoken.Claims = claimsif claimBytes, err = DecodeSegment(parts[1]); err != nil {return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}}dec := json.NewDecoder(bytes.NewBuffer(claimBytes))if p.UseJSONNumber {dec.UseNumber()}// JSON Decode. Special case for map type to avoid weird pointer behaviorif c, ok := token.Claims.(MapClaims); ok {err = dec.Decode(&c)} else {err = dec.Decode(&claims)}// Handle decode errorif err != nil {return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}}// Lookup signature methodif method, ok := token.Header["alg"].(string); ok {if token.Method = GetSigningMethod(method); token.Method == nil {return token, parts, NewValidationError("signing method (alg) is unavailable.", ValidationErrorUnverifiable)}} else {return token, parts, NewValidationError("signing method (alg) is unspecified.", ValidationErrorUnverifiable)}return token, parts, nil
}
是不是被吓到了?没关系,我带你来一步一步分析,其实重要的部分并不多。
parts = strings.Split(tokenString, ".")if len(parts) != 3 {return nil, parts, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed)}token = &Token{Raw: tokenString}
在这里,我们将原始JWT字符串利用我们之前设置的“.”分开,这时候我们便可以进行初步的判断这个接受的jwt字符串是否合法,判断之后,我们先将原始jwt字符串存入新建的token对象的raw成员中,这时候我们就需要提及我们之前没讲的raw成员,他就是我们token对象的原始jwt字符串。
var headerBytes []byteif headerBytes, err = DecodeSegment(parts[0]); err != nil {if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") {return token, parts, NewValidationError("tokenstring should not contain 'bearer '", ValidationErrorMalformed)}return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}}if err = json.Unmarshal(headerBytes, &token.Header); err != nil {return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}}
这里看着很复杂,但是实际上抛开大量的if return,真正解析的部分并不多,这一部分就是将我们的jwt中的header部分进行解析,随后判断是否出错而已,随后将header中的信息存入token中。
// parse Claimsvar claimBytes []bytetoken.Claims = claimsif claimBytes, err = DecodeSegment(parts[1]); err != nil {return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}}dec := json.NewDecoder(bytes.NewBuffer(claimBytes))if p.UseJSONNumber {dec.UseNumber()}// JSON Decode. Special case for map type to avoid weird pointer behaviorif c, ok := token.Claims.(MapClaims); ok {err = dec.Decode(&c)} else {err = dec.Decode(&claims)}// Handle decode errorif err != nil {return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}}
这一步也很简单,就是对我们的claims部分进行解析,然后放入token中就结束了。
if method, ok := token.Header["alg"].(string); ok {if token.Method = GetSigningMethod(method); token.Method == nil {return token, parts, NewValidationError("signing method (alg) is unavailable.", ValidationErrorUnverifiable)}} else {return token, parts, NewValidationError("signing method (alg) is unspecified.", ValidationErrorUnverifiable)}
在最后从header获取方法的名字,判断一下是否合法就可以返回了。
接下来回到上一层源代码
// Verify signing method is in the required setif p.ValidMethods != nil {var signingMethodValid = falsevar alg = token.Method.Alg()for _, m := range p.ValidMethods {if m == alg {signingMethodValid = truebreak}}if !signingMethodValid {// signing method is not in the listed setreturn token, NewValidationError(fmt.Sprintf("signing method %v is invalid", alg), ValidationErrorSignatureInvalid)}}
继续下一步,p.ValidMethods是一个切片
type Parser struct {ValidMethods []string // If populated, only these methods will be considered validUseJSONNumber bool // Use JSON Number format in JSON decoderSkipClaimsValidation bool // Skip claims validation during token parsing
}
包含允许的签名方法。一个条件判断检查它是否为 nil,如果不为 nil,说明存在需要验证的方法集合,随后遍历这个集合,如果使用的签名方法在这个集合中,便可进行下一步。
// Lookup keyvar key interface{}if keyFunc == nil {// keyFunc was not provided. short circuiting validationreturn token, NewValidationError("no Keyfunc was provided.", ValidationErrorUnverifiable)}if key, err = keyFunc(token); err != nil {// keyFunc returned an errorif ve, ok := err.(*ValidationError); ok {return token, ve}return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable}}
这一步很简单,就是调用我们传入的匿名函数,看看我们传入的签名方法是否和实际解析出来的的方法一致,如果一致,就进行下一步。
// Validate Claimsif !p.SkipClaimsValidation {if err := token.Claims.Valid(); err != nil {// If the Claims Valid returned an error, check if it is a validation error,// If it was another error type, create a ValidationError with a generic ClaimsInvalid flag setif e, ok := err.(*ValidationError); !ok {vErr = &ValidationError{Inner: err, Errors: ValidationErrorClaimsInvalid}} else {vErr = e}}}
p.SkipClaimsValidation就是一个bool值,这一步判断是否要跳过Claims的有效性验证,如果有效,进行下一步验证。
// Perform validationtoken.Signature = parts[2]if err = token.Method.Verify(strings.Join(parts[0:2], "."), token.Signature, key); err != nil {vErr.Inner = errvErr.Errors |= ValidationErrorSignatureInvalid}if vErr.valid() {token.Valid = truereturn token, nil}return token, vErr
}
此处又要提到我们之前提到的token结构体中的signature成员了,此处将jwt中的签名部分赋值给signatrue(签名)
这一部分用于确保 JWT 的完整性和身份验证。签名是通过将前两个部分的编码后结果与密钥进行哈希计算而生成的。
此处直接调用token.Method.Verify(…)进行验证便好。
最后调用 valid 方法检查当前的验证错误状态。如果没有错误(即 vErr 是有效的),则将 token.Valid 设置为 true,直接返回就行了。
3.2 提取信息
最后,回到我们的VerifyJWT函数
if claims, ok1 := token.Claims.(jwt.MapClaims); ok1 && token.Valid {username := claims["username"].(string)return username, nil}return "", errors.New("token invalid")
判断错误,提取信息就好了。
那么,关于源码的分析就到这里结束了,如果有什么问题,请留言~
结语
其实看源码的过程还是蛮有趣的,主要是可以感受着自己在一步一步变强的感觉(也说不准,代码的乐趣也很多),关于JWT的源码分析就到这里结束了,读完这篇文章,你应该对JWT有更深的了解了~
强者应该也不会到这里来吧…