Fabric区块链浏览器(2)

打印 上一主题 下一主题

主题 883|帖子 883|积分 2649

本文是区块链浏览器系列的第四篇。
上一篇文章介绍如何解析区块数据时,使用session对客户端上传的pb文件进行区分,到期后自动删除。
在这片文章中,会着重介绍下认证系统的实现,主要分为三部分:

  • 添加数据库,存储用户信息
  • 实现用户认证中间件
  • 修改路由
1. 用户信息存储

我这里使用MySQL来存储数据,使用gorm来实现与数据库的交换。
首先需要创建用户表:
  1. CREATE TABLE `users` (
  2.   `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  3.   `name` varchar(100) NOT NULL,
  4.   `password` varchar(100) DEFAULT NULL,
  5.   `salt` longtext,
  6.   `created_at` datetime(3) DEFAULT NULL,
  7.   `updated_at` datetime(3) DEFAULT NULL,
  8.   `deleted_at` datetime(3) DEFAULT NULL,
  9.   PRIMARY KEY (`id`)
  10. ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
复制代码
创建MySQL链接句柄:
  1. func InitDB(source string) (*gorm.DB, error) {
  2.         dblog := logger.New(
  3.                 log.New(os.Stdout, "\r\n", log.LstdFlags),
  4.                 logger.Config{
  5.                         LogLevel:                  logger.Error,
  6.                         IgnoreRecordNotFoundError: true,
  7.                         Colorful:                  true,
  8.                         SlowThreshold:             time.Second,
  9.                 },
  10.         )
  11.         return gorm.Open(mysql.Open(source), &gorm.Config{
  12.                 SkipDefaultTransaction:                   true,
  13.                 AllowGlobalUpdate:                        false,
  14.                 DisableForeignKeyConstraintWhenMigrating: true,
  15.                 Logger:                                   dblog,
  16.         })
  17. }
复制代码
表结构比较简单,实现两个查询接口:
  1. func GetUserByName(name string) (*User, error) {
  2.         var user User
  3.         db.Get().First(&user, "name = ?", name)
  4.         if user.ID == 0 {
  5.                 return nil, fmt.Errorf("user with name: %s is not found", name)
  6.         }
  7.         return &user, nil
  8. }
  9. func GetUserByID(id uint) (*User, error) {
  10.         var user User
  11.         db.Get().First(&user, "id = ?", id)
  12.         if user.ID == 0 {
  13.                 return nil, fmt.Errorf("user with id: %d is not found", id)
  14.         }
  15.         return &user, nil
  16. }
复制代码
除了查询接口外,还需要提供用户注册,这里直接使用Save()接口进行数据库写入操作:
  1. func RegisterUser(name, password string) (*LoginResponse, error) {
  2.         salt := genSalt()
  3.         u := &User{
  4.                 Name:     name,
  5.                 Password: utils.CalcPassword(password, salt),
  6.                 Salt:     salt,
  7.         }
  8.         if err := db.Get().Save(u).Error; err != nil {
  9.                 return nil, errors.Wrap(err, "RegisterUser error")
  10.         }
  11.         now := time.Now()
  12.         claims := &jwtv5.RegisteredClaims{
  13.                 ExpiresAt: jwtv5.NewNumericDate(now.Add(30 * time.Minute)),
  14.                 Issuer:    "browser",
  15.                 Subject:   fmt.Sprintf("%d", u.ID),
  16.         }
  17.         token := jwtv5.NewWithClaims(jwtv5.SigningMethodHS256, claims)
  18.         tokenString, err := token.SignedString(securityKey)
  19.         if err != nil {
  20.                 return nil, errors.Wrap(err, "create token error")
  21.         }
  22.         return &LoginResponse{
  23.                 Token:    tokenString,
  24.                 Expire:   now.Add(30 * time.Minute).Unix(),
  25.                 ID:       u.ID,
  26.                 Username: u.Name,
  27.         }, nil
  28. }
复制代码
用户认证采用的JWT(JSON Web Token),实现方法在JWT介绍有介绍,所以还需要提供两个接口:Login实现token获取,RefreshToken刷新token:
  1. func Login(name, password string) (*LoginResponse, error) {
  2.         user, err := GetUserByName(name)
  3.         if err != nil {
  4.                 return nil, errors.Wrap(err, "GetUserByName error")
  5.         }
  6.         if utils.CalcPassword(password, user.Salt) != user.Password {
  7.                 return nil, errors.New("user name or password is incorrect")
  8.         }
  9.         now := time.Now()
  10.         claims := &jwtv5.RegisteredClaims{
  11.                 ExpiresAt: jwtv5.NewNumericDate(now.Add(30 * time.Minute)),
  12.                 Issuer:    "browser",
  13.                 Subject:   fmt.Sprintf("%d", user.ID),
  14.         }
  15.         token := jwtv5.NewWithClaims(jwtv5.SigningMethodHS256, claims)
  16.         tokenString, err := token.SignedString(securityKey)
  17.         if err != nil {
  18.                 return nil, errors.Wrap(err, "create token error")
  19.         }
  20.         return &LoginResponse{
  21.                 Token:    tokenString,
  22.                 Expire:   now.Add(30 * time.Minute).Unix(),
  23.                 ID:       user.ID,
  24.                 Username: user.Name,
  25.         }, nil
  26. }
  27. func RefreshToken(id uint) (*LoginResponse, error) {
  28.         user, err := GetUserByID(id)
  29.         if err != nil {
  30.                 return nil, errors.Wrap(err, "GetUserByName error")
  31.         }
  32.         now := time.Now()
  33.         claims := &jwtv5.RegisteredClaims{
  34.                 ExpiresAt: jwtv5.NewNumericDate(now.Add(30 * time.Minute)),
  35.                 Issuer:    "browser",
  36.                 Subject:   fmt.Sprintf("%d", user.ID),
  37.         }
  38.         token := jwtv5.NewWithClaims(jwtv5.SigningMethodHS256, claims)
  39.         tokenString, err := token.SignedString(securityKey)
  40.         if err != nil {
  41.                 return nil, errors.Wrap(err, "create token error")
  42.         }
  43.         return &LoginResponse{
  44.                 Token:    tokenString,
  45.                 Expire:   now.Add(30 * time.Minute).Unix(),
  46.                 ID:       user.ID,
  47.                 Username: user.Name,
  48.         }, nil
  49. }
复制代码
2. 用户认证中间件

关于Gin中间件的开发,可以参照gin中间件开发,这里增加三种认证方式:noAuth,不使用认证;basicAuth,用户名密码方式认证;tokenAuth,使用token进行认证:
  1. func noAuth(ctx *gin.Context) {
  2.         ctx.Next()
  3. }
  4. func basicAuth(ctx *gin.Context) {
  5.         name, pwd, ok := ctx.Request.BasicAuth()
  6.         if !ok {
  7.                 srvLogger.Error("basic auth failed")
  8.                 ctx.JSON(http.StatusForbidden, gin.H{"code": http.StatusForbidden, "msg": "basic auth failed"})
  9.                 ctx.Abort()
  10.                 return
  11.         }
  12.         user, err := data.GetUserByName(name)
  13.         if err != nil {
  14.                 srvLogger.Errorf("GetUserByName error: %s", err.Error())
  15.                 ctx.JSON(http.StatusForbidden, gin.H{"code": http.StatusForbidden, "msg": err.Error()})
  16.                 ctx.Abort()
  17.                 return
  18.         }
  19.         if utils.CalcPassword(pwd, user.Salt) != user.Password {
  20.                 srvLogger.Error("user name or password is incorrect")
  21.                 ctx.JSON(http.StatusForbidden, gin.H{"code": http.StatusForbidden, "msg": "user name or password is incorrect"})
  22.                 ctx.Abort()
  23.                 return
  24.         }
  25.         ctx.Next()
  26. }
  27. func tokenAuth(ctx *gin.Context) {
  28.         if err := data.ParseJWT(strings.Split(ctx.Request.Header.Get("Authorization"), " ")[1]); err != nil {
  29.                 srvLogger.Errorf("tokenAuth error: %s", err.Error())
  30.                 ctx.JSON(http.StatusForbidden, gin.H{"code": http.StatusForbidden, "msg": "token auth failed"})
  31.                 ctx.Abort()
  32.                 return
  33.         }
  34.         ctx.Next()
  35. }
复制代码
3. 注册路由

上篇中,注册的路由是这样的:
  1. engine.POST("/login", login)
  2. engine.GET("/hi/:name", sayHi)
  3. engine.POST("/block/upload", upload)
  4. engine.GET("/block/parse/:msgType", parse)
  5. engine.POST("/block/update/:channel", updateConfig)
复制代码
现在需要对/block/upload、/block/parse/:msgType、/block/update/:channel接口增加认证,这就需要用到我们上面实现的三个中间件。
由于中间件会按照它们的注册顺利来执行,所以需要认证中间件需要在相应的处理接口前执行,针对noAuth的情况,上面的代码并不需要进行修改,但对于basicAuthtokenAuth,上面的代码就需要修改了:
  1. engine.POST("/block/upload", basicAuth, upload)
  2. engine.GET("/block/parse/:msgType", basicAuth, parse)
  3. engine.POST("/block/update/:channel", basicAuth, updateConfig)
复制代码
  1. engine.POST("/block/upload", tokenAuth, upload)
  2. engine.GET("/block/parse/:msgType", tokenAuth, parse)
  3. engine.POST("/block/update/:channel", tokenAuth, updateConfig)
复制代码
当然我们也可以使用Handle(httpMethod, relativePath string, handlers ...HandlerFunc)来进行路由注册:
  1. for _, router := range server.Routers() {
  2.         var handlers []gin.HandlerFunc
  3.         if router.AuthType == 0 {
  4.                 router.AuthType = conf.AuthType
  5.         }
  6.         switch router.AuthType {
  7.         case config.Server_BASICAUTH:
  8.                 handlers = append(handlers, basicAuth)
  9.         case config.Server_TOKENAUTH:
  10.                 handlers = append(handlers, tokenAuth)
  11.         default:
  12.                 handlers = append(handlers, noAuth)
  13.         }
  14.         handlers = append(handlers, router.Handler)
  15.         engine.Handle(router.Method, router.Path, handlers...)
  16. }
复制代码
项目完整代码可以从Github上查看。
  
声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 恋水无意

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

耶耶耶耶耶

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表