jwt 概述

JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑而独立的方法,用于在各方之间安全地将信息作为JSON对象传输。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对对JWT进行签名。

什么时候应该使用JWT

  • 授权:这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单一登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。

  • 信息交换:JSON Web令牌是在各方之间安全地传输信息的一种好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。

为什么要使用JSON Web令牌

由于JSON不如XML冗长,因此在编码时JSON的大小也较小,从而使JWT比SAML更为紧凑。这使得JWT是在HTML和HTTP环境中传递的不错的选择。

在安全方面,只能使用HMAC算法由共享机密对SWT进行对称签名。但是,JWT和SAML令牌可以使用X.509证书形式的公用/专用密钥对进行签名。与签署JSON的简单性相比,使用XML Digital Signature签署XML而不引入模糊的安全漏洞是非常困难的。

JSON解析器在大多数编程语言中都很常见,因为它们直接映射到对象。相反,XML没有自然的文档到对象的映射。与SAML断言相比,这使使用JWT更加容易。

关于用法,JWT是在Internet规模上使用的。这突显了在多个平台(尤其是移动平台)上对JSON Web令牌进行客户端处理的简便性。

go-zero中怎么使用jwt

  1. 添加配置定义和yaml配置项
    type Config struct {
     rest.RestConf
     Mysql struct{
         DataSource string
     }
     CacheRedis cache.CacheConf
     Auth      struct {
         AccessSecret string
         AccessExpire int64
     }
    }
    配置文件
    Name: user-api
    Host: 0.0.0.0
    Port: 8888
    Mysql:
    DataSource: $user:$password@tcp($url)/$db?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
    CacheRedis:
    - Host: $host
     Pass: $pass
     Type: node
    Auth:
    AccessSecret: $AccessSecret
    AccessExpire: $AccessExpire

AccessSecret:生成jwt token的密钥,最简单的方式可以使用一个uuid值。 AccessExpire:jwt token有效期,单位:秒

生成token

func (l *LoginLogic) getJwtToken(secretKey string, iat, seconds, userId int64) (string, error) {
  claims := make(jwt.MapClaims)
  claims["exp"] = iat + seconds
  claims["iat"] = iat
  claims["userId"] = userId
  token := jwt.New(jwt.SigningMethodHS256)
  token.Claims = claims
  return token.SignedString([]byte(secretKey))
}
  1. 鉴权 在api文件需要鉴权的路由请求组前声明 jwt:Auth

    @server(
     prefix: usercenter/v1
     group: user
     jwt: Auth
    )
    service usercenter {
    
     @doc "获取用户信息"
     @handler detail
     post /user/detail (UserInfoReq) returns (UserInfoResp)
    }

    生成代码

     server.AddRoutes(
         []rest.Route{
             {
                 Method:  http.MethodPost,
                 Path:    "/user/detail",
                 Handler: user.DetailHandler(serverCtx),
             },
         },
         rest.WithJwt(serverCtx.Config.Auth.AccessSecret),
         rest.WithPrefix("/usercenter/v1"),
     )

go-zero 内部会替业务做掉鉴权这件事,只需要在api里配置一下,生成代码。符合go-zero简洁,让业务快速落地的思想。

go-zero 内部实现

  • go-zero/rest/handler/authhandler.go ``` type ( // A AuthorizeOptions is authorize options. AuthorizeOptions struct {

      PrevSecret string
      Callback   UnauthorizedCallback

    }

    // UnauthorizedCallback defines the method of unauthorized callback. UnauthorizedCallback func(w http.ResponseWriter, r http.Request, err error) // AuthorizeOption defines the method to customize an AuthorizeOptions. AuthorizeOption func(opts AuthorizeOptions) )

// Authorize returns an authorize middleware. func Authorize(secret string, opts ...AuthorizeOption) func(http.Handler) http.Handler { var authOpts AuthorizeOptions for _, opt := range opts { opt(&authOpts) }

parser := token.NewTokenParser()
return func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tok, err := parser.ParseToken(r, secret, authOpts.PrevSecret)
        if err != nil {
            unauthorized(w, r, err, authOpts.Callback)
            return
        }

        if !tok.Valid {
            unauthorized(w, r, errInvalidToken, authOpts.Callback)
            return
        }

        claims, ok := tok.Claims.(jwt.MapClaims)
        if !ok {
            unauthorized(w, r, errNoClaims, authOpts.Callback)
            return
        }

        ctx := r.Context()
        for k, v := range claims {
            switch k {
            case jwtAudience, jwtExpire, jwtId, jwtIssueAt, jwtIssuer, jwtNotBefore, jwtSubject:
                // ignore the standard claims
            default:
                ctx = context.WithValue(ctx, k, v)
            }
        }

        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

}

> 用了比较经典的可选参数方式创建对象,go-zero里大多是这种方式。
> 最后这里注意,解出的claims的key和value重新被设置到context中,通常业务是使用userid来放到claims中的,这里就相当与把请求用户的userid放回到context,后面业务可以获取到具体请求的用户。

再看看ParseToken

type ( // ParseOption defines the method to customize a TokenParser. ParseOption func(parser *TokenParser)

// A TokenParser is used to parse tokens.
TokenParser struct {
    resetTime     time.Duration
    resetDuration time.Duration
    history       sync.Map
}

)

// NewTokenParser returns a TokenParser. func NewTokenParser(opts ...ParseOption) *TokenParser { parser := &TokenParser{ resetTime: timex.Now(), resetDuration: claimHistoryResetDuration, }

for _, opt := range opts {
    opt(parser)
}

return parser

}

func (tp TokenParser) ParseToken(r http.Request, secret, prevSecret string) (jwt.Token, error) { var token jwt.Token var err error

if len(prevSecret) > 0 {
    count := tp.loadCount(secret)
    prevCount := tp.loadCount(prevSecret)

    var first, second string
    if count > prevCount {
        first = secret
        second = prevSecret
    } else {
        first = prevSecret
        second = secret
    }

    token, err = tp.doParseToken(r, first)
    if err != nil {
        token, err = tp.doParseToken(r, second)
        if err != nil {
            return nil, err
        }

        tp.incrementCount(second)
    } else {
        tp.incrementCount(first)
    }
} else {
    token, err = tp.doParseToken(r, secret)
    if err != nil {
        return nil, err
    }
}

return token, nil

}


> 仍然是可选参数方式创建
> 这里用sync.Map记录了secret的计数器,go-zero作者解释这里计数是因为有新老secret交替,为了更大概率一次就用对。

## 总结
go-zero为了业务快速落地,极简的封装了公共需求,内部有注重效率。对于可以有可以没有的需求,选择了没有,比如token的自动刷新有效期,并没有封装在核心代码中,而是由客户端根据登录返回的有效期时间来自主请求刷新,在生成前端的模板源码中,能看到过半刷新的影子。当然,使用go-zero强制要后端实现刷新,也可以用自定义中间件来实现,只不过每个api服务都要过一遍中间件了。

作者: yue.zhong 创建: 2022/04/07 更新: 2022/04/07 ```

results matching ""

    No results matching ""