简易笔记的简易实现(3)鉴权认证与微服务改造 | 青训营笔记

78 阅读5分钟

这是我参与「第五届青训营」伴学笔记创作活动的第7天

前言

这是简易笔记项目的第三篇,本篇主要描述如何实现鉴权认证与微服务改造。

鉴权认证

为了确保用户只能访问自己的笔记和自己的信息,同时避免未认证用户访问。因此,我们需要对用户的访问进行权限控制。这里,我们采用JWT方式进行鉴权。

JWT简介

JSON Web Token(JWT)是一个轻量级的认证规范,这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息。其本质是一个 token ,是一种紧凑的 URL 安全方法,用于在网络通信的双方之间传递。

在本项目中,我们使用hertz官方提供的jwt插件进行验证。

安装

go get github.com/hertz-contrib/jwt

参数说明

官方配置文档中,有很多选项可以选择。我们这里只选择以下几项。

参数介绍
Key用于设置签名密钥(必要配置)
Authenticator用于设置登录时认证用户信息的函数(必要配置)
PayloadFunc用于设置登陆成功后为向 token 中添加自定义负载信息的函数
Unauthorized用于设置 jwt 验证流程失败的响应函数
IdentityHandler用于设置获取身份信息的函数,默认与 IdentityKey 配合使用
IdentityKey用于设置检索身份的键,默认为 identity

Key

这里签名密钥设置为easy-note(生产环境可不敢这么做呦)

IdentityKey

这个键设置为username

Authenticator

这里,我们参照官方文档,将写好的登陆函数稍加改造,粘贴到此处。同时,删除原有登陆函数,将路由修改到此函数上。

Authenticator: func(ctx context.Context, c *app.RequestContext) (interface{}, error) {
	var loginValue loginForm
	if err := c.BindAndValidate(&loginValue); err != nil {
		return "", jwt.ErrMissingLoginValues
	}
	username := loginValue.Username
	password := loginValue.Password
	var userRepo repo.UserRepo
	result := userRepo.CheckUser(username, password)
	if result {
		return &models.User{Username: username}, nil
	}
        return nil, jwt.ErrFailedAuthentication
}

PayloadFunc

用于获取jwt载荷的函数,在本项目中照抄文档就行。

PayloadFunc: func(data interface{}) jwt.MapClaims {
	if v, ok := data.(*models.User); ok {
		return jwt.MapClaims{
			identityKey: v.Username,
		}
	}
	return jwt.MapClaims{}
}

IdentityHandler

用于获取身份信息,这里我们获取用户的id,将其写入上下文,用于后续使用。

IdentityHandler: func(ctx context.Context, c *app.RequestContext) interface{} {
   claims := jwt.ExtractClaims(ctx, c)
   var userRepo repo.UserRepo
   user := userRepo.GetUserByUsername(claims[identityKey].(string))
   if user == nil {
      resp.BadRequestResponse(c, 6, "用户不存在")
      c.Abort()
      return nil
   }
   c.Set("userId", user.Id)
   return &user
},

Unauthorized

身份验证失败的函数。

Unauthorized: func(ctx context.Context, c *app.RequestContext, code int, message string) {
   resp.BaseResponse(c, code, code, message, nil)
},

路由改造

在完成中间件的设置后,我们需要将其绑定到路由上。因此,需要对路由进行改造。

登陆路由

登陆路由需要将登陆的函数绑定到中间件上。

var jwt = middleware.JWT()
h.POST("/login", jwt.LoginHandler)

用户路由组、笔记路由组

这两个路由组需要鉴权后访问

userRouter.Use(jwt.MiddlewareFunc())
......
noteRouter.Use(jwt.MiddlewareFunc())

访问测试

登录

使用apifox访问登录的接口,输入数据库中存在的用户名和密码,得到以下结果。可以看到已经赋予了token。

image.png

{
    "code": 200,
    "expire": "2023-02-03T17:51:33+08:00",
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NzU0MTc4OTMsIm9yaWdfaWF0IjoxNjc1NDE0MjkzLCJ1c2VybmFtZSI6Imxob3UifQ.sSEEltrD1zpMmEDRQzzAE7BM3_cNGLPxWvlyZGTQ2qM"
}

获取用户信息

在不设定token的情况下,访问获取用户信息的接口,可以看到提示没有鉴权的数据。

image.png

将上面的token添加后,成功获取到了用户的数据。

image.png

其余的测试项目结果类似。

微服务改造

本项目的最终目标是实现一个微服务项目,而目前写的项目是一个单体项目。因此,需要对其进行改造。

安装kitex

kitex的安装参考这篇文章

编写thrift文件

thrift文件约定了双方的通信规则,我们需要结合现有的函数编写对应的user.thrift,note.thrift两个文件.

// user.thrift
namespace go user

service User {
    ResCheckUser checkUser(1:ReqCheckUser req)
    ResInsertUser insertUser(1: ReqInsertUser req)
    ResGetUserById getUserById(1: ReqGetUserById req)
}

struct ReqCheckUser {
    1: string username
    2: string password
}

struct ResCheckUser {
    1: bool isExist
}

struct ReqInsertUser {
    1: string username
    2: string password
    3: string avatar
    4: i32 gender
}

struct ResInsertUser {
    1: bool isExist
}

struct ReqGetUserById {
    1: i32 userId
}

struct ResGetUserById {
    1: string username
    2: string createdAt
    3: string avatar
    4: i32 gender
    5: string updatedAt
}

使用命令编译成对应的文件

 kitex -module easy-note -service user user.thrift
 kitex -module easy-note -service note note.thrift

在编译完成后会得到一个基本文件结构。我们需要修改这个文件。

编写处理函数

修改数据操作

微服务后,每个服务都有自己的模型和数据处理逻辑,因此需要修改原有的数据函数。在原先的实现中,我们封装了一层抽象层,简化了开发。但在这里,我们需要自己实现。这里提供note的代码,user的代码与之相似。

// note/repo/repo.go 笔记的数据操作
package repo

import (
   "easy-note/models"
   "fmt"
   "gorm.io/gorm"
)

var DB *gorm.DB

type NoteRepo struct{}

func (noteRepo *NoteRepo) InsertNote(note *models.NoteInfo) (bool, error) {
   db := DB.Create(note)
   err := db.Error
   if err != nil {
      return false, err
   }
   return db.RowsAffected > 0, nil
}
......

逻辑处理

得益于kitex的自动生成,我们只需要实现对应的函数即可。这部分代码只需将原处理函数中的查找数据的部分粘贴过来即可。这里提供note的代码,user的代码与之相似。

// note/handler/handler.go
type NoteImpl struct {
   Repo repo.NoteRepo
}

// InsertNote implements the NoteImpl interface.
func (s *NoteImpl) InsertNote(ctx context.Context, req *note.ReqInsertNote) (resp *note.ResInsertNote, err error) {
   isSuccess, err := s.Repo.InsertNote(&models.NoteInfo{
      Name:   req.GetName(),
      Note:   req.GetNote(),
      UserId: int(req.GetUserId()),
   })
   if err != nil {
      return &note.ResInsertNote{IsSuccess: false}, err
   }
   return &note.ResInsertNote{IsSuccess: isSuccess}, nil
}
......

修改启动函数

由于我们需要三个微服务,为了避免端口冲突,因此需要指定端口。

addr, err := net.ResolveTCPAddr("tcp", noteConf.Addr) // 新建一个连接
if err != nil {
   log.Fatalln(err)
}
svr = note.NewServer(new(handler.NoteImpl),
   server.WithServiceAddr(addr), // 指定服务端口
   server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{
      ServiceName: "easy-note.note",
   }))

修改网关

这里将原有的处理逻辑从数据处理层修改为调用微服务的客户端。

result, err := UserClient.InsertUser(ctx, &user.ReqInsertUser{
   Username: registerForm.Username,
   Password: registerForm.Password,
   Avatar:   registerForm.Avatar,
   Gender:   registerForm.Gender,
})

同时,在服务初始化的部分添加微服务的连接。

newUserClient, err := user.NewClient("easy-note.user", client.WithResolver(r))
if err != nil {
   log.Fatalln(err)
}
handler.UserClient = newUserClient

测试

在修改完成后,我们进行拉通测试。

image.png

如图,成功登陆成功,表明我们这个接口改造成功。

总结

本篇文章是该系列的最后一篇文章。easy-note虽然是个小项目,但在大型项目开发中涉及到的普遍问题这里面都会涉及。经过这个项目的实践,我也对hertz、gorm、kitex三个框架有了大概的认识,对于成功完成青训营的大项目有了更多的信心。此项目的代码已开源,lhou/easy-note,欢迎访问和提issue。如果能给个star就更好了。