Go-Zero项目开发15: IM服务用户JWT鉴权与WebSocket登录实现
纲要用户登录连接需求分析客户端登录流程Token的生成与解析go-zero的 JWT 鉴权原理中间件Authorize核心逻辑密钥与解析器自定义Auth实现基于 JWT新增JwtAuth结构体实现Authenticate与UserID方法配置文件扩充与加载集成到WebSocket服务通过ServerOptions注入自定义鉴权握手阶段的鉴权流程测试验证获取Token并携带至WebSocket连接鉴权失败与成功的表现总结用户登录连接需求分析在即时通讯系统中WebSocket连接建立后需要与用户身份进行绑定。通常客户端首先调用用户服务完成登录获取一个签发的JWT Token然后在发起WebSocket连接时携带该Token。服务端在握手阶段解析并校验Token从中提取用户 ID后续所有消息路由、状态同步都依赖于此身份标识。WebSocket 服务用户服务 (API)客户端WebSocket 服务用户服务 (API)客户端alt[鉴权成功][鉴权失败]登录请求 (用户名/密码)返回 JWT Token发起 WebSocket 连接携带 Token解析 Token获取用户 ID连接建立身份绑定拒绝连接或返回错误其核心在于JWT的解析与校验因此需要深入理解go-zero框架对JWT的处理方式并将其复用至WebSocket服务层。go-zero 的 JWT 鉴权原理go-zero在rest模块中内置了JWT鉴权中间件其核心实现在rest/httpx包内。当为路由启用JWT后框架会通过Authorize中间件执行以下步骤从配置中读取密钥等信息创建jwt.Parser解析器从请求头或 URL 参数中提取Token调用Parse方法解析并验证签名验证通过后将token claims注入context后续业务可通过框架提供的方法获取用户信息。关键代码逻辑如下简化自go-zero源码funcAuthorize(secretstring,opts...JwtOption)func(http.HandlerFunc)http.HandlerFunc{parser:jwt.NewParser(secret)returnfunc(next http.HandlerFunc)http.HandlerFunc{returnfunc(w http.ResponseWriter,r*http.Request){token,err:parser.Parse(r,opts...)iferr!nil{httpx.Error(w,err)return}claims,_:token.Claims.(jwt.MapClaims)ctx:context.WithValue(r.Context(),jwtContextKey,claims)next.ServeHTTP(w,r.WithContext(ctx))}}}我们将在WebSocket鉴权中复用类似思路手动调用解析器成功后将用户信息存入context供后续UserID方法提取。自定义 Auth 实现基于 JWT在之前的工程中我们定义了Auth接口并提供了默认的空实现。现在需要实现一个基于JWT的真正鉴权器。配置文件扩充在etc/im-ws.yaml中增加JWT相关配置Name:im-wsHost:0.0.0.0Port:8888JwtAuth:Secret:your-secret-key-for-jwtExpire:86400在internal/config/config.go中定义对应的结构体并嵌入rest.RestConf保持兼容packageconfigimportgithub.com/zeromicro/go-zero/resttypeConfigstruct{rest.RestConf JwtAuth JwtAuthConfjson:,optional}typeJwtAuthConfstruct{SecretstringExpireint64}实现 JwtAuth创建internal/logic/jwt_auth.go实现Auth接口。我们借助go-zero自带的jwt解析器从请求头Authorization或 URL 参数token中提取并校验Token然后将解析出的用户 ID 注入到context。packagelogicimport(net/httpstringsgithub.com/zeromicro/go-zero/core/logxgithub.com/zeromicro/go-zero/rest/httpxgithub.com/zeromicro/go-zero/rest/jwt)const(jwtUserIdKeyuserId)// JwtAuth 基于 JWT 的用户鉴权实现typeJwtAuthstruct{secretstringparser*jwt.Parser logx.Logger}// NewJwtAuth 创建 JWT 鉴权器funcNewJwtAuth(secretstring)*JwtAuth{returnJwtAuth{secret:secret,parser:jwt.NewParser(secret),Logger:logx.WithContext(nil),}}// Authenticate 实现 Auth 接口校验请求中的 Token 是否合法func(a*JwtAuth)Authenticate(w http.ResponseWriter,r*http.Request)bool{token,err:a.parser.Parse(r)iferr!nil{a.Errorf(JWT parse error: %v,err)httpx.Error(w,err)returnfalse}claims,ok:token.Claims.(jwt.MapClaims)if!ok{httpx.Error(w,jwt.ErrInvalidToken)returnfalse}// 提取用户 ID通常需要根据实际 claims 结构调整userID,exists:claims[jwtUserIdKey]if!exists{httpx.Error(w,jwt.ErrInvalidToken)returnfalse}// 将用户 ID 写入 context供 UserID 方法获取ctx:context.WithValue(r.Context(),jwtUserIdKey,userID)*r*r.WithContext(ctx)returntrue}// UserID 实现 Auth 接口从请求上下文中获取用户 IDfunc(a*JwtAuth)UserID(r*http.Request)string{ifval:r.Context().Value(jwtUserIdKey);val!nil{returnval.(string)}return}注意这里的jwt包为github.com/zeromicro/go-zero/rest/jwt其Parser默认从请求头Authorization提取Bearer token同时也支持从tokenURL 参数读取。MapClaims的userId键值取决于实际签发的JWT数据结构可根据业务自行调整。集成到 WebSocket 服务在main.go中初始化JwtAuth并通过ServerOptions注入到Server中。之前我们预留了WithAuth选项刚好在此使用。// main.go (部分)funcmain(){flag.Parse()varc config.Config conf.MustLoad(*configFile,c)logx.MustSetup(c.Log)// 创建 JwtAuthjwtAuth:logic.NewJwtAuth(c.JwtAuth.Secret)// 创建 ServiceContext传入鉴权实现ctx:svc.NewServiceContext(c,jwtAuth)// 注册路由handler.RegisterRoutes(ctx)logx.Infof(Starting WebSocket service at %s:%d,c.Host,c.Port)ctx.WsServer.Start()}ServiceContext需要相应调整接收Auth接口packagesvcimport(im-ws/internal/configim-ws/internal/logic)typeServiceContextstruct{Config config.Config WsServer*logic.Server}funcNewServiceContext(c config.Config,auth logic.Auth)*ServiceContext{server:logic.NewServer(c.Host:strconv.Itoa(c.Port),logic.WithAuth(auth))returnServiceContext{Config:c,WsServer:server,}}Server构造函数中的鉴权选项设置保持不变。当WebSocket握手时ServeWS方法会先调用auth.Authenticate若失败则直接关闭连接通过后才能进行后续消息路由。测试验证首先启动WebSocket服务。获取合法 Token使用任意方式例如用户服务提供的登录接口获取一个有效的JWT Token假设其claims中包含userId: user123。连接 WebSocket使用ApiPost或websocat连接时需要携带该Token。根据解析器默认规则可在请求头中加入GET ws://localhost:8888/ws Authorization: Bearer your-token-here或在 URL 参数中传递$ websocatws://localhost:8888/ws?tokenyour-token-here连接建立后服务端会在握手阶段执行JwtAuth.Authenticate。若Token无效或缺失框架会返回错误并关闭连接日志中会记录类似JWT parse error: token not found的信息。验证身份绑定连接成功后发送任意消息例如{method:getOnlineUsers,from_id:,data:null}服务端通过s.auth.UserID(r)即可获取当前连接的用户 ID并将其用于业务逻辑。可通过调试日志确认解析出的userId与预期一致。当尝试使用错误或过期的Token连接时连接将被拒绝证明鉴权已生效。总结本文详细介绍了如何将go-zero内置的JWT鉴权能力迁移到WebSocket服务中。通过自定义Auth接口实现我们复用了框架的jwt.Parser在握手阶段完成了Token的解析与校验并将用户 ID 安全地注入连接上下文。这种设计保持了与API服务一致的鉴权体验同时保留了鉴权逻辑的灵活替换能力。至此IM服务的WebSocket模块具备了完整的用户身份识别能力为后续消息路由、状态管理等功能奠定了坚实基础。