导言
本人最近在写一个聊天室项目练手,最开始是一个极简陋的基于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 中的数据(如 id 和 username)。
完成这些后客户端就会获得一个 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成功的时候确实很开心。
最近时间学长的学习过程,说实话很震惊,一年时间学的又多又深。感觉前方的路途很遥远,不过才刚刚开始,希望我也能成为一名编程糕手
絮絮叨:好累啊,一写就没停下来,国庆还是得好好玩一下
27万+

被折叠的 条评论
为什么被折叠?



