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
- 添加配置定义和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))
}
鉴权 在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 ```