1 Star 0 Fork 0

姜春 / vlog

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
v2.md 33.23 KB
一键复制 编辑 原始数据 按行查看 历史
姜春 提交于 2024-02-20 14:19 . 增加makefile脚手架

vblog项目重构 v2

v1的问题

v1:

  • UserServiceImp
  • TokenServiceImpl
  • TokenApiHandler
package main

import (
	"github.com/gin-gonic/gin"
	"gitlab.com/go-course-project/go13/vblog/apps/token/api"
	token_impl "gitlab.com/go-course-project/go13/vblog/apps/token/impl"
	user_impl "gitlab.com/go-course-project/go13/vblog/apps/user/impl"
)


func main() {
	// user service impl
	usvc := user_impl.NewUserServiceImpl()

	// token service impl
	tsvc := token_impl.NewTokenServiceImpl(usvc)

	// api
	TokenApiHander := api.NewTokenApiHandler(tsvc)

	// Protocol
	engine := gin.Default()

	rr := engine.Group("/vblog/api/v1")
	TokenApiHander.Registry(rr)

	// 把Http协议服务器启动起来
	if err := engine.Run(":8080"); err != nil {
		panic(err)
	}
}

当模块众多的时候, main里面 手动组装对象的难度很越来越大

ioc: 依赖反转

控制反转(Inversion of Control,缩写为IoC)

1. 注册对象(采用 init()导入的方式来执行注册)

_ "gitlab.com/go-course-project/go13/vblog/apps/token/api"
_ "gitlab.com/go-course-project/go13/vblog/apps/token/impl"
_ "gitlab.com/go-course-project/go13/vblog/apps/user/impl"

ioc.Registry("user_service_impl", &UserServiceImpl{})
ioc.Registry("token_service_impl", &TokenServiceImpl{})

2. 没有依赖关系的管理, 每个对象自己去ioc获取自己依赖
// 怎么实现token.Service接口?
// 定义TokenServiceImpl来实现接口
type TokenServiceImpl struct {
	// 依赖了一个数据库操作的链接池对象
	db *gorm.DB

	// 依赖user.Service, 没有 UserServiceImpl 具体实现
	// 依赖接口,不要接口的具体实现
	user user.Service
}

// 依赖的关系解决 分层2个阶段, 一个注册, 一个初始化(组件完善自己的依赖关系)
func (i *TokenServiceImpl) Init() {
    // 先通过ioc获取对象, 然后再把对象断言成 你需要接口
    // 	tsvc := token_impl.NewTokenServiceImpl(usvc) 都不要
    i.user = ioc.Get("user_service_impl").(user.Service)
}

3. ioc 来完成的对象的初始化, 让每个注册的对象去完成依赖的自主寻找
ioc.InitAllObject()
  1. 每写一个对象 就注册一个对象, 参考mcenter的具体做法
package apps

import (
	// 注册所有内部服务模块, 无须对外暴露的服务, 用于内部依赖
	_ "github.com/infraboard/mcenter/apps/counter/impl"
	_ "github.com/infraboard/mcenter/apps/ip2region/impl"

	// 引入第三方存储模块(Mongo)
	_ "github.com/infraboard/mcube/v2/ioc/apps/oss/mongo"

	// 注册所有GRPC服务模块, 暴露给框架GRPC服务器加载, 注意 导入有先后顺序
	_ "github.com/infraboard/mcenter/apps/domain/impl"
	_ "github.com/infraboard/mcenter/apps/endpoint/impl"
	_ "github.com/infraboard/mcenter/apps/instance/impl"
	_ "github.com/infraboard/mcenter/apps/label/impl"
	_ "github.com/infraboard/mcenter/apps/namespace/impl"
	_ "github.com/infraboard/mcenter/apps/notify/impl"
	_ "github.com/infraboard/mcenter/apps/policy/impl"
	_ "github.com/infraboard/mcenter/apps/resource/impl"
	_ "github.com/infraboard/mcenter/apps/role/impl"
	_ "github.com/infraboard/mcenter/apps/service/impl"
	_ "github.com/infraboard/mcenter/apps/token/impl"
	_ "github.com/infraboard/mcenter/apps/user/impl"
)

ioc 具体实现

使用map[string]Object 来实现一个简易版本的ioc Ioc 实现

ioc改造

  • 完成对象的注册和对象的依赖获取(Init)
  • 使用ioc管理对象

改造控制器

改造之前: token(controller) ---> user(controller)

	// user service impl
	usvc := user_impl.NewUserServiceImpl()

	// token service impl
	tsvc := token_impl.NewTokenServiceImpl(usvc)

改造:

  1. UserServiceImpl 对象注册
// 通过Import 自动完成注册
// 为什么不能直接在这里把db对象给初始化了?
// 这个逻辑是在import时候执行的, 程序在import 后才,执行的配置模块加载, 此时的conf.C()为nil
// 这个db属性的初始化一定要在配置加载后执行: conf.Load(), ioc.Init()
func init() {
	ioc.Controller().Registry(user.AppName, &UserServiceImpl{})
}
  1. TokenServiceImpl 对像注册: ioc.Init() 来执行依赖获取
func init() {
	ioc.Controller().Registry(token.AppName, &TokenServiceImpl{})
}

// 对象属性初始化
func (i *TokenServiceImpl) Init() error {
	i.db = conf.C().DB()
	i.user = ioc.Controller().Get(user.AppName).(user.Service)
	return nil
}

改造Api

改造之前: token(api) ---> token(controller)

	// token service impl
	tsvc := token_impl.NewTokenServiceImpl(usvc)

	// api
	TokenApiHander := api.NewTokenApiHandler(tsvc)

改造之后: TokenApiHander的注册, ioc.Init() 来执行依赖获取

func init() {
	ioc.Api().Registry(token.AppName, &TokenApiHandler{})
}

func (h *TokenApiHandler) Init() error {
	// 从Controller空间中 获取 模块的具体实现
	// 断言该实现是满足该接口的
	h.svc = ioc.Controller().Get(token.AppName).(token.Service)
	return nil
}

基于ioc的启动

之前的启动方法:

func main() {
	// user service impl
	usvc := user_impl.NewUserServiceImpl()

	// token service impl
	tsvc := token_impl.NewTokenServiceImpl(usvc)

	// api
	TokenApiHander := api.NewTokenApiHandler(tsvc)

	// Protocol
	engine := gin.Default()

	rr := engine.Group("/vblog/api/v1")
	TokenApiHander.Registry(rr)

	// 把Http协议服务器启动起来
	if err := engine.Run(":8080"); err != nil {
		panic(err)
	}
}

之后的代码:

import (
	"github.com/gin-gonic/gin"
	"gitlab.com/go-course-project/go13/vblog/apps/token"
	"gitlab.com/go-course-project/go13/vblog/apps/token/api"
	"gitlab.com/go-course-project/go13/vblog/conf"
	"gitlab.com/go-course-project/go13/vblog/ioc"

	// 2.1 先注册对象
	_ "gitlab.com/go-course-project/go13/vblog/apps/token/impl"
	_ "gitlab.com/go-course-project/go13/vblog/apps/user/impl"
	_ "gitlab.com/go-course-project/go13/vblog/apps/token/api"
)

func main() {
	// 1. 初始化程序配置, 这里没有配置,使用默认值
	if err := conf.LoadFromEnv(); err != nil {
		panic(err)
	}

	// 2.2 程序对象管理
	if err := ioc.Init(); err != nil {
		panic(err)
	}

	// Protocol
	engine := gin.Default()

	rr := engine.Group("/vblog/api/v1")

	// ioc 能不能帮忙把 模块API的注册也一起管理
	// ioc.Init() , 对象初始化完成后, 如果对象是 api对象,就帮忙完成下注册
	// ioc.RegistryGin(rr)?
	ioc.Api().Get(token.AppName).(*api.TokenApiHandler).Registry(rr)

	// 把Http协议服务器启动起来
	if err := engine.Run(":8080"); err != nil {
		panic(err)
	}
}

ioc 通用功能管理

type GinApi interface {
	// 基础约束
	Object
	// 额外约束
	// ioc.Api().Get(token.AppName).(*api.TokenApiHandler).Registry(rr)
	Registry(rr gin.IRouter)
}

// 遍历所有的对象, 帮忙完成Api的注册
// 由ioc调用对象提供的 Registry方法,来吧模块的api 注册给gin root router
func (c *NamespaceContainer) RegistryGinApi(rr gin.IRouter) {
	// 遍历Namespace
	for key := range c.ns {
		c := c.ns[key]
		// 遍历Namespace里面的对象
		for objectName := range c.storage {
			obj := c.storage[objectName]
			// 如果判断一个对象是不是GinApi对象(约束)
			// 判断对象有没有Registry(rr gin.IRouter)
			// 断言该对象 满足GinApi接口, 实现了Registry函数
			if v, ok := obj.(GinApi); ok {
				v.Registry(rr)
			}
		}
	}
}

改造后,我们只需要完成业务对象注册, 其他操作 ioc帮忙进行管理

func main() {
	// 1. 初始化程序配置, 这里没有配置,使用默认值
	if err := conf.LoadFromEnv(); err != nil {
		panic(err)
	}

	// 2. 程序对象管理
	if err := ioc.Init(); err != nil {
		panic(err)
	}

	// Protocol
	engine := gin.Default()

	rr := engine.Group("/vblog/api/v1")
	ioc.RegistryGinApi(rr)

	// 把Http协议服务器启动起来
	if err := engine.Run(":8080"); err != nil {
		panic(err)
	}
}
    1. 注册对象
    1. ioc管理

通过 导入"gitlab.com/go-course-project/go13/vblog/apps" 来导入所有的业务实现

package apps

// 注册业务实现: API + Controller
import (
	// 通过import方法 完成注册
	_ "gitee.com/baicaijc/vblog/apps/token/api"
	_ "gitee.com/baicaijc/vblog/apps/token/impl"
	_ "gitee.com/baicaijc/vblog/apps/user/impl"
)

入口就不需要修改:

package main

import (
	"gitee.com/baicaijc/vblog/conf"
	"gitee.com/baicaijc/vblog/ioc"
	"github.com/gin-gonic/gin"

	// 2.1 通过import方法 完成注册
	_ "gitee.com/baicaijc/vblog/apps"
)

func main() {
	// 1. 初始化程序配置, 这里没有配置,使用默认值
	if err := conf.LoadFromFile("conf/etc/application.toml"); err != nil {
		panic(err)
	}

	// 2.2 程序对象管理
	if err := ioc.Init(); err != nil {
		panic(err)
	}

	// Protocol
	engine := gin.Default()

	rr := engine.Group("/vblog/api/v1")
	// ioc.Api().Get(token.AppName).(*api.TokenApiHandler).Registry(rr)
	ioc.RegistryGinApi(rr)

	// 把Http协议服务器启动起来
	if err := engine.Run(":8080"); err != nil {
		panic(err)
	}
}

有了ioc后,我们业务开发流程, 编写:

  • api 对象
  • controller 对象

基于ioc blog模块

定义Blog业务(Define)

// Blog Service接口定义, CRUD
type Service interface {
	// 创建一个博客
	CreateBlog(context.Context, *CreateBlogRequest) (*Blog, error)
	// 获取博客列表
	QueryBlog(context.Context, *QueryBlogRequest) (*BlogSet, error)
	// 获取博客详情
	DescribeBlog(context.Context, *DescribeBlogRequest) (*Blog, error)
	// 更新博客
	UpdateBlog(context.Context, *UpdateBlogRequest) (*Blog, error)
	// 删除博客
	DeleteBlog(context.Context, *DeleteBlogRequest) (*Blog, error)
	// 文章状态修改, 比如发布
	ChangedBlogStatus(context.Context, *ChangedBlogStatusRequest) (*Blog, error)
	// 文章审核
	AuditBlog(context.Context, *AuditInfo) (*Blog, error)
}

实现业务(Controller)

  1. 定义对象:
func init() {
	ioc.Controller().Registry(blog.AppName, &blogServiceImpl{})
}

// blog.Service接口, 是直接注册给Ioc, 不需要对我暴露
type blogServiceImpl struct {
	// 依赖了一个数据库操作的链接池对象
	db *gorm.DB
}

func (i *blogServiceImpl) Init() error {
	i.db = conf.C().DB()
	return nil
}

func (i *blogServiceImpl) Destory() error {
	return nil
}
  1. 托管对象
// 注册业务实现: API + Controller
import (
	_ "gitee.com/baicaijc/vblog/apps/blog/impl"
)
  1. 对象的业务接口实现

package impl

import (
	"context"

	"gitee.com/baicaijc/vblog/apps/blog"
)

// 创建一个博客
func (i *blogServiceImpl) CreateBlog(ctx context.Context, req *blog.CreateBlogRequest) (*blog.Blog, error) {

	return nil, nil
}

// 获取博客列表
func (i *blogServiceImpl) QueryBlog(ctx context.Context, in *blog.QueryBlogRequest) (*blog.BlogSet, error) {

	return nil, nil
}

// 获取博客详情
func (i *blogServiceImpl) DescribeBlog(ctx context.Context, in *blog.DescribeBlogRequest) (*blog.Blog, error) {
	return nil, nil
}

// 删除博客
func (i *blogServiceImpl) DeleteBlog(ctx context.Context, req *blog.DeleteBlogRequest) (*blog.Blog, error) {

	return nil, nil
}

// 更新博客
// 1. 全新更新: 对象的替换
// 2. 部分更新: (old obj) --patch--> new obj ---> save
func (i *blogServiceImpl) UpdateBlog(ctx context.Context, req *blog.UpdateBlogRequest) (*blog.Blog, error) {
	return nil, nil
}

// 文章状态修改, 比如发布
func (i *blogServiceImpl) ChangedBlogStatus(ctx context.Context, req *blog.ChangedBlogStatusRequest) (*blog.Blog, error) {
	return nil, nil
}

// 文章审核
func (i *blogServiceImpl) AuditBlog(ctx context.Context, req *blog.AuditInfo) (*blog.Blog, error) {
	return nil, nil
}

4.1 编写单元测试: 准备被测试的对象

package impl_test

import (
	"context"

	"gitee.com/baicaijc/vblog/apps/blog"
	"gitee.com/baicaijc/vblog/ioc"

	// 1. 加载对象
	_ "gitee.com/baicaijc/vblog/apps"
)

// blog service 的实现的具体对象是在ioc中,
// 需要在ioc中获取具体的svc 用来测试

var (
	impl blog.Service
	ctx  = context.Background()
)

func init() {
	// 2. ioc获取对象
	impl = ioc.Controller().Get(blog.AppName).(blog.Service)

	// ioc需要初始化 才能填充 db属性
	if err := ioc.Init(); err != nil {
		panic(err)
	}
}

4.2 编写单元测试: 测试接口实现

package impl_test

import (
	"testing"

	"gitee.com/baicaijc/vblog/apps/blog"
)

func TestCreateBlog(t *testing.T) {
	req := blog.NewCreateBlogRequest()
	req.Title = "go语言全栈开发"
	req.Author = "oldyu"
	req.Content = "xxx"
	req.Summary = "xx"
	req.Tags["目录"] = "Go语言"
	ins, err := impl.CreateBlog(ctx, nil)
	if err != nil {
		t.Fatal(err)
	}
	t.Log(ins)
}


// 创建一个博客
func (i *blogServiceImpl) CreateBlog(ctx context.Context, req *blog.CreateBlogRequest) (*blog.Blog, error) {
	// 1. 校验请求
	if err := req.Validate(); err != nil {
		return nil, exception.ErrBadRequest.WithMessagef("创建博客失败, %s", err)
	}

	// 2. 构造对象
	ins := blog.NewBlog(req)

	// 3. 对象入库
	// INSERT INTO `blogs` (`created_at`,`updated_at`,`title`,`author`,`content`,`summary`,`create_by`,`tags`,`published_at`,`status`,`audit_at`,`is_audit_pass`) VALUES (1706340774,1706340774,'go语言全栈开发','oldyu','xxx','xx','','{"目录":"Go语言"}',0,'0',0,false)
	err := i.db.WithContext(ctx).Create(ins).Error
	if err != nil {
		return nil, err
	}

	// 4. 返回对象
	return ins, nil
}

关于Mergo

// 更新博客
// 1. 全新更新: 对象的替换
// 2. 部分更新: (old obj) --patch--> new obj ---> save
func (i *blogServiceImpl) UpdateBlog(ctx context.Context, req *blog.UpdateBlogRequest) (*blog.Blog, error) {
	// 查询老的对象, 需要被更新的博客对象
	ins, err := i.DescribeBlog(ctx, blog.NewDescribeBlogRequest(req.Id))
	if err != nil {
		return nil, err
	}

	// 对象更新
	switch req.UpdateMode {
	case common.UPDATE_MODE_PATCH:
		// if req.Author != "" {
		// 	ins.Author = req.Author
		// }
		// if req.Title != "" {
		// 	ins.Title = req.Title
		// }
		//... 有没有其他的办法 帮我们完成2个结构图的合并 merge(patch)
		// https://github.com/darccio/mergo
		// // WithOverride will make merge override non-empty dst attributes with non-empty src attributes values.
		//
		if err := mergo.MapWithOverwrite(ins.CreateBlogRequest, req.CreateBlogRequest); err != nil {
			return nil, err
		}
	default:
		ins.CreateBlogRequest = req.CreateBlogRequest
	}

	// 再次校验对象, 校验更新后的数据是否合法
	if err := ins.Validate(); err != nil {
		return nil, exception.ErrBadRequest.WithMessagef("校验更新请求失败: %s", err)
	}

	// 更新数据库
	// UPDATE `blogs` SET `id`=48,`created_at`=1706344163,`updated_at`=1706344423,`title`='go语言全栈开发V2',`author`='oldyu',`content`='xxx',`summary`='xx',`tags`='{"目录":"Go语言"}' WHERE id = 48
	err = i.db.WithContext(ctx).Model(&blog.Blog{}).Where("id = ?", ins.Id).Updates(ins).Error
	if err != nil {
		return nil, err
	}

	return ins, nil
}

暴露接口(API)

  • 创建博客: POST /vblogs/api/v1/blogs
  • 修改博客(部分): PATCH /vblogs/api/v1/blogs/:id
  • 修改博客(全量): PUT /vblogs/api/v1/blogs/:id
  • 删除博客: DELETE /vblogs/api/v1/blogs/:id
  • 查询列表: GET /vblogs/api/v1/blogs
  • 查询详情: GET /vblogs/api/v1/blogs/:id
  1. 定义对象
func init() {
	ioc.Api().Registry(blog.AppName, &blogApiHandler{})
}

// blog.Service接口, 是直接注册给Ioc, 不需要对我暴露
type blogApiHandler struct {
	svc blog.Service
}

func (i *blogApiHandler) Init() error {
	i.svc = ioc.Controller().Get(blog.AppName).(blog.Service)
	return nil
}

func (i *blogApiHandler) Destory() error {
	return nil
}
  1. 托管对象(apps模块下)
// 注册业务实现: API + Controller
import (
	_ "gitlab.com/go-course-project/go13/vblog/apps/blog/api"
)
  1. 具体实现

// + 创建博客: POST /vblogs/api/v1/blogs
func (h *blogApiHandler) CreateBlog(c *gin.Context) {
	// h.tk.Validate()
	req := blog.NewCreateBlogRequest()

	// // 后面请求如何获取 中间信息
	// if v, ok := c.Get(token.TOKEN_MIDDLEWARE_KEY); ok {
	// 	req.CreateBy = v.(*token.Token).UserName
	// }

	if err := c.BindJSON(req); err != nil {
		response.Failed(c, err)
		return
	}
	ins, err := h.svc.CreateBlog(c.Request.Context(), req)
	if err != nil {
		response.Failed(c, err)
		return
	}
	response.Success(c, ins)
}

// + 修改博客(部分): PATCH /vblogs/api/v1/blogs/:id
// /vblogs/api/v1/blogs/10 --> id = 10
// /vblogs/api/v1/blogs/20 --> id = 20
// c.Param("id") 获取路径变量的值
func (h *blogApiHandler) PatchBlog(c *gin.Context) {
	// h.tk.Validate()

	// 如果解析路径里面的参数
	req := blog.NewUpdateBlogRequest(c.Param("id"))
	req.UpdateMode = common.UPDATE_MODE_PATCH
	// 用户传递的数据
	if err := c.BindJSON(req.CreateBlogRequest); err != nil {
		response.Failed(c, err)
		return
	}
	ins, err := h.svc.UpdateBlog(c.Request.Context(), req)
	if err != nil {
		response.Failed(c, err)
		return
	}
	response.Success(c, ins)
}

// + 修改博客(全量): PUT /vblogs/api/v1/blogs/:id
func (h *blogApiHandler) UpdateBlog(c *gin.Context) {
	// 如何解析路径里面的参数
	req := blog.NewUpdateBlogRequest(c.Param("id"))
	req.UpdateMode = common.UPDATE_MODE_PUT
	// 用户传递的数据
	if err := c.BindJSON(req.CreateBlogRequest); err != nil {
		response.Failed(c, err)
		return
	}
	ins, err := h.svc.UpdateBlog(c.Request.Context(), req)
	if err != nil {
		response.Failed(c, err)
		return
	}
	response.Success(c, ins)
}

// + 删除博客: DELETE /vblogs/api/v1/blogs/:id
func (h *blogApiHandler) DeleteBlog(c *gin.Context) {
	req := blog.NewDeleteBlogRequest(c.Param("id"))
	ins, err := h.svc.DeleteBlog(c.Request.Context(), req)
	if err != nil {
		response.Failed(c, err)
		return
	}
	response.Success(c, ins)
}

// + 查询列表: GET /vblogs/api/v1/blogs?page_size=10&page_number=2
func (h *blogApiHandler) QueryBlog(c *gin.Context) {
	req := blog.NewQueryBlogRequestFromGin(c)
	set, err := h.svc.QueryBlog(c.Request.Context(), req)
	if err != nil {
		response.Failed(c, err)
		return
	}
	response.Success(c, set)
}

// + 查询详情: GET /vblogs/api/v1/blogs/:id
func (h *blogApiHandler) DescribeBlog(c *gin.Context) {
	req := blog.NewDescribeBlogRequest(c.Param("id"))
	ins, err := h.svc.DescribeBlog(c.Request.Context(), req)
	if err != nil {
		response.Failed(c, err)
		return
	}
	response.Success(c, ins)
}
  1. 把接口暴露出去(接口注册)
// 让ioc帮我们完成接口的路由注册 ioc.GinApi
//
//	type GinApi interface {
//		// 基础约束
//		Object
//		// 额外约束
//		// ioc.Api().Get(token.AppName).(*api.TokenApiHandler).Registry(rr)
//		Registry(rr gin.IRouter)
//	}
func (i *blogApiHandler) Registry(rr gin.IRouter) {
	r := rr.Group(blog.AppName)
	r.POST("/", i.CreateBlog)
	r.GET("/", i.QueryBlog)
	r.GET("/:id", i.DescribeBlog)
	r.PATCH("/:id", i.PatchBlog)
	r.PUT("/:id", i.UpdateBlog)
	r.DELETE("/:id", i.DeleteBlog)
}

权限中间件

现在写的blog api 是没有权限认证的

可以单独在每个请求内部去校验逻辑:

// + 创建博客: POST /vblogs/api/v1/blogs
func (h *blogApiHandler) CreateBlog(c *gin.Context) {
	// h.tk.Validate()
	...
}

使用中间件来补充中间件:

Gin 中间件

中间件是一个函数, 这个函数参数的定义是 框架定义

use(middleware)

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
  1. 实现一个认证中间件
import (
	"github.com/gin-gonic/gin"
	"gitlab.com/go-course-project/go13/vblog/apps/token"
	"gitlab.com/go-course-project/go13/vblog/ioc"
	"gitlab.com/go-course-project/go13/vblog/response"
)

func Auth(c *gin.Context) {
	// 获取tk 模块
	svc := ioc.Controller().Get(token.AppName).(token.Service)

	ak := token.GetAccessTokenFromHttp(c.Request)
	req := token.NewValidateTokenRequest(ak)
	tk, err := svc.ValidateToken(c.Request.Context(), req)
	if err != nil {
		response.Failed(c, err)
		return
	}
	// 请求通过, 用户的身份信息 携带在 请求的上下文当中,传递给后续请求
	// 对于Gin, 如果把请求的中间数据传递下,使用了一个map, 这个map在request对象上
	c.Keys[token.TOKEN_MIDDLEWARE_KEY] = tk

	// 后面请求如何获取 中间信息
	// c.Keys[token.TOKEN_MIDDLEWARE_KEY].(*token.Token).UserName
}
  1. 如何使用认证中间件
func (i *blogApiHandler) Registry(rr gin.IRouter) {
	r := rr.Group(blog.AppName)

	// 普通接口, 允许访客使用, 不需要权限
	r.GET("/", i.QueryBlog)
	r.GET("/:id", i.DescribeBlog)

	// 整个模块后面的请求 都需求认证
	r.Use(middleware.Auth)

	// 后面管理的接口 需要权限
	r.POST("/", i.CreateBlog)
	r.PATCH("/:id", i.PatchBlog)
	r.PUT("/:id", i.UpdateBlog)
	r.DELETE("/:id", i.DeleteBlog)
}
  1. 验证无认证接口
curl --location 'localhost:8080/vblog/api/v1/blogs'

curl --location 'localhost:8080/vblog/api/v1/blogs/48'
  1. 验证有认证的接口
curl --location '127.0.0.1:8080/vblog/api/v1/blogs' \
--header 'Content-Type: application/json' \
--header 'Cookie: token=cmqd7apus0na8uhinjl0' \
--data '{
    "title": "Go全线开发",
    "author": "老喻",
    "content": "Go可以"
}'

简化版本RBAC(增强)

用户关联角色,角色关联具体功能

// required(vistor,admin)
// 方式一
// r.use(role_name, ...)
// r.GET('/', h.GetBlog)
// 方式二
// r.GET('/', h.Required(visitor), h.GetBLog)
  1. 可以选择不做鉴权: 不添加鉴权逻辑
  2. 需要鉴权: Required('admin') ---> 中间件出来 ---> r.GET('/', h.Required(visitor), h.GetBLog)

定义鉴权失败异常

	// 鉴权失败, 认证通过,但是没有权限操作 该接口
	ErrPermissionDeny = NewAPIException(http.StatusForbidden, http.StatusText(http.StatusForbidden)).WithHttpCode(http.StatusForbidden)

定义鉴权中间件

package middleware

import (
	"fmt"

	"gitee.com/baicaijc/vblog/apps/token"
	"gitee.com/baicaijc/vblog/apps/user"
	"gitee.com/baicaijc/vblog/exception"
	"gitee.com/baicaijc/vblog/response"
	"github.com/gin-gonic/gin"
)

// Required(user.ROLE_ADMIN) ---> gin.HandlerFunc
func Required(roles ...user.Role) gin.HandlerFunc {
	return func(c *gin.Context) {
		// 检验用户的角色
		// 直接通过上下文取出Token, 通过Token获取用户角色,来定义访问这个接口的角色
		// 后面请求如何获取 中间信息
		if v, ok := c.Get(token.TOKEN_MIDDLEWARE_KEY); ok {
			// 遍历判断 用户是否在运行的角色列表里面
			hasPerm := false
			for _, r := range roles {
				fmt.Println(roles, v.(*token.Token).Role)
				if r == v.(*token.Token).Role {
					hasPerm = true
				}
			}
			if !hasPerm {
				response.Failed(c, exception.ErrPermissionDeny.WithMessagef("允许访问的角色: %v", roles))
				return
			}
		} else {
			response.Failed(c, exception.ErrUnauthorized)
			return
		}
	}
}

校验Token时补充用户角色

	// 3. 补充用户角色
	u, err := i.user.DescribeUser(ctx, user.NewDescribeUserRequest(tk.UserId))
	if err != nil {
		return nil, err
	}
	tk.Role = u.Role

映射枚举字段

// 定义 ChipType 类型的方法 String(), 返回字符串。
func (r Role) String() string {
	switch r {
	case ROLE_VISITOR:
		return "member"
	case ROLE_ADMIN:
		return "admin"
	default:
		return "NA"
	}
}

使用中间件

	// 只允许管理员才能删除
	r.DELETE("/:id", middleware.Required(user.ROLE_ADMIN), i.DeleteBlog)

使用访客账号来测试 文章的删除

curl --location --request DELETE '127.0.0.1:8080/vblog/api/v1/blogs/1' \
--header 'Cookie: token=cn8c3s5u48h2g3432300'
{
    "code": 403,
    "reason": "Forbidden",
    "message": "允许访问的角色: [admin]"
}

scope 数据的访问控制

权限是控制 用户对某个功能 是否可以使用, 比如 文件编辑接口, 不能让说有的人 对所有的文章都能编辑, 只希望每个作者只能编辑自己的文章

就需要对接口的访问范围做控制

通过查询参数 来数据的访问范围(软控制)

只访问 属于自己的文章, username=?

参数获取create_by

func NewQueryBlogRequestFromGin(c *gin.Context) *QueryBlogRequest {
	req := NewQueryBlogRequest()
	req.CreateBy = c.Query("create_by")
	ps := c.Query("page_size")
	if ps != "" {
		req.PageSize, _ = strconv.Atoi(ps)
	}
	pn := c.Query("page_number")
	if pn != "" {
		req.PageNumber, _ = strconv.Atoi(pn)
	}
	return req
}

type QueryBlogRequest struct {
	// 分页大小, 一个多少个
	PageSize int
	// 当前页, 查询哪一页的数据
	PageNumber int
	// 谁创建的文章
	CreateBy string
}

实现时,带上过滤条件

// 获取博客列表
// 获取博客列表
func (i *blogServiceImpl) QueryBlog(ctx context.Context, in *blog.QueryBlogRequest) (*blog.BlogSet, error) {
	set := blog.NewBlogSet()

	// 1. 初始化查询对象
	query := i.db.WithContext(ctx).Model(blog.Blog{})

	// 补充查询条件
	if in.CreateBy != "" {
		query = query.Where("create_by = ?", in.CreateBy)
	}

	// 查询总算
	err := query.Count(&set.Total).Error
	if err != nil {
		return nil, err
	}

	// 查询具体的数据
	err = query.
		Limit(in.Limit()).
		Offset(in.Offset()).
		Find(&set.Items).
		Error
	if err != nil {
		return nil, err
	}

	return set, nil
}
带着这个请求的请求文章, create_by就是过滤条件, 如果做成通用的, 就成了资源的scope
/vblog/api/v1/blogs?create_by="usera"

通过Token进行 硬控制

在编辑的时候,强制控制 访问数据的访问是 用户自己的

强制补充scope CreateBy

// + 修改博客(全量): PUT /vblogs/api/v1/blogs/:id
func (h *blogApiHandler) UpdateBlog(c *gin.Context) {
	// 如果解析路径里面的参数
	req := blog.NewUpdateBlogRequest(c.Param("id"))
	req.UpdateMode = common.UPDATE_MODE_PUT
	// 用户传递的数据
	if err := c.BindJSON(req.CreateBlogRequest); err != nil {
		response.Failed(c, err)
		return
	}

	// 后面请求如何获取 中间信息
	if v, ok := c.Get(token.TOKEN_MIDDLEWARE_KEY); ok {
		req.CreateBy = v.(*token.Token).UserName
	}
	ins, err := h.svc.UpdateBlog(c.Request.Context(), req)
	if err != nil {
		response.Failed(c, err)
		return
	}
	response.Success(c, ins)
}

补充 Scope访问控制

	// 更新数据库
	// UPDATE `blogs` SET `id`=48,`created_at`=1706344163,`updated_at`=1706344423,`title`='go语言全栈开发V2',`author`='oldyu',`content`='xxx',`summary`='xx',`tags`='{"目录":"Go语言"}' WHERE id = 48
	stmt := i.db.WithContext(ctx).Model(&blog.Blog{}).Where("id = ?", ins.Id)
	if req.CreateBy != "" {
		stmt = stmt.Where("create_by = ?", ins.CreateBy)
	}

为程序补充CLI

为什么需要CLI

如何初始化Root/管理员用户 是个问题? (Admin 来创建用户)

需要程序提供初始化的功能:

vblog init : 初始化admin用户
vblog start: 启动程序

// 带有子命令的CLI
kubectl pods list
kubectl pods delete
go run main.go

由cli为我们程序提供多个执行的入口:

  • 标准库: Flag
  • 第三方库: cobra

cobra 执行结构

首先定义Root Cmd

就是cli的根, 常用于:

  • 打印帮助信息
  • 打印一下全局信息, 版本信息
docker -v
Docker version 25.0.1, build 29cf629

// vblog init
var rootCmd = &cobra.Command{
  Use:   "hugo",
  Short: "Hugo is a very fast static site generator",
  Long: `A Fast and Flexible Static Site Generator built with
                love by spf13 and friends in Go.
                Complete documentation is available at https://gohugo.io/documentation/`,
  Run: func(cmd *cobra.Command, args []string) {
    // Do Stuff Here
  },
}

func Execute() {
  if err := rootCmd.Execute(); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

定义Root

package cmd

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
	Use:   "vblog",
	Short: "vblog api server",
	Run: func(cmd *cobra.Command, args []string) {
		// 什么都不做的时候打印帮助信息
		cmd.Help()
	},
}

func Execute() {
	// 注册Root命令的子命令
	rootCmd.AddCommand(initCmd, startCmd)

	if err := rootCmd.Execute(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

再定义 Sub Cmd

迁移start命令

package cmd

import (
	"gitee.com/baicaijc/vblog/conf"
	"gitee.com/baicaijc/vblog/ioc"
	"github.com/gin-gonic/gin"
	"github.com/spf13/cobra"
)

var startCmd = &cobra.Command{
	Use:   "start",
	Short: "启动服务器",
	Run: func(cmd *cobra.Command, args []string) {
		// 什么都不做的时候打印帮助信息
		// 1. 初始化程序配置, 这里没有配置,使用默认值
		if err := conf.LoadFromEnv(); err != nil {
			panic(err)
		}

		// 2. 程序对象管理
		if err := ioc.Init(); err != nil {
			panic(err)
		}

		// Protocol
		engine := gin.Default()

		rr := engine.Group("/vblog/api/v1")
		ioc.RegistryGinApi(rr)

		// 把Http协议服务器启动起来
		if err := engine.Run(":8080"); err != nil {
			panic(err)
		}
	},
}

定义init命令:

package cmd

import "github.com/spf13/cobra"

var initCmd = &cobra.Command{
	Use:   "init",
	Short: "程序初始化",
	Run: func(cmd *cobra.Command, args []string) {
		// 什么都不做的时候打印帮助信息
	},
}

启动CLI

执行Root Cmd

package main

import (

	// 1 初始化配置
	"gitee.com/baicaijc/vblog/cmd"
	_ "gitee.com/baicaijc/vblog/conf"
	// 2.1 通过import方法 完成注册
	_ "gitee.com/baicaijc/vblog/apps"
)

func main() {
	// // 2.2 程序对象管理
	// if err := ioc.Init(); err != nil {
	// 	panic(err)
	// }

	// // Protocol
	// engine := gin.Default()

	// rr := engine.Group("/vblog/api/v1")
	// // ioc.Api().Get(token.AppName).(*api.TokenApiHandler).Registry(rr)
	// ioc.RegistryGinApi(rr)

	// // 把Http协议服务器启动起来
	// if err := engine.Run(":8080"); err != nil {
	// 	panic(err)
	// }

	cmd.Execute()
}

启动CLI

go run main.go start

// 编译后再运行
// go build -o vblog main.go
// vblog start

完成Init命令

var initCmd = &cobra.Command{
	Use:   "init",
	Short: "程序初始化",
	Run: func(cmd *cobra.Command, args []string) {
		// 什么都不做的时候打印帮助信息
		// 程序对象管理
		cobra.CheckErr(ioc.Init())

		// 需要初始化 管理员用户
		// 使用构造函数创建请求对象
		// user.CreateUserRequest{}
		req := user.NewCreateUserRequest()
		req.Username = "admin"
		req.Password = xid.New().String()
		req.Role = user.ROLE_ADMIN

		fmt.Println("用户名: ", req.Username)
		fmt.Println("密码  : ", req.Password)

		u, err := ioc.Controller().Get(user.AppName).(user.Service).CreateUser(
			cmd.Context(),
			req,
		)
		cobra.CheckErr(err)

		// 正常打印对象
		fmt.Println(u)
	},
}

/*
PS E:\浏览器下载\mage_course\vblog> go run main.go init
用户名:  admin
密码  :  cn91gkdu48h3cl7dp6ig

2024/02/18 22:46:41 E:/浏览器下载/mage_course/vblog/apps/user/impl/user.go:37
[4.857ms] [rows:1] INSERT INTO `users` (`created_at`,`updated_at`,`username`,`password`,`role`,`label`) VALUES (1708267601,1708267601,'admin','$2a$10$XnUNDaZQ/IVuiLl7SrNR/uHDOFIpdgLlccciMJdBf34tN2urUSljm',1,'{}')
{
 "id": 15,
 "created_at": 1708267601,
 "updated_at": 1708267601,
 "username": "admin",
 "password": "$2a$10$XnUNDaZQ/IVuiLl7SrNR/uHDOFIpdgLlccciMJdBf34tN2urUSljm",
 "role": 1,
 "label": {}
}
PS E:\浏览器下载\mage_course\vblog> 
*/
# 初始化管理员用户
vblog init
# 再启动服务
vblog start

为程序添加脚手架

Makefile文件(windows 自己安装下 windows下的make)

PKG := "gitee.com/baicaijc/vblog"

dep: ## Get the dependencies
	@go mod tidy

run: ## Run Server
	@go run main.go start

help: ## Display this help screen
	@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
  • 变量声明: PKG := "xx"
  • 指令: run: go run main.go start
  • @ 表示是否打印命令本身
  • help: 通过awk 打印使用说明
➜  vblog git:(main) ✗ make help
dep                            Get the dependencies
run                            Run Server
help                           Display this help screen
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
Go
1
https://gitee.com/baicaijc/vblog.git
git@gitee.com:baicaijc/vblog.git
baicaijc
vblog
vlog
8e0b234b9a26

搜索帮助