Fabric区块链浏览器(2)
本文是区块链浏览器系列的第四篇。
在上一篇文章介绍如何解析区块数据时,使用session
对客户端上传的pb文件进行区分,到期后自动删除。
在这片文章中,会着重介绍下认证系统的实现,主要分为三部分:
- 添加数据库,存储用户信息
- 实现用户认证中间件
- 修改路由
1. 用户信息存储
我这里使用MySQL来存储数据,使用gorm来实现与数据库的交换。
首先需要创建用户表:
CREATE TABLE `users` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL, `password` varchar(100) DEFAULT NULL, `salt` longtext, `created_at` datetime(3) DEFAULT NULL, `updated_at` datetime(3) DEFAULT NULL, `deleted_at` datetime(3) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
创建MySQL链接句柄:
func InitDB(source string) (*gorm.DB, error) { dblog := logger.New( log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{ LogLevel: logger.Error, IgnoreRecordNotFoundError: true, Colorful: true, SlowThreshold: time.Second, }, ) return gorm.Open(mysql.Open(source), &gorm.Config{ SkipDefaultTransaction: true, AllowGlobalUpdate: false, DisableForeignKeyConstraintWhenMigrating: true, Logger: dblog, }) }
表结构比较简单,实现两个查询接口:
func GetUserByName(name string) (*User, error) { var user User db.Get().First(&user, "name = ?", name) if user.ID == 0 { return nil, fmt.Errorf("user with name: %s is not found", name) } return &user, nil } func GetUserByID(id uint) (*User, error) { var user User db.Get().First(&user, "id = ?", id) if user.ID == 0 { return nil, fmt.Errorf("user with id: %d is not found", id) } return &user, nil }
除了查询接口外,还需要提供用户注册,这里直接使用Save()
接口进行数据库写入操作:
func RegisterUser(name, password string) (*LoginResponse, error) { salt := genSalt() u := &User{ Name: name, Password: utils.CalcPassword(password, salt), Salt: salt, } if err := db.Get().Save(u).Error; err != nil { return nil, errors.Wrap(err, "RegisterUser error") } now := time.Now() claims := &jwtv5.RegisteredClaims{ ExpiresAt: jwtv5.NewNumericDate(now.Add(30 * time.Minute)), Issuer: "browser", Subject: fmt.Sprintf("%d", u.ID), } token := jwtv5.NewWithClaims(jwtv5.SigningMethodHS256, claims) tokenString, err := token.SignedString(securityKey) if err != nil { return nil, errors.Wrap(err, "create token error") } return &LoginResponse{ Token: tokenString, Expire: now.Add(30 * time.Minute).Unix(), ID: u.ID, Username: u.Name, }, nil }
用户认证采用的JWT(JSON Web Token),实现方法在JWT介绍有介绍,所以还需要提供两个接口:Login实现token获取,RefreshToken刷新token:
func Login(name, password string) (*LoginResponse, error) { user, err := GetUserByName(name) if err != nil { return nil, errors.Wrap(err, "GetUserByName error") } if utils.CalcPassword(password, user.Salt) != user.Password { return nil, errors.New("user name or password is incorrect") } now := time.Now() claims := &jwtv5.RegisteredClaims{ ExpiresAt: jwtv5.NewNumericDate(now.Add(30 * time.Minute)), Issuer: "browser", Subject: fmt.Sprintf("%d", user.ID), } token := jwtv5.NewWithClaims(jwtv5.SigningMethodHS256, claims) tokenString, err := token.SignedString(securityKey) if err != nil { return nil, errors.Wrap(err, "create token error") } return &LoginResponse{ Token: tokenString, Expire: now.Add(30 * time.Minute).Unix(), ID: user.ID, Username: user.Name, }, nil } func RefreshToken(id uint) (*LoginResponse, error) { user, err := GetUserByID(id) if err != nil { return nil, errors.Wrap(err, "GetUserByName error") } now := time.Now() claims := &jwtv5.RegisteredClaims{ ExpiresAt: jwtv5.NewNumericDate(now.Add(30 * time.Minute)), Issuer: "browser", Subject: fmt.Sprintf("%d", user.ID), } token := jwtv5.NewWithClaims(jwtv5.SigningMethodHS256, claims) tokenString, err := token.SignedString(securityKey) if err != nil { return nil, errors.Wrap(err, "create token error") } return &LoginResponse{ Token: tokenString, Expire: now.Add(30 * time.Minute).Unix(), ID: user.ID, Username: user.Name, }, nil }
2. 用户认证中间件
关于Gin中间件的开发,可以参照gin中间件开发,这里增加三种认证方式:noAuth,不使用认证;basicAuth,用户名密码方式认证;tokenAuth,使用token进行认证:
func noAuth(ctx *gin.Context) { ctx.Next() } func basicAuth(ctx *gin.Context) { name, pwd, ok := ctx.Request.BasicAuth() if !ok { srvLogger.Error("basic auth failed") ctx.JSON(http.StatusForbidden, gin.H{"code": http.StatusForbidden, "msg": "basic auth failed"}) ctx.Abort() return } user, err := data.GetUserByName(name) if err != nil { srvLogger.Errorf("GetUserByName error: %s", err.Error()) ctx.JSON(http.StatusForbidden, gin.H{"code": http.StatusForbidden, "msg": err.Error()}) ctx.Abort() return } if utils.CalcPassword(pwd, user.Salt) != user.Password { srvLogger.Error("user name or password is incorrect") ctx.JSON(http.StatusForbidden, gin.H{"code": http.StatusForbidden, "msg": "user name or password is incorrect"}) ctx.Abort() return } ctx.Next() } func tokenAuth(ctx *gin.Context) { if err := data.ParseJWT(strings.Split(ctx.Request.Header.Get("Authorization"), " ")[1]); err != nil { srvLogger.Errorf("tokenAuth error: %s", err.Error()) ctx.JSON(http.StatusForbidden, gin.H{"code": http.StatusForbidden, "msg": "token auth failed"}) ctx.Abort() return } ctx.Next() }
3. 注册路由
在上篇中,注册的路由是这样的:
engine.POST("/login", login) engine.GET("/hi/:name", sayHi) engine.POST("/block/upload", upload) engine.GET("/block/parse/:msgType", parse) engine.POST("/block/update/:channel", updateConfig)
现在需要对/block/upload
、/block/parse/:msgType
、/block/update/:channel
接口增加认证,这就需要用到我们上面实现的三个中间件。
由于中间件会按照它们的注册顺利来执行,所以需要认证中间件需要在相应的处理接口前执行,针对noAuth的情况,上面的代码并不需要进行修改,但对于basicAuth、tokenAuth,上面的代码就需要修改了:
engine.POST("/block/upload", basicAuth, upload) engine.GET("/block/parse/:msgType", basicAuth, parse) engine.POST("/block/update/:channel", basicAuth, updateConfig)
或
engine.POST("/block/upload", tokenAuth, upload) engine.GET("/block/parse/:msgType", tokenAuth, parse) engine.POST("/block/update/:channel", tokenAuth, updateConfig)
当然我们也可以使用Handle(httpMethod, relativePath string, handlers ...HandlerFunc)
来进行路由注册:
for _, router := range server.Routers() { var handlers []gin.HandlerFunc if router.AuthType == 0 { router.AuthType = conf.AuthType } switch router.AuthType { case config.Server_BASICAUTH: handlers = append(handlers, basicAuth) case config.Server_TOKENAUTH: handlers = append(handlers, tokenAuth) default: handlers = append(handlers, noAuth) } handlers = append(handlers, router.Handler) engine.Handle(router.Method, router.Path, handlers...) }
项目完整代码可以从Github上查看。

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 恋水无意
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程