在典型的业务场景中, 认证与鉴权是十分基础的.
对于 API 接口, 通常是在第一次验证之后生成一个带有时效的 token.
接下来的一系列请求都携带这个 token, 服务器会对这个 token 进行验证.
介绍 jwt
JSON Web Tokens(jwt) 是一种用于在两个主体间传递认证消息的方式.
注意, 消息是通过数字签名的, 因此可以被验证和信任, 但却不是加密的.
一个 jwt 由三部分组成:
- Header
- Payload
- Signature
Header 部分通常只有两个字段, 分别定义了签名算法和 token 类型.
{
"alg": "HS256",
"typ": "JWT"
}
Payload 部分是实际负载, 用于声明. 通常存储一些用户 ID 之类的索引数据,
也可以放一些其他有用的信息. 注意, 不要存储机密数据.
jwt 规范也在 Payload 中预定义了推荐字段, 但非强制的, 但很多库都会遵照着实现.
比如 iss 字段定义发布者, exp 定义 token 的过期时间. 更多字段可以在
rfc7519 规范
中查看.
Signature 就是签名了, 大致样式如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
将 header 和 payload 分别使用 base64UrlEncode 编码, 中间加上点号.
,
然后使用 header 中指定的签名算法.
签名用于发布者验证消息没有被篡改, 如果使用非对称加密, 还可以验证发布者身份.
jwt 通常用在请求头的 Authorization
字段中, 形如:
Authorization: Bearer <token>
更多内容可以参考 Introduction to JSON Web Tokens.
实践
对于如何使用 jwt, 应该是非常清晰了.
首先, 我们要定义一个接口签发 jwt.
获取到 token 之后, 就可以在请求其他资源时带上这个 token.
验证环节可以用到上一节中讲到的中间件技术.
jwt.io 页面上列出了很多 Go 库,
这里选择功能最全的 github.com/gbrlsnchs/jwt/v3
.
go get -u github.com/gbrlsnchs/jwt/v3
定义功能
对于 jwt 有两个必须实现的功能, 签发 token 和验证 token.
首先, 首先定义 Payload 内容, 这里保持用户的 ID 和昵称.
// 记录登录信息的 JWT
type LoginToken struct {
jwt.Payload
ID uint `json:"id"`
Username string `json:"username"`
}
然后选择签名方法.
// 签名算法, 随机, 不保存密钥, 每次都是随机的
var privateKey, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
var publicKey = &privateKey.PublicKey
var hs = jwt.NewES256(
jwt.ECDSAPublicKey(publicKey),
jwt.ECDSAPrivateKey(privateKey),
)
编写签发和验证函数.
// 签名
func Sign(id uint, username string) (string, error) {
now := time.Now()
pl := LoginToken{
Payload: jwt.Payload{
Issuer: "coolcat",
Subject: "login",
Audience: jwt.Audience{},
ExpirationTime: jwt.NumericDate(now.Add(7 * 24 * time.Hour)),
NotBefore: jwt.NumericDate(now.Add(30 * time.Minute)),
IssuedAt: jwt.NumericDate(now),
JWTID: uuid.NewV4().String(),
},
ID: id,
Username: username,
}
token, err := jwt.Sign(pl, hs)
return string(token), err
}
// 验证
func Verify(token []byte) (*LoginToken, error) {
pl := &LoginToken{}
_, err := jwt.Verify(token, hs, pl)
return pl, err
}
签发接口
首先, 构建一个接口签发 jwt.
func Login(ctx *gin.Context) {
var u model.UserModel
// 应该使用 ShouldBindJSON, 以便使用自定义的 handler.SendResponse
if err := ctx.ShouldBindJSON(&u); err != nil {
handler.SendResponse(ctx, errno.New(errno.ErrBind, err), nil)
return
}
user, err := model.GetUserByName(u.Username)
if err != nil {
handler.SendResponse(ctx, errno.New(errno.ErrDatabase, err), nil)
return
}
if err := user.Compare(u.Password); err != nil {
handler.SendResponse(ctx, errno.New(errno.ErrPasswordIncorrect, err), nil)
return
}
// 签发 token
t, err := token.Sign(user.ID, user.Username)
if err != nil {
handler.SendResponse(ctx, errno.New(errno.ErrTokenSign, err), nil)
return
}
handler.SendResponse(ctx, nil, model.Token{Token: t})
}
用户传递用户名和密码, 通过验证后返回 jwt.
验证中间件
因为验证可能有很多接口都用得到, 所以写成中间件是最自然的方式.
前面介绍过标准的传递 jwt 的方式是存储在 Authorization 请求头中,
Authorization: Bearer <token>
所以, 这里也依据这种规范来验证 jwt.
// AuthJWT 验证 JWT 的中间件
func AuthJWT() gin.HandlerFunc {
return func(ctx *gin.Context) {
header := ctx.GetHeader("Authorization")
headerList := strings.Split(header, " ")
if len(headerList) != 2 {
err := errors.New("无法解析 Authorization 字段")
handler.SendResponse(ctx, errno.New(errno.ErrTokenInvalid, err), nil)
ctx.Abort()
return
}
t := headerList[0]
content := headerList[1]
if t != "Bearer" {
err := errors.New("认证类型错误, 当前只支持 Bearer")
handler.SendResponse(ctx, errno.New(errno.ErrTokenInvalid, err), nil)
ctx.Abort()
return
}
if _, err := token.Verify([]byte(content)); err != nil {
handler.SendResponse(ctx, errno.New(errno.ErrTokenInvalid, err), nil)
ctx.Abort()
return
}
ctx.Next()
}
}
使用
定义好中间件后, 就可以在 router 中使用了.
g.POST("/v1/create", user.Create) // 为了方便创建用户, 无需认证
u := g.Group("/v1/user")
u.Use(middleware.AuthJWT()) // 添加认证
{
u.GET("", user.List)
u.POST("", user.Create)
u.GET("/:id", user.Get)
u.PUT("/:id", user.Save)
u.PATCH("/:id", user.Update)
u.DELETE("/:id", user.Delete)
}
这里为了方便, 有个创建用户的接口放在了外边, 逃避了 jwt 验证.
不然一开始没有用户又无法创建挺尴尬的.
总结
认证与鉴权是 API 接口比不可少的一部分, 这里介绍了 jwt.
更复杂强大的授权协议是 OAuth 2.0,
OAuth 2.0 更多用在协作共享资源上, 对于简单的 API 服务器, jwt 就足够了.
jwt 也可以作为 OAuth 2.0 的一部分, 用于承载内容.
当前部分的代码
作为版本 v0.9.0
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。