Fabric区块链浏览器(2):用户认证

2023-10-16 20:01:33 浏览数 (1)

本文是区块链浏览器系列的第四篇。

在上一篇文章[1]介绍如何解析区块数据时,使用session对客户端上传的pb文件进行区分,到期后自动删除。

在这片文章中,会着重介绍下认证系统的实现,主要分为三部分:

•添加数据库,存储用户信息

•实现用户认证中间件

•修改路由

1. 用户信息存储

我这里使用MySQL来存储数据,使用gorm来实现与数据库的交换。

首先需要创建用户表:

代码语言:javascript复制
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链接句柄:

代码语言:javascript复制
func InitDB(source string) (*gorm.DB, error) {
    dblog := logger.New(
        log.New(os.Stdout, "rn", 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,
    })
}

表结构比较简单,实现两个查询接口:

代码语言:javascript复制
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()接口进行数据库写入操作:

代码语言:javascript复制
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介绍[2]有介绍,所以还需要提供两个接口:Login实现token获取,RefreshToken刷新token:

代码语言:javascript复制
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中间件开发[3],这里增加三种认证方式:noAuth,不使用认证;basicAuth,用户名密码方式认证;tokenAuth,使用token进行认证:

代码语言:javascript复制
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. 注册路由

在上篇[4]中,注册的路由是这样的:

代码语言:javascript复制
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的情况,上面的代码并不需要进行修改,但对于basicAuthtokenAuth,上面的代码就需要修改了:

代码语言:javascript复制
engine.POST("/block/upload", basicAuth, upload)
engine.GET("/block/parse/:msgType", basicAuth, parse)
engine.POST("/block/update/:channel", basicAuth, updateConfig)

代码语言:javascript复制
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)来进行路由注册:

代码语言:javascript复制
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[5]上查看。

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)[6]进行许可,使用时请注明出处。 Author: mengbin[7] blog: mengbin[8] Github: mengbin92[9] cnblogs: 恋水无意[10]

References

[1] 上一篇文章: https://mengbin.top/2023-08-13-blockBrowser/ [2] JWT介绍: https://mengbin.top/2023-08-17-jwt/ [3] gin中间件开发: https://mengbin.top/2023-08-10-middleware/ [4] 上篇: https://mengbin.top/2023-08-13-blockBrowser/ [5] Github: https://github.com/mengbin92/browser/tree/gin [6] 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0): https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh [7] mengbin: mengbin1992@outlook.com [8] mengbin: https://mengbin.top [9] mengbin92: https://mengbin92.github.io/ [10] 恋水无意: https://www.cnblogs.com/lianshuiwuyi/

0 人点赞