从JWT到hertzJWT实战

【投稿赢 iPhone 17】「我的第一个开源项目」故事征集:用代码换C位出道! 10w+人浏览 1.5k人参与

导言

本人最近在写一个聊天室项目练手,最开始是一个极简陋的基于websocket的聊天室框架这里,于是想把它展开,做一个较为规范化的项目,在分包后将框架升级为了hertz并添加了用户的注册与登录模块,其中用到了JWT。于是我从0学习JWT并将其运用于实战,本文用于记录这段经历,并欢迎相互交流

1.JWT(Json Web Token)

JWT (JSON Web Token) 是一种开放标准 (RFC 7519),用于在各方之间安全地传输信息作为 JSON 对象。这种信息可以被验证和信任,因为它是经过数字签名的。

JWT由三部分构成,分别是:

Header(头部):

头部主要包含了签名算法和令牌类型

{
  "alg": "HS256",  // 签名算法(HS256表示HMAC SHA-256)
  "typ": "JWT"     // 令牌类型
}
Payload(负载):

包含声明(claims),也是一个JSON对象,储存了在请求中需要用到的数据,例如

{
  "sub": "0721",  //主题
  "name": "mao",  //名称
  "admin": true   //是否管理员
}
3. Signature(签名

签名有header,payload和一个密钥通过头部的算法编码而成

jwt流程

2.Hertz框架下的JWT实战

官方文档

重点是用JWT的是用户的登陆部分以及之后受到保护的路由,在实践中我建立了一个浅显的认知,用户通过登录获得了一张具有信息(比如过期时间等)的游乐园门票(保存在本地),每次游玩游乐园的时候必须出示门票才能游玩,一旦过期等就不能使用

核心代码:

因为仅用作个人学习使用,以及经验浅薄,错误处理写的方便个人查找,可能具有安全漏洞,等等不足之处,欢迎指出

package middleware
​
import (
    "context"
    "fmt"
    "log"
    "talkSpace/dao"
    "talkSpace/model"
    "time"
​
    "github.com/cloudwego/hertz/pkg/app"
    "github.com/hertz-contrib/jwt"
)
​
type LoginUser struct{
    UserName string
    ID int
}
​
​
​
func InitJwtMiddleware() *jwt.HertzJWTMiddleware{
    authMiddle,err := jwt.New(&jwt.HertzJWTMiddleware{
        Realm: "protect",
        Key: []byte(model.GlobalConfig.JwtConfig.Jwt.Secret),//此处为安全密钥
        Timeout: time.Hour * 24,
        MaxRefresh: time.Hour * 24 * 30,
        IdentityKey: "id",//登录成功时: 库会从你的用户数据中读取用户的唯一标识符(例如,数据库中的 ID),并将它存储到 JWT 载荷中,使用的键名就是 "id"
​
        //核心:处理登陆请求,验证数据库凭证
        Authenticator: func(c context.Context, ctx *app.RequestContext) (interface{}, error) {
            var user model.User
            if err := ctx.BindAndValidate(&user);err != nil{
                return nil,jwt.ErrMissingLoginValues
            }
            if user.Username =="" || user.Password ==""{
                return nil,fmt.Errorf("不允许出现空用户名或密码")
            }
            user1,err := dao.JudgeUsername(user.Username,user.Password)
            if err != nil{
                return nil,err
            }
            //使用一个新结构体来确保不会将密码等敏感内容放入负荷
            return &LoginUser {
                UserName: user1.Username,
                ID: user.ID,
            },nil
​
        },
​
        //认证成功后将用户数据转换为payload
        PayloadFunc: func(data interface{}) jwt.MapClaims {
            if v, ok := data.(*LoginUser);ok{
                //存入负荷
                return jwt.MapClaims{
                    "id": v.ID,
                    "username": v.UserName,
                }
            }
            
        return jwt.MapClaims{}
        },
​
        //登陆成功后每次请求从token中提取用户信息
        IdentityHandler: func(ctx context.Context, c *app.RequestContext) interface{} {
            claims := jwt.ExtractClaims(ctx, c)
            userID := int(claims["id"].(float64))
            userName := claims["username"].(string)
            return &LoginUser{
                UserName: userName,
                ID: userID,
            }
        },
​
        TokenLookup: "header:Authorization,query:token",
​
    })
    if err != nil{
        log.Fatalf("jwt初始化失败")
    }
    return authMiddle
}
配置
Realm: "protect",     //作用域
Key: []byte(model.GlobalConfig.JwtConfig.Jwt.Secret),//此处为安全密钥
Timeout: time.Hour * 24,            //token过期时间
MaxRefresh: time.Hour * 24 * 30,    //用于设置最大 token 刷新时间,即通过刷新token过期时间以达到的最长token持有时间
IdentityKey: "id",                  ////登录成功时: 库会从你的用户数据中读取用户的唯一标识符(例如,数据库中的 ID),并将它存储到 JWT 载荷中,使用的键名就是 "id"

更多配置详见官方文档

处理登录

首先我们需要处理用户的登录请求并向PayloadFun发送数据来写入负载

 //核心:处理登陆请求,验证数据库凭证
Authenticator: func(c context.Context, ctx *app.RequestContext) (interface{}, error) {
    var user model.User    //创建一个实例来接受用户传入的信息,比如用户名,密码 
    if err := ctx.BindAndValidate(&user);err != nil{  //绑定并验证
        return nil,jwt.ErrMissingLoginValues
    }
    if user.Username =="" || user.Password ==""{       //防止输入空值
                return nil,fmt.Errorf("不允许出现空用户名或密码")
    }
    user1,err := dao.JudgeUsername(user.Username,user.Password)   //此处为自己封装的一个与mysql的用户数据比较,返回用户不存在或者密码错误的error和model.User
    if err != nil{
        return nil,err
    }
    //使用一个新结构体来确保不会将密码等敏感内容放入负荷
    return &LoginUser {
        UserName: user1.Username,
        ID: user.ID,
    },nil
​
},
认证成功后将用户数据转换为payload
PayloadFunc: func(data interface{}) jwt.MapClaims {
            if v, ok := data.(*LoginUser);ok{  //因为data是interface{},编译器无法确认类型,因此需要类型断言
                //存入负载
                return jwt.MapClaims{
                    "id": v.ID,
                    "username": v.UserName,
                }
            }
            
        return jwt.MapClaims{}
        },

Authenticator 函数负责验证用户(检查用户名和密码是否正确)。

一旦验证成功,它返回一个包含用户信息的 Go 结构体 (&LoginUser)。

PayloadFunc 函数接收这个用户信息 (*LoginUser),并将其转换为 JWT 的负载(Claims),也就是要存入 Token 中的数据(如 idusername)。

完成这些后客户端就会获得一个 JWT,此后客户端在后续的每次请求中都需要携带这个 Token,作为身份认证的凭证

用户请求
//登陆成功后每次请求从token中提取用户信息
IdentityHandler: func(ctx context.Context, c *app.RequestContext) interface{} { //c是客户端请求的上下文,携带数据
    claims := jwt.ExtractClaims(ctx, c)         //读取负载,函数返回的值底层还是一个空接口,因此仍然需要类型断言
    
    //userID := claims["id"].(int),出现问题的原因当你使用 github.com/hertz-contrib/jwt 或大多数标准 JWT 库时,JWT Claims(Payload)在解析时通常遵循 JSON 规范。在 Go 语言中,标准库的 JSON 解码器在解析 不带小数点的数字 时,默认会将其解析为 float64 类型,而不是 int。
    
    userID := int(claims["id"].(float64))       
    userName := claims["username"].(string)
    return &LoginUser{
        UserName: userName,
        ID: userID,
    }
},

路由
package router
​
import (
    "talkSpace/service"
    "talkSpace/utils/middleware"
​
    "github.com/cloudwego/hertz/pkg/app/server"
)
​
func InitRouter(h *server.Hertz){
    authMiddle := middleware.InitJwtMiddleware()
    h.Static("/","./static/index.html")
    h.POST("/register",service.UserRegister)
    h.POST("/login",authMiddle.LoginHandler) //Authenticator与HertzJWTMiddleware.LoginHandler配合,登录时触发,用于认证用户的登录信息。
    h.GET("/refresh_token", authMiddle.RefreshHandler)
    apiGroup := h.Group("/api/v1",authMiddle.MiddlewareFunc()) //注意下文提示
    {
        apiGroup.GET("/talkSpace",service.HandleConnection)
    }
    go service.HandleMessages()
}
提示

因为 JWT 的核心是认证授权,所以在使用 Hertz 的 jwt 扩展时,不仅需要为 /login 接口绑定认证逻辑 authMiddleware.LoginHandler

还要以中间件的方式,为需要授权访问的路由组注入授权逻辑 authMiddleware.MiddlewareFunc()

结语

本文是golang小白第一次写项目,边写边学了很多东西,写了很久。中间的过程很苦,但是每个模块run成功的时候确实很开心。

最近时间学长的学习过程,说实话很震惊,一年时间学的又多又深。感觉前方的路途很遥远,不过才刚刚开始,希望我也能成为一名编程糕手

絮絮叨:好累啊,一写就没停下来,国庆还是得好好玩一下

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值