宝藏工具!基于gin的golang网站开发的认证利器jwt

代码小能手 · · 495 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

JSON Web令牌(JWT)是一种很流行的跨域认证解决方案,JWT基于JSON可以在进行验证的同时附带身份信息,对于之前分离项目很有帮助。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT由三部分组成,每个部分之间用点.替换,分别称为HEADER,PAYLOAD和VERIFYSIGNATURE。HEADER和PAYLOAD通过base64解码后为JSON明文。

HEADER包含两个长度,指定的algJWT的签名算法,typ固定为JWT。
有效载荷中包含JWT的声明信息,标准中定义了iss,sub,aud等声明字段,如果标准声明不够用的话,我们还可以增加自定义声明。要注意两点,第一有效载荷只是经过的base64编码,几乎就等于是明文,不要包含敏感信息。第二不要在PAYLOAD中加入过多的信息,因为验证通过以后每一个请求都要包含JWT,信息太多的话会造成一些没有必要的资源浪费。
VERIFY SIGNATURE为使用HEADER中指定的算法生成的签名。例如签名alg:HS256算法HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),密钥)
了解完JWT的基本原理之后,我们来看一下在gin中是怎么使用JWT的。

约会gin-jwt中间件
在Gin中使用jwt有个开源项目gin-jwt,这项目几乎包含了我们要用到的一切。例如定义PAYLOAD中的声明,授权验证的方法,是否使用COOKIE等。

package main

import (
"log"
"net/http"
"os"
"time"

jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"

)

type login struct {
Username string form:"username" json:"username" binding:"required"
Password string form:"password" json:"password" binding:"required"
}

var identityKey = "id"

func helloHandler(c gin.Context) {
claims := jwt.ExtractClaims(c)
user, _ := c.Get(identityKey)
c.JSON(200, gin.H{
"userID": claims[identityKey],
"userName": user.(
User).UserName,
"text": "Hello World.",
})
}

type User struct {
UserName string
FirstName string
LastName string
}

func main() {
port := os.Getenv("PORT")
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())

if port == "" {
    port = "8000"
}

authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
    Realm:       "test zone",
    Key:         []byte("secret key"),
    Timeout:     time.Hour,
    MaxRefresh:  time.Hour,
    IdentityKey: identityKey,
    PayloadFunc: func(data interface{}) jwt.MapClaims {
        if v, ok := data.(*User); ok {
            return jwt.MapClaims{
                identityKey: v.UserName,
            }
        }
        return jwt.MapClaims{}
    },
    IdentityHandler: func(c *gin.Context) interface{} {
        claims := jwt.ExtractClaims(c)
        return &User{
            UserName: claims[identityKey].(string),
        }
    },
    Authenticator: func(c *gin.Context) (interface{}, error) {
        var loginVals login
        if err := c.ShouldBind(&loginVals); err != nil {
            return "", jwt.ErrMissingLoginValues
        }
        userID := loginVals.Username
        password := loginVals.Password

        if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
            return &User{
                UserName:  userID,
                LastName:  "Bo-Yi",
                FirstName: "Wu",
            }, nil
        }

        return nil, jwt.ErrFailedAuthentication
    },
    Authorizator: func(data interface{}, c *gin.Context) bool {
        if v, ok := data.(*User); ok && v.UserName == "admin" {
            return true
        }

        return false
    },
    Unauthorized: func(c *gin.Context, code int, message string) {
        c.JSON(code, gin.H{
            "code":    code,
            "message": message,
        })
    },

    TokenLookup: "header: Authorization, query: token, cookie: jwt",
    TokenHeadName: "Bearer",
    TimeFunc: time.Now,
})

if err != nil {
    log.Fatal("JWT Error:" + err.Error())
}

errInit := authMiddleware.MiddlewareInit()

if errInit != nil {
    log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error())
}

r.POST("/login", authMiddleware.LoginHandler)

r.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) {
    claims := jwt.ExtractClaims(c)
    log.Printf("NoRoute claims: %#v\n", claims)
    c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
})

auth := r.Group("/auth")
auth.GET("/refresh_token", authMiddleware.RefreshHandler)
auth.Use(authMiddleware.MiddlewareFunc())
{
    auth.GET("/hello", helloHandler)
}

if err := http.ListenAndServe(":"+port, r); err != nil {
    log.Fatal(err)
}

}
我们可以看到jwt.GinJWTMiddleware用于声明一个中间件。PayloadFunc方法中给放置的PAYLOAD增加了id长度,获取了用户名。Authenticator认证器,我们可以在这里验证用户身份,参数为* gin.Context,所以在这里我们可以像写Gin Handler那样获取到HTTP请求中的各种内容。Authorizator授权器可以判断当前JWT是否有权限继续访问。当然还可以设置像过期的时间,密钥,是否设置COOKIE等其他。选项。

登录处理程序
以上r.POST("/login", authMiddleware.LoginHandler)示例中配置了路由下面我们来看一下登录过程是怎样的。

func (mw *GinJWTMiddleware) LoginHandler(c *gin.Context) {
if mw.Authenticator == nil {
mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(ErrMissingAuthenticatorFunc, c))
return
}

data, err := mw.Authenticator(c)

if err != nil {
    mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(err, c))
    return
}

// Create the token
token := jwt.New(jwt.GetSigningMethod(mw.SigningAlgorithm))
claims := token.Claims.(jwt.MapClaims)

if mw.PayloadFunc != nil {
    for key, value := range mw.PayloadFunc(data) {
        claims[key] = value
    }
}

expire := mw.TimeFunc().Add(mw.Timeout)
claims["exp"] = expire.Unix()
claims["orig_iat"] = mw.TimeFunc().Unix()
tokenString, err := mw.signedString(token)

if err != nil {
    mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrFailedTokenCreation, c))
    return
}

// set cookie
if mw.SendCookie {
    expireCookie := mw.TimeFunc().Add(mw.CookieMaxAge)
    maxage := int(expireCookie.Unix() - mw.TimeFunc().Unix())

    if mw.CookieSameSite != 0 {
        c.SetSameSite(mw.CookieSameSite)
    }

    c.SetCookie(
        mw.CookieName,
        tokenString,
        maxage,
        "/",
        mw.CookieDomain,
        mw.SecureCookie,
        mw.CookieHTTPOnly,
    )
}

mw.LoginResponse(c, http.StatusOK, tokenString, expire)

}
LoginHandler整体逻辑还是比较简单的,检查并调用前面的设置的Authorenticator方法,验证成功的话生成一个新的JWT,调用PayloadFunc方法设置PAYLOAD的自定义标题,根据SendCookie确定是否需要在HTTP中设置COOKIE,最后调用登录响应方法设置返回值。

使用中间件
jwt-gin包提供了一个标准的Gin中间件,我们可以在需要验证JWT的路由上设置/auth中间件auth.Use(authMiddleware.MiddlewareFunc())。

func (mw *GinJWTMiddleware) MiddlewareFunc() gin.HandlerFunc {
return func(c *gin.Context) {
mw.middlewareImpl(c)
}
}

func (mw *GinJWTMiddleware) middlewareImpl(c *gin.Context) {
claims, err := mw.GetClaimsFromJWT(c)
if err != nil {
mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(err, c))
return
}

if claims["exp"] == nil {
    mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrMissingExpField, c))
    return
}

if _, ok := claims["exp"].(float64); !ok {
    mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrWrongFormatOfExp, c))
    return
}

if int64(claims["exp"].(float64)) < mw.TimeFunc().Unix() {
    mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrExpiredToken, c))
    return
}

c.Set("JWT_PAYLOAD", claims)
identity := mw.IdentityHandler(c)

if identity != nil {
    c.Set(mw.IdentityKey, identity)
}

if !mw.Authorizator(identity, c) {
    mw.unauthorized(c, http.StatusForbidden, mw.HTTPStatusMessageFunc(ErrForbidden, c))
    return
}

c.Next()

}
GetClaimsFromJWT方法在当前位置中获取JWT,失败的话返回未授权。接着会判断JWT是否过期,最后前面设置的Authoror方法验证是否有权限继续访问。

作者:陈宏博说他再想想
链接:https://juejin.cn/post/6904129867302830088
来源:掘金


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:代码小能手

查看原文:宝藏工具!基于gin的golang网站开发的认证利器jwt

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

495 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传