Skip to content

purezeroadmin

一步步实现微服务权限管理系统(12)

前言

  • 前端参考了很多框架,可谓百花齐放,但很多封装过剩,不利于学习和应用,最终我选择了 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin)
  • 后端我将使用 go-zero 来带领大家一步步实现一个权限管理系统
  • 本系列项目存放在 purezeroadmin 中,每一部分我都将打tag,并保证每个tag能正常运行。请多点赞和评论。
  • 后面示例均为 purezeroadmin 项目为例,你们可以根据需要自建工程来进行试验。均采用vscode进行试验。
  • go-zero 常用命令我将放入其对应的 makefile 文件中。

本节概述

  • 前端角色管理相关逻辑修改
  • 后端增加相应数据和接口

前端修改

src/views/system/role/utils/hook.tsx

  • 状态值改为数字
const form = reactive({
    name: "",
    code: "",
    status: 1
});
  • 页码和页大小作为参数传递
async function onSearch() {
    loading.value = true;
    const { data } = await getRoleList({
        ...toRaw(form),
        page: pagination.currentPage,
        pageSize: pagination.pageSize
    });
    dataList.value = data.list;
    pagination.total = data.total;

    setTimeout(() => {
        loading.value = false;
    }, 500);
}

src/views/system/role/index.vue状态值改为数字

<el-option label="已启用" :value="1" />
<el-option label="已停用" :value="0" />

src/api/system.ts 获取角色菜单改为 get 方式

/** 获取角色管理-权限-菜单权限 */
export const getRoleMenu = (data?: object) => {
  return http.request<Result>("get", "/api/role-menu", { data });
};

后端修改

user-api/api/user.api 改动

增加 /api/role /api/role-menu /api/role-menu-ids 3个路由,用于查询角色,查询角色菜单,查询角色菜单所有id

type (
    UserRoleReq {
        Name     string `json:"name,optional"`
        Code     string `json:"code,optional"`
        Status   *int64 `json:"status,optional"`
        Page     int64  `json:"page,default=1"`
        PageSize int64  `json:"pageSize,default=10"`
    }
    UserRoleData {
        Id         int64  `json:"id"`
        Code       string `json:"code"`
        Name       string `json:"name"`
        Status     int64  `json:"status"`
        Remark     string `json:"remark"`
        CreateTime int64  `json:"createTime"`
        UpdateTime int64  `json:"updateTime"`
    }
    UserRoleResp {
        List  []*UserRoleData `json:"list"`
        Total int64           `json:"total"`
    }
)

@server (
    jwt: Auth // 开启 jwt 认证
)
service user-api {
    @doc "获取路由"
    @handler userRole
    post /api/role (UserRoleReq) returns ([]*UserRoleResp)
}

type (
    UserRoleMenuReq  {}
    UserRoleMenu {
        ParentId int64  `json:"parentId"`
        Id       int64  `json:"id"`
        MenuType int64  `json:"menuType"`
        Title    string `json:"title"`
    }
)

@server (
    jwt: Auth // 开启 jwt 认证
)
service user-api {
    @doc "获取菜单"
    @handler userMenu
    get /api/role-menu (UserRoleMenuReq) returns ([]*UserRoleMenu)
}

type (
    UserRoleMenuIDReq {
        Id int64 `json:"id"`
    }
)

@server (
    jwt: Auth // 开启 jwt 认证
)
service user-api {
    @doc "获取菜单详情"
    @handler userMenuID
    post /api/role-menu-ids (UserRoleMenuIDReq) returns ([]int64)
}

models文件夹修改

  • user-api/models/tbrolemodel.go增加根据状态值筛选
TbRoleModel interface {
    tbRoleModel
    withSession(session sqlx.Session) TbRoleModel
    FindAll(ctx context.Context) ([]*TbRole, error)
    FindList(ctx context.Context, name, code string, status *int64, page, pageSize int64) ([]*TbRole, int64, error)
}

...

func (m *customTbRoleModel) FindList(ctx context.Context, name, code string, status *int64, page, pageSize int64) ([]*TbRole, int64, error) {
    base := ""
    needAnd := false
    if name != "" {
        base += fmt.Sprintf("name = '%s'", name)
        needAnd = true
    }
    if code != "" {
        if needAnd {
            base += " and "
        }
        base += fmt.Sprintf("code = '%s'", code)
    }
    if status != nil {
        if needAnd {
            base += " and "
        }
        base += fmt.Sprintf("status = %v", *status)
    }
    if base != "" {
        base = " where " + base
    }

    var total int64
    err := m.conn.QueryRowCtx(ctx, &total, "select count(*) from tb_role"+base)
    if err != nil {
        return nil, 0, err
    }
    var list []*TbRole
    err = m.conn.QueryRowsCtx(ctx, &list, fmt.Sprintf("select * from tb_role%s limit ?,?", base), (page-1)*pageSize, pageSize)
    return list, total, err
}
  • user-api/models/tbroutermodel.go 增加获取所有路由
...
func (m *customTbRouterModel) FindAll(ctx context.Context) ([]*TbRouter, error) {
    var resp []*TbRouter
    err := m.conn.QueryRowsCtx(ctx, &resp, "select * from tb_router")
    return resp, err
}

user-api/helper/helper.go 增加

func GetAuthsInfos(ctx context.Context, svcCtx *svc.ServiceContext) (tbUser *models.TbUser, mTbRole map[int64]*models.TbRole, err error) {
    userID, err := GetUserIDFromContext(ctx)
    if err != nil {
        return nil, nil, err
    }

    tbUser, err = svcCtx.TbUserModel.FindOne(ctx, userID)
    if err != nil {
        return nil, nil, err
    }

    mTbRole = make(map[int64]*models.TbRole)

    if tbUser.Username == "admin" {
        tbRoles, err := svcCtx.TbRoleModel.FindAll(ctx)
        if err != nil {
            return nil, nil, err
        }

        for _, tbRole := range tbRoles {
            mTbRole[tbRole.Id] = tbRole
        }
        return tbUser, mTbRole, nil
    }

    roles, err := jsonutil.ToArray[string](tbUser.Roles)
    if err != nil {
        return nil, nil, err
    }

    for _, role := range roles {
        tbRole, err := svcCtx.TbRoleModel.FindOneByCode(ctx, role)
        if err != nil {
            return nil, nil, err
        }

        mTbRole[tbRole.Id] = tbRole
    }

    return tbUser, mTbRole, nil
}

logic修改

  • user-api/internal/logic/usermenuidlogic.go 根据角色ID获取菜单ID
func (l *UserMenuIDLogic) UserMenuID(req *types.UserRoleMenuIDReq) (resp []int64, err error) {
    _, mTbRole, err := helper.GetAuthsInfos(l.ctx, l.svcCtx)
    if err != nil {
        return nil, err
    }

    tbRole, ok := mTbRole[req.Id]
    if !ok {
        return nil, errors.New("无对应校色权限")
    }

    rolePermissions, err := jsonutil.ToArray[string](tbRole.Permissions)
    if err != nil {
        return nil, err
    }

    routers, err := l.svcCtx.TbRouterModel.FindAll(l.ctx)
    if err != nil {
        return nil, err
    }

    for _, router := range routers {
        if !helper.RouterPass(l.svcCtx, router, []string{tbRole.Code}, rolePermissions) {
            continue
        }
        resp = append(resp, router.Id)
    }

    return resp, nil
}
  • user-api/internal/logic/userrolelogic.go 获取角色数据
func (l *UserRoleLogic) UserRole(req *types.UserRoleReq) (resp *types.UserRoleResp, err error) {
    list, total, err := l.svcCtx.TbRoleModel.FindList(l.ctx, req.Name, req.Code, req.Status, req.Page, req.PageSize)
    if err != nil {
        return nil, err
    }

    resp = &types.UserRoleResp{
        List:  make([]*types.UserRoleData, len(list)),
        Total: total,
    }

    for i, tbRole := range list {
        resp.List[i] = &types.UserRoleData{
            Id:         tbRole.Id,
            Code:       tbRole.Code,
            Name:       tbRole.Name,
            Status:     tbRole.Status,
            Remark:     tbRole.Remark,
            CreateTime: tbRole.CreateTs,
            UpdateTime: tbRole.UpdateTs,
        }
    }

    return resp, nil
}
  • user-api/internal/logic/usermenulogic.go 获取角色菜单数据
func (l *UserMenuLogic) UserMenu(req *types.UserRoleMenuReq) (resp []*types.UserRoleMenu, err error) {
    userID, err := helper.GetUserIDFromContext(l.ctx)
    if err != nil {
        return nil, err
    }

    tbUser, err := l.svcCtx.TbUserModel.FindOne(l.ctx, userID)
    if err != nil {
        return nil, err
    }

    roles, permissions, err := helper.GetAuths(l.ctx, l.svcCtx, tbUser)
    if err != nil {
        return nil, err
    }

    err = l.GetMenuByParentID(0, roles, permissions, &resp)
    if err != nil {
        return nil, err
    }
    return resp, nil
}

func (l *UserMenuLogic) GetMenuByParentID(parentID int64, roles, permissions []string, roleMenus *[]*types.UserRoleMenu) (err error) {
    routers, err := l.svcCtx.TbRouterModel.FindAllFromParentID(l.ctx, parentID)
    if err != nil {
        return err
    }

    for _, v := range routers {
        if pass := helper.RouterPass(l.svcCtx, v, roles, permissions); pass {
            *roleMenus = append(*roleMenus, &types.UserRoleMenu{
                ParentId: v.ParentId,
                Id:       v.Id,
                MenuType: v.MenuType,
                Title:    v.MetaTitle,
            })
            err = l.GetMenuByParentID(v.Id, roles, permissions, roleMenus)
            if err != nil {
                return err
            }
        }
    }

    return nil
}

测试

  • 前端测试,可发现权限页面出现
  • 权限获取正常

tag版本

purezeroadmin 项目下

git checkout v1.11.0

接下来

权限编辑

一步步实现微服务权限管理系统(11)

前言

  • 前端参考了很多框架,可谓百花齐放,但很多封装过剩,不利于学习和应用,最终我选择了 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin)
  • 后端我将使用 go-zero 来带领大家一步步实现一个权限管理系统
  • 本系列项目存放在 purezeroadmin 中,每一部分我都将打tag,并保证每个tag能正常运行。请多点赞和评论。
  • 后面示例均为 purezeroadmin 项目为例,你们可以根据需要自建工程来进行试验。均采用vscode进行试验。
  • go-zero 常用命令我将放入其对应的 makefile 文件中。

本节概述

  • 前端精简版中加入角色管理
  • 后端增加相应数据和接口

前端修改

  • 在外部通过命令pure 创建 vue-pure-admin 项目
  • vue-pure-admin 项目 中 src/views/system 拷贝到当前项目对应位置
  • 只保留role 相关数据
  • 删除多余的依赖项
  • 因为没采用国际化,所以删除 transformI18n 相关依赖
  • 感觉迁移困难可以参考本机tag的前端部分

后端修改

角色相关信息修改

官方前端示例中 code 作为其标识,所以先修改为跟官方一致,再修改和增加其他字段

  • tb_role 字段修改
ALTER TABLE `purezeroadmin`.`tb_role` CHANGE `name` `code` VARCHAR(32) NOT NULL; 
  • 重新生成model
goctl model mysql datasource --url "root:123456@tcp(127.0.0.1:55506)/purezeroadmin"  -t="tb_*" --dir user-api/models --home template

生成成功后运行后端程序成功,当前中间tag版本 1.10.0

  • 修改 tb_role 表信息以符合角色管理
ALTER TABLE `purezeroadmin`.`tb_role`
  ADD COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT FIRST,
  ADD COLUMN `name` VARCHAR (32) NOT NULL AFTER `code`,
  ADD COLUMN `status` TINYINT (1) NOT NULL AFTER `name`,
  ADD COLUMN `remark` VARCHAR (256) NOT NULL AFTER `status`,
  DROP PRIMARY KEY,
  ADD PRIMARY KEY (`id`),
  ADD UNIQUE INDEX (`code`);
UPDATE `purezeroadmin`.`tb_role` SET `name` = '超级管理员' , `status` = '1' WHERE `id` = '1';
update `purezeroadmin`.`tb_roLE` SEt `name` = '普通角色' , `Status` = '1' where `id` = '2'; 
update `pUREZEroadmin`.`tb_role` set `remark` = '超级管理员拥有最高权限' where `ID` = '1'; 
update `purezeroadmin`.`tb_role` set `REMArk` = '普通角色拥有部分权限' wherE `ID` = '2';
UPDATE `purezeroadmin`.`tb_role` SET `create_ts` = '1605456000000' , `update_ts` = '1684512000000' WHERE `id` = '1'; 
update `purezeroadmin`.`tb_rOLE` Set `create_ts` = '1605456000000' , `update_ts` = '1684512000000' where `id` = '2'; 
  • 重新生成model,并修改 user-api/helper/helper.go
func GetAuths(ctx context.Context, svcCtx *svc.ServiceContext, tbUser *models.TbUser) (roles, permissions []string, err error) {
    roles, err = jsonutil.ToArray[string](tbUser.Roles)
    if err != nil {
        return nil, nil, err
    }

    for _, role := range roles {
        tbRole, err := svcCtx.TbRoleModel.FindOneByCode(ctx, role)
        if err != nil {
            return nil, nil, err
        }

        rolePermissions, err := jsonutil.ToArray[string](tbRole.Permissions)
        if err != nil {
            return nil, nil, err
        }

        permissions = arrutil.UniqueConcat(permissions, rolePermissions)
    }

    return roles, permissions, nil
}
  • 增加获取所有角色

  • tb_router 表修改, 用于存储菜单类型

ALTER TABLE `purezeroadmin`.`tb_router` ADD COLUMN `menu_type` BIGINT NOT NULL AFTER `parent_id`;

tag版本

purezeroadmin 项目下

git checkout v1.10.0

接下来

前端引入角色管理,可编辑管理权限

一步步实现微服务权限管理系统(10)

前言

  • 前端参考了很多框架,可谓百花齐放,但很多封装过剩,不利于学习和应用,最终我选择了 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin)
  • 后端我将使用 go-zero 来带领大家一步步实现一个权限管理系统
  • 本系列项目存放在 purezeroadmin 中,每一部分我都将打tag,并保证每个tag能正常运行。请多点赞和评论。
  • 后面示例均为 purezeroadmin 项目为例,你们可以根据需要自建工程来进行试验。均采用vscode进行试验。
  • go-zero 常用命令我将放入其对应的 makefile 文件中。

本节概述

  • casbin 规则文件持久化
  • casbin 没有对应的 go-zero 的orm模型适配器, 而 go-zero 的orm框架调用了 database/sql, 可以用github.com/Blank-Xu/sql-adapter 来实现

后端修改

引入adpater

  • user-api/internal/svc/servicecontext.go
conn := sqlx.NewMysql(c.Dsn)
db, err := conn.RawDB()
if err != nil {
    logx.Errorf("Error on conn.RawDB: %+v", err)
    return nil
}
policy, err := sqladapter.NewAdapter(db, "mysql", "casbin_rule")
if err != nil {
    logx.Errorf("Error on sqladapter.NewAdapter: %+v", err)
    return nil
}
enforcer, err := casbin.NewSyncedEnforcer(c.Casbin.ModelFile, policy)
if err != nil {
    logx.Errorf("Error on casbin.NewSyncedEnforcer: %+v", err)
    return nil
}
  • 启动后端,然后插入策略文件
insert  into `casbin_rule`(`p_type`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) values ('p','1000000','/permission','get','','','');
insert  into `casbin_rule`(`p_type`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) values ('p','1010000','/permission/page/index','get','','','');
insert  into `casbin_rule`(`p_type`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) values ('p','1020000','/permission/button','get','','','');
insert  into `casbin_rule`(`p_type`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) values ('p','1020100','/permission/button/router','get','','','');
insert  into `casbin_rule`(`p_type`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) values ('p','1020200','/permission/button/login','get','','','');
insert  into `casbin_rule`(`p_type`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) values ('g','permission:btn:add','1020100','','','','');
insert  into `casbin_rule`(`p_type`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) values ('g','permission:btn:edit','1020100','','','','');
insert  into `casbin_rule`(`p_type`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) values ('g','permission:btn:delete','1020100','','','','');
insert  into `casbin_rule`(`p_type`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) values ('g','common','1000000','','','','');
insert  into `casbin_rule`(`p_type`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) values ('g','common','1020000','','','','');
insert  into `casbin_rule`(`p_type`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) values ('g','common','permission:btn:add','','','','');
insert  into `casbin_rule`(`p_type`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) values ('g','common','permission:btn:edit','','','','');
insert  into `casbin_rule`(`p_type`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) values ('g','common','1020200','','','','');

测试

  • 前后端测试,通过

添加一行策略测试

user-api/internal/svc/servicecontext.go

// 测试完清空此内容
enforcer.AddPolicy("10086", "/10086", "get")

可以看的启动后端后,数据库多了一行记录

其他操作

policy 持久化到数据库中了,不需要原始文件及相关操作了

  • 删除 casbin.csv 文件
  • 删除 user-api/etc/user-api.yaml 相关配置信息
  • 删除 user-api/internal/config/config.go 相关配置信息

tag版本

purezeroadmin 项目下

git checkout v1.9.0

接下来

前端引入角色管理,可编辑管理权限

一步步实现微服务权限管理系统(9)

前言

  • 前端参考了很多框架,可谓百花齐放,但很多封装过剩,不利于学习和应用,最终我选择了 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin)
  • 后端我将使用 go-zero 来带领大家一步步实现一个权限管理系统
  • 本系列项目存放在 purezeroadmin 中,每一部分我都将打tag,并保证每个tag能正常运行。请多点赞和评论。
  • 后面示例均为 purezeroadmin 项目为例,你们可以根据需要自建工程来进行试验。均采用vscode进行试验。
  • go-zero 常用命令我将放入其对应的 makefile 文件中。

本节概述

  • casbin 是一个非常流行的访问控制模型,用来RBAC权限控制非常方便, 参考 https://casbin.org/zh/docs/rbac
  • 先将路由所对应的权限映射为casbin的文件测试
  • 之前生成路由树的逻辑中权限判断可以交给 casbin 来做了

后端修改

测试 casbin

  • 创建 casbin.conf
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act || r.sub == "admin"
  • 创建 casbin.csv
p, 1, permission:btn:add, get
p, 2, permission:btn:edit, get
p, 3, permission:btn:delete, get

p, 1000000, /permission, get
p, 1010000, /permission/page/index, get
p, 1020000, /permission/button, get
p, 1020100, /permission/button/router, get
p, 1020200, /permission/button/login, get

g, 1, 1020100
g, 2, 1020100
g, 3, 1020100

g, common,  1000000
g, common,  1020000
g, common,  1
g, common,  2
g, common,  1020200
  • 测试请求
common, /permission, get
common, /permission/page/index, get
common, /permission/button, get
common, /permission/button/router, get
common, /permission/button/login, get
common, permission:btn:add, get
common, permission:btn:edit, get
common, permission:btn:delete, get

admin, /permission, get
admin, /permission/page/index, get
admin, /permission/button, get
admin, /permission/button/router, get
admin, /permission/button/login, get
admin, permission:btn:add, get
admin, permission:btn:edit, get
admin, permission:btn:delete, get
  • 执行结果
true Reason: ["1000000","/permission","get"]
false
true Reason: ["1020000","/permission/button","get"]
true Reason: ["1020100","/permission/button/router","get"]
true Reason: ["1020200","/permission/button/login","get"]
true Reason: ["1","permission:btn:add","get"]
true Reason: ["2","permission:btn:edit","get"]
false
// ignore
true Reason: ["1","permission:btn:add","get"]
true Reason: ["1","permission:btn:add","get"]
true Reason: ["1","permission:btn:add","get"]
true Reason: ["1","permission:btn:add","get"]
true Reason: ["1","permission:btn:add","get"]
true Reason: ["1","permission:btn:add","get"]
true Reason: ["1","permission:btn:add","get"]
true Reason: ["1","permission:btn:add","get"]
// ignore

可根据上述文件在 https://casbin.org/zh/editor 进行测试

permission:btn:add 这些是为了查看调用所以映射了subject

casbin 引入到后端

  • 配置信息修改

user-api/etc/user-api.yaml

# casbin
Casbin:
  ModelFile: "casbin.conf"
  PolicyFile: "casbin.csv"

user-api/internal/config/config.go

type Config struct {
    ...
    Casbin struct {
        ModelFile  string
        PolicyFile string
    }
}
  • 修改服务上下文user-api/internal/svc/servicecontext.go
type ServiceContext struct {
    Config        config.Config
    TbUserModel   models.TbUserModel
    TbRoleModel   models.TbRoleModel
    TbRouterModel models.TbRouterModel
    Enforcer      *casbin.SyncedEnforcer
}

func NewServiceContext(c config.Config) *ServiceContext {
    conn := sqlx.NewMysql(c.Dsn)
    enforcer, err := casbin.NewSyncedEnforcer(c.Casbin.ModelFile, c.Casbin.PolicyFile)
    if err != nil {
        logx.Errorf("Error on NewSyncedEnforcer: %+v", err)
        return nil
    }
    return &ServiceContext{
        Config:        c,
        TbUserModel:   models.NewTbUserModel(conn),
        TbRoleModel:   models.NewTbRoleModel(conn),
        TbRouterModel: models.NewTbRouterModel(conn),
        Enforcer:      enforcer,
    }
}
  • user-api/helper/helper.go 判断逻辑改成casbin来判断
func RouterToData(svcCtx *svc.ServiceContext, router *models.TbRouter, isAdmin bool, roles, permissions []string) (routerData *types.RouterData, err error) {
    routerData = &types.RouterData{
        Path:      router.Path,
        Name:      router.Name,
        Component: router.Component,
        Meta: types.Meta{
            Title: router.MetaTitle,
            Icon:  router.MetaIcon,
            Rank:  router.MetaRank,
        },
    }

    pass := false
    for _, role := range roles {
        getPass, _ := svcCtx.Enforcer.Enforce(role, routerData.Path, "get")
        if getPass {
            pass = true
        }
    }
    for _, permission := range permissions {
        getPass, _ := svcCtx.Enforcer.Enforce(permission, routerData.Path, "get")
        if getPass {
            pass = true
        }
    }

    if !pass {
        return nil, nil
    }

    if router.MetaRoles.Valid {
        routerData.Meta.Roles, err = jsonutil.ToArray[string](router.MetaRoles.String)
        if err != nil {
            return nil, err
        }
    }

    if router.MetaAuths.Valid {
        routerData.Meta.Auths, err = jsonutil.ToArray[string](router.MetaAuths.String)
        if err != nil {
            return nil, err
        }
    }

    return routerData, nil
}

测试

  • 前后端测试,通过

tag版本

purezeroadmin 项目下

git checkout v1.8.0

接下来

casbin 规则持久化

一步步实现微服务权限管理系统(8)

前言

  • 前端参考了很多框架,可谓百花齐放,但很多封装过剩,不利于学习和应用,最终我选择了 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin)
  • 后端我将使用 go-zero 来带领大家一步步实现一个权限管理系统
  • 本系列项目存放在 purezeroadmin 中,每一部分我都将打tag,并保证每个tag能正常运行。请多点赞和评论。
  • 后面示例均为 purezeroadmin 项目为例,你们可以根据需要自建工程来进行试验。均采用vscode进行试验。
  • go-zero 常用命令我将放入其对应的 makefile 文件中。

本节概述

  • 路由数据持久化
  • 路由数据组装

后端修改

新增 tb_router 表及相关数据

  • 生成对应数据
/*!40101 SET NAMES utf8mb4 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`purezeroadmin` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;

USE `purezeroadmin`;

/*Table structure for table `tb_router` */

CREATE TABLE `tb_router` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `parent_id` bigint NOT NULL DEFAULT '0',
  `path` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '',
  `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '',
  `component` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '',
  `meta_title` varchar(64) NOT NULL,
  `meta_icon` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '',
  `meta_rank` bigint NOT NULL DEFAULT '0',
  `meta_roles` json DEFAULT NULL,
  `meta_auths` json DEFAULT NULL,
  `create_ts` bigint NOT NULL DEFAULT '0',
  `update_ts` bigint NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

/*Data for the table `tb_router` */

insert  into `tb_router`(`id`,`parent_id`,`path`,`name`,`component`,`meta_title`,`meta_icon`,`meta_rank`,`meta_roles`,`meta_auths`,`create_ts`,`update_ts`) values (1,0,'/permission','','','权限管理','ep:lollipop',10,NULL,NULL,0,0);
insert  into `tb_router`(`id`,`parent_id`,`path`,`name`,`component`,`meta_title`,`meta_icon`,`meta_rank`,`meta_roles`,`meta_auths`,`create_ts`,`update_ts`) values (2,1,'/permission/page/index','PermissionPage','','页面权限','',0,'[\"admin\"]',NULL,0,0);
insert  into `tb_router`(`id`,`parent_id`,`path`,`name`,`component`,`meta_title`,`meta_icon`,`meta_rank`,`meta_roles`,`meta_auths`,`create_ts`,`update_ts`) values (3,1,'/permission/button','','','按钮权限','',0,'[\"admin\", \"common\"]',NULL,0,0);
insert  into `tb_router`(`id`,`parent_id`,`path`,`name`,`component`,`meta_title`,`meta_icon`,`meta_rank`,`meta_roles`,`meta_auths`,`create_ts`,`update_ts`) values (4,3,'/permission/button/router','PermissionButtonRouter','permission/button/index','路由返回按钮权限','',0,NULL,'[\"permission:btn:add\", \"permission:btn:edit\", \"permission:btn:delete\"]',0,0);
insert  into `tb_router`(`id`,`parent_id`,`path`,`name`,`component`,`meta_title`,`meta_icon`,`meta_rank`,`meta_roles`,`meta_auths`,`create_ts`,`update_ts`) values (5,3,'/permission/button/login','PermissionButtonLogin','permission/button/perms','登录接口返回按钮权限','',0,NULL,NULL,0,0);
  • 生成 models
goctl model mysql datasource --url "root:123456@tcp(127.0.0.1:55506)/purezeroadmin"  -t="*" --dir user-api/models --home template
  • 配置 muser-api/internal/svc/servicecontext.go
type ServiceContext struct {
    Config        config.Config
    TbUserModel   models.TbUserModel
    TbRoleModel   models.TbRoleModel
    TbRouterModel models.TbRouterModel
}

func NewServiceContext(c config.Config) *ServiceContext {
    conn := sqlx.NewMysql(c.Dsn)
    return &ServiceContext{
        Config:        c,
        TbUserModel:   models.NewTbUserModel(conn),
        TbRoleModel:   models.NewTbRoleModel(conn),
        TbRouterModel: models.NewTbRouterModel(conn),
    }
}
  • user-api/models/tbroutermodel.go 增加函数
...

TbRouterModel interface {
    tbRouterModel
    withSession(session sqlx.Session) TbRouterModel
    FindAllFromParentID(ctx context.Context, parentID int64) ([]*TbRouter, error)
}

...

func (m *customTbRouterModel) FindAllFromParentID(ctx context.Context, parentID int64) ([]*TbRouter, error) {
    var resp []*TbRouter
    err := m.conn.QueryRowsCtx(ctx, &resp, "select * from tb_router where parent_id = ?", parentID)
    return resp, err
}

user-api/api/user.apiRouterData 返回修改为指针形式

type RouterData {
    Path      string        `json:"path"`
    Name      string        `json:"name,omitempty"`
    Component string        `json:"component,omitempty"`
    Meta      Meta          `json:"meta"`
    Children  []*RouterData `json:"children,omitempty"`
}

...

@server (
    jwt: Auth // 开启 jwt 认证
)
service user-api {
    @doc "获取路由"
    @handler userRouter
    get /api/get-async-routes (UserRouterReq) returns ([]*RouterData)
}

user-api/internal/logic/userrouterlogic.go 主要生成逻辑

func (l *UserRouterLogic) UserRouter(req *types.UserRouterReq) (resp []*types.RouterData, err error) {
    userID, err := helper.GetUserIDFromContext(l.ctx)
    if err != nil {
        return nil, err
    }

    tbUser, err := l.svcCtx.TbUserModel.FindOne(l.ctx, userID)
    if err != nil {
        return nil, err
    }

    roles, permissions, err := helper.GetAuths(l.ctx, l.svcCtx, tbUser)
    if err != nil {
        return nil, err
    }

    isAdmin := arrutil.Contains(roles, "admin")

    return l.GetRecursionRoutersByParentID(0, isAdmin, roles, permissions)
}

func (l *UserRouterLogic) GetRouterByID(id int64, isAdmin bool, roles, permissions []string) (routerData *types.RouterData, err error) {

    router, err := l.svcCtx.TbRouterModel.FindOne(l.ctx, id)
    if err != nil {
        return nil, err
    }

    return helper.RouterToData(router, isAdmin, roles, permissions)
}

func (l *UserLoginLogic) GetRoutersByParentID(parentID int64, isAdmin bool, roles, permissions []string) (routerDatas []*types.RouterData, err error) {
    routers, err := l.svcCtx.TbRouterModel.FindAllFromParentID(l.ctx, parentID)
    if err != nil {
        return nil, err
    }

    for _, v := range routers {
        routerData, err := helper.RouterToData(v, isAdmin, roles, permissions)
        if err != nil {
            return nil, err
        }
        routerDatas = append(routerDatas, routerData)
    }

    return routerDatas, nil
}

func (l *UserRouterLogic) UpdateRouterData(routerData *types.RouterData, id int64, isAdmin bool, roles, permissions []string) (err error) {
    routers, err := l.svcCtx.TbRouterModel.FindAllFromParentID(l.ctx, id)
    if err != nil {
        return err
    }

    for _, v := range routers {
        child, err := l.GetRecursionRouterByID(v.Id, isAdmin, roles, permissions)
        if err != nil {
            return err
        }
        if child == nil {
            continue
        }
        routerData.Children = append(routerData.Children, child)
    }
    return nil
}

func (l *UserRouterLogic) GetRecursionRouterByID(id int64, isAdmin bool, roles, permissions []string) (routerData *types.RouterData, err error) {

    routerData, err = l.GetRouterByID(id, isAdmin, roles, permissions)
    if err != nil {
        return nil, err
    }

    err = l.UpdateRouterData(routerData, id, isAdmin, roles, permissions)
    return routerData, err
}

func (l *UserRouterLogic) GetRecursionRoutersByParentID(parentID int64, isAdmin bool, roles, permissions []string) (routerDatas []*types.RouterData, err error) {
    routers, err := l.svcCtx.TbRouterModel.FindAllFromParentID(l.ctx, parentID)
    if err != nil {
        return nil, err
    }

    for _, v := range routers {
        routerData, err := helper.RouterToData(v, isAdmin, roles, permissions)
        if err != nil {
            return nil, err
        }
        err = l.UpdateRouterData(routerData, v.Id, isAdmin, roles, permissions)
        if err != nil {
            return nil, err
        }
        routerDatas = append(routerDatas, routerData)
    }

    return routerDatas, nil
}

测试

  • 登陆不同用户可观察到菜单变化

tag版本

purezeroadmin 项目下

git checkout v1.7.0

接下来

casbin的引入

一步步实现微服务权限管理系统(7)

前言

  • 前端参考了很多框架,可谓百花齐放,但很多封装过剩,不利于学习和应用,最终我选择了 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin)
  • 后端我将使用 go-zero 来带领大家一步步实现一个权限管理系统
  • 本系列项目存放在 purezeroadmin 中,每一部分我都将打tag,并保证每个tag能正常运行。请多点赞和评论。
  • 后面示例均为 purezeroadmin 项目为例,你们可以根据需要自建工程来进行试验。均采用vscode进行试验。
  • go-zero 常用命令我将放入其对应的 makefile 文件中。

本节概述

  • 之前数据都是模拟的,现在需要将数据持久化,这里我选择使用mysql。
  • 本节及之后的后端辅助函数示例请自行查看utls目录下的文件

后端修改

增加docker-compose.yaml 文件,创建测试mysql数据库

services:
  mysql:
    restart: always
    privileged: true
    image: mysql:8.0.40
    container_name: purezeroadmin
    volumes:
      - ./data/mysql:/var/lib/mysql
    command:
      --default-authentication-plugin=mysql_native_password
      --explicit_defaults_for_timestamp=true
      --max_connections=5000
    environment:  
      TZ: Asia/Shanghai
      MYSQL_ROOT_PASSWORD: "123456"
      MYSQL_INITDB_SKIP_TZINFO: "Asia/Shanghai"
      LANG: C.UTF-8 
    ports:
      - 55506:3306

启动mysql后创建测试数据库和表

/*!40101 SET NAMES utf8mb4 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`purezeroadmin` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;

USE `purezeroadmin`;

/*Table structure for table `tb_role` */

CREATE TABLE `tb_role` (
  `name` varchar(32) NOT NULL,
  `permissions` json NOT NULL,
  `create_ts` bigint NOT NULL,
  `update_ts` bigint NOT NULL,
  PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

/*Data for the table `tb_role` */

insert  into `tb_role`(`name`,`permissions`,`create_ts`,`update_ts`) values ('admin','[\"*:*:*\"]',1,1);
insert  into `tb_role`(`name`,`permissions`,`create_ts`,`update_ts`) values ('common','[\"permission:btn:add\", \"permission:btn:edit\"]',2,2);

/*Table structure for table `tb_user` */

CREATE TABLE `tb_user` (
  `user_id` bigint NOT NULL AUTO_INCREMENT,
  `username` varchar(64) NOT NULL,
  `password` varchar(256) NOT NULL,
  `nickname` varchar(64) NOT NULL,
  `avatar` varchar(256) NOT NULL,
  `roles` json NOT NULL,
  `create_ts` bigint NOT NULL,
  `update_ts` bigint NOT NULL,
  PRIMARY KEY (`user_id`),
  UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

/*Data for the table `tb_user` */

insert  into `tb_user`(`user_id`,`username`,`password`,`nickname`,`avatar`,`roles`,`create_ts`,`update_ts`) values (1,'admin','0192023a7bbd73250516f069df18b500','小铭','https://avatars.githubusercontent.com/u/44761321','[\"admin\"]',20060102150405,20060102150405);
insert  into `tb_user`(`user_id`,`username`,`password`,`nickname`,`avatar`,`roles`,`create_ts`,`update_ts`) values (2,'common','f9851fac6a5fa7f2500099c8715cc436','小林','https://avatars.githubusercontent.com/u/52823142','[\"admin\", \"common\"]',20060102150405,20060102150405);

生成 models

goctl model mysql datasource --url "root:123456@tcp(127.0.0.1:55506)/purezeroadmin"  -t="*" --dir user-api/models --home template

配置增加mysql连接信息

# mysql dsn
Dsn: "root:123456@tcp(127.0.0.1:55506)/purezeroadmin?charset=utf8mb4&parseTime=true&loc=Local"

user-api/internal/config/config.go同理

type Config struct {
    rest.RestConf
    Auth struct { // JWT 认证需要的密钥和过期时间配置
        AccessSecret string
        AccessExpire int64 // 单位为秒
    }
    Dsn string // mysql dsn
}

user-api/internal/svc/servicecontext.go 增加mysql相关model初始化

package svc

import (
    "backend/user-api/internal/config"
    "backend/user-api/models"

    "github.com/zeromicro/go-zero/core/stores/sqlx"
)

type ServiceContext struct {
    Config      config.Config
    TbUserModel models.TbUserModel
    TbRoleModel models.TbRoleModel
}

func NewServiceContext(c config.Config) *ServiceContext {
    conn := sqlx.NewMysql(c.Dsn)
    return &ServiceContext{
        Config:      c,
        TbUserModel: models.NewTbUserModel(conn),
        TbRoleModel: models.NewTbRoleModel(conn),
    }
}

user-api/internal/logic/userloginlogic.go 登陆逻辑修改

func (l *UserLoginLogic) UserLogin(req *types.UserLoginReq) (resp *types.UserLoginResp, err error) {

    tbUser, err := l.svcCtx.TbUserModel.FindOneByUsername(l.ctx, req.UserName)
    if err != nil {
        return nil, err
    }

    if tbUser.Password != codeutil.Md5Str(req.Password) {
        return nil, errors.New("用户或密码错误")
    }

    roles, err := jsonutil.ToArray[string](tbUser.Roles)
    if err != nil {
        return nil, err
    }

    var permissions []string
    for _, role := range roles {
        tbRole, err := l.svcCtx.TbRoleModel.FindOne(l.ctx, role)
        if err != nil {
            return nil, err
        }

        rolePermissions, err := jsonutil.ToArray[string](tbRole.Permissions)
        if err != nil {
            return nil, err
        }

        permissions = arrutil.UniqueConcat(permissions, rolePermissions)
    }

    tNow := time.Now()
    tExpire := tNow.Add(time.Second * time.Duration(l.svcCtx.Config.Auth.AccessExpire))

    mPayload := map[string]any{
        global.CtxJwtUserIDKey: tbUser.UserId,
    }

    accessToken, err := jwtutil.GetToken(l.svcCtx.Config.Auth.AccessSecret, tNow.Unix(), tExpire.Unix(), mPayload)
    if err != nil {
        return nil, err
    }

    refreshToken, err := jwtutil.GetToken(l.svcCtx.Config.Auth.AccessSecret, tNow.Unix(), tExpire.Unix()+86400, mPayload)
    if err != nil {
        return nil, err
    }

    return &types.UserLoginResp{
        Avatar:       tbUser.Avatar,
        Username:     tbUser.Username,
        Nickname:     tbUser.Nickname,
        Roles:        roles,
        Permissions:  permissions,
        AccessToken:  accessToken,
        RefreshToken: refreshToken,
        Expires:      tExpire.Format("2006/01/02 15:04:05"),
    }, nil
}

测试

  • 前后端分别启动测试,正常

tag版本

purezeroadmin 项目下

git checkout v1.6.0

接下来

路由数据也需要进行持久化

一步步实现微服务权限管理系统(6)

前言

  • 前端参考了很多框架,可谓百花齐放,但很多封装过剩,不利于学习和应用,最终我选择了 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin)
  • 后端我将使用 go-zero 来带领大家一步步实现一个权限管理系统
  • 本系列项目存放在 purezeroadmin 中,每一部分我都将打tag,并保证每个tag能正常运行。请多点赞和评论。
  • 后面示例均为 purezeroadmin 项目为例,你们可以根据需要自建工程来进行试验。均采用vscode进行试验。
  • go-zero 常用命令我将放入其对应的 makefile 文件中。

待解决

当前jwt都是模拟的,本节加上处理逻辑

后端修改

添加jwt认证

  • 修改 user-api/api/user.api

删除了 refreshToken 作为json参数传入,前端采用 jwt 规范传入 refreshToken

type (
    UserRefreshTokenReq {
    }
    UserRefreshTokenResp {
        AccessToken  string `json:"accessToken"`
        RefreshToken string `json:"refreshToken"`
        Expires      string `json:"expires"`
    }
)

@server (
    jwt: Auth // 开启 jwt 认证
)
service user-api {
    @doc "刷新token"
    @handler UserRefreshToken
    post /api/refresh-token (UserRefreshTokenReq) returns (UserRefreshTokenResp)
}

...

@server (
    jwt: Auth // 开启 jwt 认证
)
service user-api {
    @doc "获取路由"
    @handler userRouter
    get /api/get-async-routes (UserRouterReq) returns ([]RouterData)
}
- 增加Auth配置 user-api/etc/user-api.yaml

# jwt认证相关
Auth:
  AccessSecret: "purezeroadminxxx"
  AccessExpire: 86400
- 增加配置结构体 user-api/internal/config/config.go

type Config struct {
    rest.RestConf
    Auth struct { // JWT 认证需要的密钥和过期时间配置
        AccessSecret string
        AccessExpire int64 // 单位为秒
    }
}
  • 重新生成代码
goctl api  go --api user-api/api/user.api --dir user-api --home template

修改jwt相关逻辑

  • utls/jwtutil/jwt.go 新建工具类函数
package jwtutil

import (
    "github.com/golang-jwt/jwt/v4"
)

func GetToken(secretKey string, iat, seconds int64, payload map[string]any) (string, error) {
    claims := make(jwt.MapClaims)
    claims["exp"] = iat + seconds
    claims["iat"] = iat

    for k, v := range payload {
        claims[k] = v
    }

    token := jwt.New(jwt.SigningMethodHS256)
    token.Claims = claims
    return token.SignedString([]byte(secretKey))
}
  • user-api/global/ctxdata.go 增加jwt payload的key
package global

const CtxJwtUserIDKey = "userID"
  • user-api/internal/logic/userloginlogic.go 修改jwt相关逻辑
func (l *UserLoginLogic) UserLogin(req *types.UserLoginReq) (resp *types.UserLoginResp, err error) {
    var userID int64
    resp = &types.UserLoginResp{
        Username: req.UserName,
    }

    if req.UserName == "admin" && req.Password == "admin123" {
        userID = 1
        resp.Avatar = "https://avatars.githubusercontent.com/u/44761321"
        resp.Nickname = "小铭"
        resp.Roles = []string{"admin"}
        resp.Permissions = []string{"*:*:*"}
    } else if req.UserName == "common" && req.Password == "common123" {
        userID = 2
        resp.Avatar = "https://avatars.githubusercontent.com/u/52823142"
        resp.Nickname = "小林"
        resp.Roles = []string{"common"}
        resp.Permissions = []string{"permission:btn:add", "permission:btn:edit"}
    } else {
        return nil, errors.New("登陆失败")
    }

    tNow := time.Now()
    tExpire := tNow.Add(time.Second * time.Duration(l.svcCtx.Config.Auth.AccessExpire))

    mPayload := map[string]any{
        global.CtxJwtUserIDKey: userID,
    }

    accessToken, err := jwtutil.GetToken(l.svcCtx.Config.Auth.AccessSecret, tNow.Unix(), tExpire.Unix(), mPayload)
    if err != nil {
        return nil, err
    }

    refreshToken, err := jwtutil.GetToken(l.svcCtx.Config.Auth.AccessSecret, tNow.Unix(), tExpire.Unix()+86400, mPayload)
    if err != nil {
        return nil, err
    }

    resp.AccessToken = accessToken
    resp.RefreshToken = refreshToken
    resp.Expires = tExpire.Format("2006/01/02 15:04:05")

    return resp, nil
}
  • user-api/helper/helper.go jwt获取userID
package helper

import (
    "backend/user-api/global"
    "context"
    "fmt"
)

func GetUserIDFromContext(ctx context.Context) (int64, error) {

    uid, ok := ctx.Value(global.CtxJwtUserIDKey).(json.Number)
    if !ok {
        return 0, fmt.Errorf("jwt has no userID")
    }

    return uid.Int64()
}
  • user-api/internal/logic/userrefreshtokenlogic.go 修改jwt相关逻辑
func (l *UserRefreshTokenLogic) UserRefreshToken(req *types.UserRefreshTokenReq) (resp *types.UserRefreshTokenResp, err error) {
    userID, err := helper.GetUserIDFromContext(l.ctx)
    if err != nil {
        return nil, err
    }

    tNow := time.Now()
    tExpire := tNow.Add(time.Second * time.Duration(l.svcCtx.Config.Auth.AccessExpire))

    mPayload := map[string]any{
        global.CtxJwtUserIDKey: userID,
    }

    accessToken, err := jwtutil.GetToken(l.svcCtx.Config.Auth.AccessSecret, tNow.Unix(), tExpire.Unix(), mPayload)
    if err != nil {
        return nil, err
    }

    refreshToken, err := jwtutil.GetToken(l.svcCtx.Config.Auth.AccessSecret, tNow.Unix(), tExpire.Unix()+86400, mPayload)
    if err != nil {
        return nil, err
    }

    return &types.UserRefreshTokenResp{
        AccessToken:  accessToken,
        RefreshToken: refreshToken,
        Expires:      tExpire.Format("2006/01/02 15:04:05"),
    }, nil
}
  • user-api/internal/logic/userrouterlogic.go 路由common用户删除页面权限
func (l *UserRouterLogic) UserRouter(req *types.UserRouterReq) (resp []*types.RouterData, err error) {
    userID, err := helper.GetUserIDFromContext(l.ctx)
    if err != nil {
        return nil, err
    }

    if userID == 1 {
        return []*types.RouterData{
            {
                Path: "/permission/page/index",
                Name: "PermissionPage",
                Meta: types.Meta{
                    Title: "页面权限",
                    Roles: []string{"admin", "common"},
                },
            },
            {
                Path: "/permission/button",
                Meta: types.Meta{
                    Title: "按钮权限",
                    Roles: []string{"admin", "common"},
                },
                Children: []types.RouterData{
                    {
                        Path:      "/permission/button/router",
                        Component: "permission/button/index",
                        Name:      "PermissionButtonRouter",
                        Meta: types.Meta{
                            Title: "路由返回按钮权限",
                            Auths: []string{
                                "permission:btn:add",
                                "permission:btn:edit",
                                "permission:btn:delete",
                            },
                        },
                    },
                    {
                        Path:      "/permission/button/login",
                        Component: "permission/button/perms",
                        Name:      "PermissionButtonLogin",
                        Meta: types.Meta{
                            Title: "登录返回按钮权限",
                        },
                    },
                },
            },
        }, nil
    }

    return []*types.RouterData{
        {
            Path: "/permission/page/index",
            Name: "PermissionPage",
            Meta: types.Meta{
                Title: "页面权限",
                Roles: []string{"admin"},
            },
        },
        {
            Path: "/permission/button",
            Meta: types.Meta{
                Title: "按钮权限",
                Roles: []string{"admin", "common"},
            },
            Children: []types.RouterData{
                {
                    Path:      "/permission/button/router",
                    Component: "permission/button/index",
                    Name:      "PermissionButtonRouter",
                    Meta: types.Meta{
                        Title: "路由返回按钮权限",
                        Auths: []string{
                            "permission:btn:add",
                            "permission:btn:edit",
                            "permission:btn:delete",
                        },
                    },
                },
                {
                    Path:      "/permission/button/login",
                    Component: "permission/button/perms",
                    Name:      "PermissionButtonLogin",
                    Meta: types.Meta{
                        Title: "登录返回按钮权限",
                    },
                },
            },
        },
    }, nil
}

前端修改

  • src/views/login/index.vue 登陆采用表单密码
const onLogin = async (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  await formEl.validate((valid, fields) => {
    if (valid) {
      loading.value = true;
      useUserStoreHook()
        .loginByUsername({ username: ruleForm.username, password: ruleForm.password })
        .then(res => {
          if (res.code === 0) {
            // 获取后端路由
            return initRouter().then(() => {
              router.push(getTopMenu(true).path).then(() => {
                message("登录成功", { type: "success" });
              });
            });
          } else {
            message("登录失败", { type: "error" });
          }
        })
        .finally(() => (loading.value = false));
    }
  });
};
  • src/utils/http/index.ts 白名单加前缀 /api
/** 请求白名单,放置一些不需要`token`的接口(通过设置请求白名单,防止`token`过期后再请求造成的死循环问题) */
const whiteList = ["/api/refresh-token", "/api/login"];
  • refreshToken 传递jwt

  • src/api/user.ts

/** 刷新`token` */
export const refreshTokenApi = (refreshToken: string) => {
  return http.request<RefreshTokenResult>(
    "post",
    "/api/refresh-token",
    {},
    {
      headers: {
        Authorization: `Bearer ${refreshToken}`
      }
    }
  );
};
  • src/store/modules/user.ts
/** 刷新`token` */
async handRefreshToken(refreshToken) {
    return new Promise<RefreshTokenResult>((resolve, reject) => {
    refreshTokenApi(refreshToken)
        .then(data => {
        if (data) {
            setToken(data.data);
            resolve(data);
        }
        })
        .catch(error => {
        reject(error);
        });
    });
}
  • src/utils/http/index.ts
// token过期刷新
useUserStoreHook()
    .handRefreshToken(data.refreshToken)
    .then(res => {
    const token = res.data.accessToken;
    config.headers["Authorization"] = formatToken(token);
    PureHttp.requests.forEach(cb => cb(token));
    PureHttp.requests = [];
    })
    .finally(() => {
    PureHttp.isRefreshing = false;
    });

测试

  • 可以将 AccessExpire 配置修改为 10s, 登陆后刷新,观察是否调用 refreshToken 逻辑
  • 测试 common 用户,可观察期没有页眉权限

tag版本

purezeroadmin 项目下

git checkout v1.5.0

接下来

数据持久化

一步步实现微服务权限管理系统(5)

前言

  • 前端参考了很多框架,可谓百花齐放,但很多封装过剩,不利于学习和应用,最终我选择了 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin)
  • 后端我将使用 go-zero 来带领大家一步步实现一个权限管理系统
  • 本系列项目存放在 purezeroadmin 中,每一部分我都将打tag,并保证每个tag能正常运行。请多点赞和评论。
  • 后面示例均为 purezeroadmin 项目为例,你们可以根据需要自建工程来进行试验。均采用vscode进行试验。
  • go-zero 常用命令我将放入其对应的 makefile 文件中。

待解决

  1. 当前后端只修改了一个 userloginhandler.go,还有其他handler都需要修改,如果以后增加业务都要做相关的修改,那太麻烦了。
  2. base-api/base.api 不再需要了
  3. 前端修改对应格式

后端自定义 handler 模板

初始化模板

back-end操作

 goctl template init
Templates are generated in /root/.goctl/1.7.3, edit on your risk!
# 拷贝到该项目,保证项目与项目模板依赖独立 cp ~/.goctl/1.7.3/ template

修改handler 模板

vim template/api/handler.tpl

package {{.PkgName}}

import (
    "net/http"

    "github.com/zeromicro/go-zero/rest/httpx"
    {{.ImportPackages}}

    xhttp "github.com/zeromicro/x/http"
)

{{if .HasDoc}}{{.Doc}}{{end}}
func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        {{if .HasRequest}}var req types.{{.RequestType}}
        if err := httpx.Parse(r, &req); err != nil {
            xhttp.JsonBaseResponseCtx(r.Context(), w, err)
            return
        }

        {{end}}l := {{.LogicName}}.New{{.LogicType}}(r.Context(), svcCtx)
        {{if .HasResp}}resp, {{end}}err := l.{{.Call}}({{if .HasRequest}}&req{{end}})
        if err != nil {
            xhttp.JsonBaseResponseCtx(r.Context(), w, err)
        } else {
            {{if .HasResp}}xhttp.JsonBaseResponseCtx(r.Context(), w, resp){{else}}xhttp.JsonBaseResponseCtx(r.Context(), w, nil){{end}}
        }
    }
}

生成新handler

userrefreshtokenhandler.gouserrouterhandler.go 需要重新生成,先删除对应的文件,再执行命令

goctl api  go --api user-api/api/user.api --dir user-api --home template

删除base-api依赖

  • 删除base-api/base.api

  • user-api/api/user.api修改

type (
    UserLoginReq {
        UserName string `json:"username"`
        Password string `json:"password"`
    }
    UserLoginResp {
        Avatar       string   `json:"avatar"`
        Username     string   `json:"username"`
        Nickname     string   `json:"nickname"`
        Roles        []string `json:"roles"`
        Permissions  []string `json:"permissions"`
        AccessToken  string   `json:"accessToken"`
        RefreshToken string   `json:"refreshToken"`
        Expires      string   `json:"expires"`
    }
)

service user-api {
    @doc "用户登录"
    @handler userLogin
    post /api/login (UserLoginReq) returns (UserLoginResp)
}

type (
    UserRefreshTokenReq {
        RefreshToken string `json:"refreshToken"`
    }
    UserRefreshTokenResp {
        AccessToken  string `json:"accessToken"`
        RefreshToken string `json:"refreshToken"`
        Expires      string `json:"expires"`
    }
)

service user-api {
    @doc "刷新token"
    @handler UserRefreshToken
    post /api/refresh-token (UserRefreshTokenReq) returns (UserRefreshTokenResp)
}

type RouterData {
    Path      string       `json:"path"`
    Name      string       `json:"name,omitempty"`
    Component string       `json:"component,omitemty"`
    Meta      Meta         `json:"meta"`
    Children  []RouterData `json:"children,omitempty"`
}

type Meta {
    Title string   `json:"title"`
    Icon  string   `json:"icon,omitempty"`
    Rank  int64    `json:"rank,omitempty"`
    Roles []string `json:"roles,omitempty"`
    Auths []string `json:"auths,omitempty"`
}

type (
    UserRouterReq  {}
)

service user-api {
    @doc "获取路由"
    @handler userRouter
    get /api/get-async-routes (UserRouterReq) returns ([]RouterData)
}
  • 修改 userrefreshtokenlogic.gouserrouterlogic.go
// user-api/internal/logic/userrefreshtokenlogic.go
func (l *UserRefreshTokenLogic) UserRefreshToken(req *types.UserRefreshTokenReq) (resp *types.UserRefreshTokenResp, err error) {
    if req.RefreshToken != "" {
        return &types.UserRefreshTokenResp{
            AccessToken:  "eyJhbGciOiJIUzUxMiJ9.newAdmin",
            RefreshToken: "eyJhbGciOiJIUzUxMiJ9.newAdminRefresh",
            Expires:      "2030/10/30 23:59:59",
        }, nil
    }
    return nil, errors.New("refresh token error")
}

// user-api/internal/logic/userrouterlogic.go
func (l *UserRouterLogic) UserRouter(req *types.UserRouterReq) (resp []*types.RouterData, err error) {
    return []*types.RouterData{
        {
            Path: "/permission/page/index",
            Name: "PermissionPage",
            Meta: types.Meta{
                Title: "页面权限",
                Roles: []string{"admin", "common"},
            },
        },
        {
            Path: "/permission/button",
            Meta: types.Meta{
                Title: "按钮权限",
                Roles: []string{"admin", "common"},
            },
            Children: []types.RouterData{
                {
                    Path:      "/permission/button/router",
                    Component: "permission/button/index",
                    Name:      "PermissionButtonRouter",
                    Meta: types.Meta{
                        Title: "路由返回按钮权限",
                        Auths: []string{
                            "permission:btn:add",
                            "permission:btn:edit",
                            "permission:btn:delete",
                        },
                    },
                },
                {
                    Path:      "/permission/button/login",
                    Component: "permission/button/perms",
                    Name:      "PermissionButtonLogin",
                    Meta: types.Meta{
                        Title: "登录返回按钮权限",
                    },
                },
            },
        },
    }, nil
}

测试

 curl --request POST \
  --url http://localhost:8888/api/login\?debug \
  --header 'Content-Type: application/json' \
  --data '{
    "username": "admin",
    "password": "admin123"
}'
{"code":0,"msg":"ok","data":{"avatar":"https://avatars.githubusercontent.com/u/44761321","username":"admin","nickname":"小铭","roles":["admin"],"permissions":["*:*:*"],"accessToken":"eyJhbGciOiJIUzUxMiJ9.admin","refreshToken":"eyJhbGciOiJIUzUxMiJ9.adminRefresh","expires":"2030/10/30 00:00:00"}}# curl --request POST \
  --url http://localhost:8888/api/refresh-token \
  --header 'Content-Type: application/json' \
  --data '{
    "refreshToken": "xx"
}'
{"code":0,"msg":"ok","data":{"accessToken":"eyJhbGciOiJIUzUxMiJ9.newAdmin","refreshToken":"eyJhbGciOiJIUzUxMiJ9.newAdminRefresh","expires":"2030/10/30 23:59:59"}}# curl http://localhost:8888/api/get-async-routes
{"code":0,"msg":"ok","data":[{"path":"/permission/page/index","name":"PermissionPage","component":"","meta":{"title":"页面权限","roles":["admin","common"]}},{"path":"/permission/button","component":"","meta":{"title":"按钮权限","roles":["admin","common"]},"children":[{"path":"/permission/button/router","name":"PermissionButtonRouter","component":"permission/button/index","meta":{"title":"路由返回按钮权限","auths":["permission:btn:add","permission:btn:edit","permission:btn:delete"]}},{"path":"/permission/button/login","name":"PermissionButtonLogin","component":"permission/button/perms","meta":{"title":"登录返回按钮权限"}}]}]}# 

前端修改对应接口

  • src/api/routes.ts
type Result = {
  code: number;
  msg: string;
  data: Array<any>;
};
  • src/api/user.ts
export type UserResult = {
  code: number;
  msg: string;
  data: {
    /** 头像 */
    avatar: string;
    /** 用户名 */
    username: string;
    /** 昵称 */
    nickname: string;
    /** 当前登录用户的角色 */
    roles: Array<string>;
    /** 按钮级别权限 */
    permissions: Array<string>;
    /** `token` */
    accessToken: string;
    /** 用于调用刷新`accessToken`的接口时所需的`token` */
    refreshToken: string;
    /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */
    expires: Date;
  };
};

export type RefreshTokenResult = {
  code: number;
  msg: string;
  data: {
    /** `token` */
    accessToken: string;
    /** 用于调用刷新`accessToken`的接口时所需的`token` */
    refreshToken: string;
    /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */
    expires: Date;
  };
};
  • src/store/modules/user.ts
/** 登入 */
async loginByUsername(data) {
    return new Promise<UserResult>((resolve, reject) => {
    getLogin(data)
        .then(data => {
        if (data?.code === 0) setToken(data.data);
        resolve(data);
        })
        .catch(error => {
        reject(error);
        });
    });
},
  • src/views/login/index.vue
const onLogin = async (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  await formEl.validate((valid, fields) => {
    if (valid) {
      loading.value = true;
      useUserStoreHook()
        .loginByUsername({ username: ruleForm.username, password: "admin123" })
        .then(res => {
          if (res.code === 0) {
            // 获取后端路由
            return initRouter().then(() => {
              router.push(getTopMenu(true).path).then(() => {
                message("登录成功", { type: "success" });
              });
            });
          } else {
            message("登录失败", { type: "error" });
          }
        })
        .finally(() => (loading.value = false));
    }
  });
};
  • src/views/permission/page/index.vue
function onChange() {
  useUserStoreHook()
    .loginByUsername({ username: username.value, password: "admin123" })
    .then(res => {
      if (res.code) {
        storageLocal().removeItem("async-routes");
        usePermissionStoreHook().clearAllCachePage();
        initRouter();
      }
    });
}

测试

以上修改完毕,运行前后端测试通过

tag版本

purezeroadmin 项目下

git checkout v1.4.0

接下来

jwt认证

一步步实现微服务权限管理系统(4)

前言

  • 前端参考了很多框架,可谓百花齐放,但很多封装过剩,不利于学习和应用,最终我选择了 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin)
  • 后端我将使用 go-zero 来带领大家一步步实现一个权限管理系统
  • 本系列项目存放在 purezeroadmin 中,每一部分我都将打tag,并保证每个tag能正常运行。请多点赞和评论。
  • 后面示例均为 purezeroadmin 项目为例,你们可以根据需要自建工程来进行试验。均采用vscode进行试验。
  • go-zero 常用命令我将放入其对应的 makefile 文件中。

统一返回

  • 参考 https://go-zero.dev/docs/tutorials/customization/template
  • api返回一般采用以下返回
{
    "code": 0,
    "msg": "ok",
    "data": {}
}

修改后端登陆模块支持验证和返回错误

修改登陆逻辑

user-api/internal/logic/userloginlogic.go修改

func (l *UserLoginLogic) UserLogin(req *types.UserLoginReq) (resp *types.UserLoginResp, err error) {
    if req.UserName == "admin" && req.Password == "admin123" {
        return &types.UserLoginResp{
            Base: types.Base{
                Success: true,
            },
            Data: types.UserLoginData{
                Avatar:       "https://avatars.githubusercontent.com/u/44761321",
                Username:     "admin",
                Nickname:     "小铭",
                Roles:        []string{"admin"},
                Permissions:  []string{"*:*:*"},
                AccessToken:  "eyJhbGciOiJIUzUxMiJ9.admin",
                RefreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh",
                Expires:      "2030/10/30 00:00:00",
            },
        }, nil
    }

    if req.UserName == "common" && req.Password == "123456" {
        return &types.UserLoginResp{
            Base: types.Base{
                Success: true,
            },
            Data: types.UserLoginData{
                Avatar:       "https://avatars.githubusercontent.com/u/52823142",
                Username:     "common",
                Nickname:     "小林",
                Roles:        []string{"common"},
                Permissions:  []string{"permission:btn:add", "permission:btn:edit"},
                AccessToken:  "eyJhbGciOiJIUzUxMiJ9.common",
                RefreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh",
                Expires:      "2030/10/30 00:00:00",
            },
        }, nil
    }

    return nil, errors.New("登陆失败")
}

观测

curl 测试,返回不一致

 curl --request POST \
  --url http://localhost:8888/api/login \
  --header 'Content-Type: application/json' \
  --data '{
    "username": "admin",
    "password": "admin123"
}'
{"success":true,"data":{"avatar":"https://avatars.githubusercontent.com/u/44761321","username":"admin","nickname":"小铭","roles":["admin"],"permissions":["*:*:*"],"accessToken":"eyJhbGciOiJIUzUxMiJ9.admin","refreshToken":"eyJhbGciOiJIUzUxMiJ9.adminRefresh","expires":"2030/10/30 00:00:00"}}# 
curl --request POST \
  --url http://localhost:8888/api/login \
  --header 'Content-Type: application/json' \
  --data '{
    "username": "admin",
    "password": "admin1234"
}'
登陆失败

修改user-api/internal/handler/userloginhandler.go核心函数,支持统一返回

  • 修改
// 用户登录
func userLoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var req types.UserLoginReq
        if err := httpx.Parse(r, &req); err != nil {
            xhttp.JsonBaseResponseCtx(r.Context(), w, err)
            return
        }

        l := logic.NewUserLoginLogic(r.Context(), svcCtx)
        resp, err := l.UserLogin(&req)
        if err != nil {
            xhttp.JsonBaseResponseCtx(r.Context(), w, err)
        } else {
            xhttp.JsonBaseResponseCtx(r.Context(), w, resp)
        }
    }
}

完成后 go mod tidy 一下

  • 测试
 curl --request POST \
  --url http://localhost:8888/api/login\?debug \
  --header 'Content-Type: application/json' \
  --data '{
    "username": "admin",
    "password": "admin1234"
}'
{"code":-1,"msg":"登陆失败"}

tag版本

purezeroadmin 项目下

git checkout v1.3.0

接下来

  • 模型修改不采用前端 {success: true, data: xx } 模式
  • 统一前后端修改为 {code: 0, msg: "ok", data: xx} 模式

一步步实现微服务权限管理系统(3)

前言

  • 前端参考了很多框架,可谓百花齐放,但很多封装过剩,不利于学习和应用,最终我选择了 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin)
  • 后端我将使用 go-zero 来带领大家一步步实现一个权限管理系统
  • 本系列项目存放在 purezeroadmin 中,每一部分我都将打tag,并保证每个tag能正常运行。请多点赞和评论。
  • 后面示例均为 purezeroadmin 项目为例,你们可以根据需要自建工程来进行试验。均采用vscode进行试验。
  • go-zero 常用命令我将放入其对应的 makefile 文件中。

前端所有请求接口前缀加api

修改对应请求文件

  1. src/api/user.ts
/** 登录 */
export const getLogin = (data?: object) => {
  return http.request<UserResult>("post", "/api/login", { data });
};

/** 刷新`token` */
export const refreshTokenApi = (data?: object) => {
  return http.request<RefreshTokenResult>("post", "/api/refresh-token", { data });
};
  1. src/api/routes.ts
export const getAsyncRoutes = () => {
  return http.request<Result>("get", "/api/get-async-routes");
};

修改本地跨域代理

  • vite.config.ts
// 本地跨域代理 https://cn.vitejs.dev/config/server-options.html#server-proxy
proxy: {
'/api': {
    target: 'http://localhost:8888',
    changeOrigin: true,
}
},

后端接口前缀统一为/api

修改user-api/api/user.api相关信息

...
service user-api {
    @doc "用户登录"
    @handler userLogin
    post /api/login (UserLoginReq) returns (UserLoginResp)
}
...
service user-api {
    @doc "刷新token"
    @handler UserRefreshToken
    post /api/refresh-token (UserRefreshTokenReq) returns (UserRefreshTokenResp)
}
...
service user-api {
    @doc "获取路由"
    @handler userRouter
    get /api/get-async-routes (UserRouterReq) returns (UserRouterResp)
}

修改后重新生成代码

测试

完成以上可进行测试

tag版本

purezeroadmin 项目下

git checkout v1.2.0

接下来

现在有个问题,就是后端成功和失败返回的格式不统一,如用户登陆,成功返回 types.UserLoginResp, 而失败返回 error, 这是不规范的,下一节将解决相关问题