这是我参与「第五届青训营」伴学笔记创作活动的第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。
{
"code": 200,
"expire": "2023-02-03T17:51:33+08:00",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NzU0MTc4OTMsIm9yaWdfaWF0IjoxNjc1NDE0MjkzLCJ1c2VybmFtZSI6Imxob3UifQ.sSEEltrD1zpMmEDRQzzAE7BM3_cNGLPxWvlyZGTQ2qM"
}
获取用户信息
在不设定token的情况下,访问获取用户信息的接口,可以看到提示没有鉴权的数据。
将上面的token添加后,成功获取到了用户的数据。
其余的测试项目结果类似。
微服务改造
本项目的最终目标是实现一个微服务项目,而目前写的项目是一个单体项目。因此,需要对其进行改造。
安装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 ¬e.ResInsertNote{IsSuccess: false}, err
}
return ¬e.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
测试
在修改完成后,我们进行拉通测试。
如图,成功登陆成功,表明我们这个接口改造成功。
总结
本篇文章是该系列的最后一篇文章。easy-note虽然是个小项目,但在大型项目开发中涉及到的普遍问题这里面都会涉及。经过这个项目的实践,我也对hertz、gorm、kitex三个框架有了大概的认识,对于成功完成青训营的大项目有了更多的信心。此项目的代码已开源,lhou/easy-note,欢迎访问和提issue。如果能给个star就更好了。