一年前写了个用户中心,基本设计在这里老的设计与实现,但只实现了诸如用户的 CURD、登录、注册和登出。根本就没有按 RBAC 来。今年填 qiankun 坑的时候顺便重构了用户中心,基于 RBAC 去实现。

前端仓库
后端仓库

回顾一下

\(\begin{matrix} 权限 \in 角色 \in 用户 \end{matrix}\)

用户角色的集合,角色又是权限的集合,
RBAC 就是维护这么一个关系

我们自下而上按 权限 -> 角色 -> 用户 来依次实现

权限

真实情况下权限、菜单非常的相似。可以说菜单继承自权限。所以我合在了一起实现:

权限 modal

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type Perm struct {
	ID       *string    `json:"id,omitempty" bson:"_id,omitempty"`                        // id
	Name     *string    `json:"name,omitempty" bson:"name,omitempty" validate:"required"` // 权限名
	CreateAt *time.Time `json:"createAt,omitempty" bson:"createAt,omitempty"`             // 创建时间
	UpdateAt *time.Time `json:"updateAt,omitempty" bson:"updateAt,omitempty"`             // 更新时间
	Order    *int       `json:"order,omitempty" bson:"order,omitempty"`                   // 排序

	// menu
	IsMenu     *bool   `json:"isMenu,omitempty" bson:"isMenu,omitempty" validate:"required"`          // 是否菜单
	IsHide     *bool   `json:"isHide,omitempty" bson:"isHide,omitempty"`                              // 是否不在菜单中显视
	IsMicroApp *bool   `json:"isMicroApp,omitempty" bson:"isMicroApp,omitempty"`                      // 是否微应用入口
	PID        *string `json:"pID,omitempty" bson:"pID,omitempty"`                                    // 父级id
	Url        *string `json:"url,omitempty" bson:"url,omitempty" validate:"required_if=IsMenu true"` // url
	Icon       *string `json:"icon,omitempty" bson:"icon,omitempty" `                                 // icon
}

我通过 pid 提供它们的树级关系。建树算法放在前端实现。
建树是 dfs+memo 实现,复杂度控制在O(N*logN)
其中genealogy是族谱,用于记录路径,
可以用于防止环状链表生成、溯源等。

建树算法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
export function _perm2Tree(
  origin: Perm[],
  pNode?: PermOpt
): { matched: PermOpt[]; mismatched: PermOpt[] } {
  let _matched: PermOpt[] = [],
    _mismatched: PermOpt[] = [];

  for (const p of origin) {
    let opt: PermOpt = {
      ...p,
      label: p.name,
      value: p.id,
      genealogy: (pNode?.genealogy ?? []).concat(p.id),
    };
    if (p.pID === pNode?.id) {
      _matched.push(opt);
    } else {
      _mismatched.push(opt);
    }
  }

  let solution: PermOpt[] = [];

  for (const match of _matched) {
    let { matched, mismatched } = _perm2Tree(_mismatched, match);
    solution.push({ ...match, children: matched });
    _mismatched = mismatched;
  }

  return {
    matched: solution,
    mismatched: _mismatched,
  };
}

genealogy 的使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const id = getFieldValue(["id"]),
  validOpt = dfsMap<Partial<PermOpt>>(
    { children: perm2Tree(perms?.data?.data) },
    "children",
    (t) => {
      const ouroboros = t?.genealogy?.includes(id); // 衔尾蛇
      return {
        ...t,
        disabled: ouroboros,
        name: (
          <Tooltip title={ouroboros ? "不能选子孙节点" : t.url}>
            {t.name}
          </Tooltip>
        ),
      };
    }
  ).children;

角色

1
2
3
4
5
6
7
8

type Role struct {
	ID       *string    `json:"id,omitempty" bson:"_id,omitempty"`                        // id
	Name     *string    `json:"name,omitempty" bson:"name,omitempty" validtor:"required"` // 角色名
	CreateAt *time.Time `json:"createAt,omitempty" bson:"createAt,omitempty"`             // 创建时间
	UpdateAt *time.Time `json:"updateAt,omitempty" bson:"updateAt,omitempty"`             // 更新时间
	Perms    []*string  `json:"perms,omitempty" bson:"perms,omitempty"`                   // 权限列表
}

用户

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type User struct {
	ID       *primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"`                                // id
	Name     *string             `json:"name,omitempty" bson:"name,omitempty" validate:"required"`         // 用户昵称
	Pwd      *string             `json:"pwd,omitempty" bson:"pwd,omitempty" validate:"required,min=8"`     // 密码
	Email    *string             `json:"email,omitempty" bson:"email,omitempty" validate:"required,email"` // 邮箱
	CreateAt *time.Time          `json:"createAt,omitempty" bson:"createAt,omitempty"`                     // 创建时间
	UpdateAt *time.Time          `json:"updateAt,omitempty" bson:"updateAt,omitempty"`                     // 更新时间
	Roles    []string            `json:"roles,omitempty" bson:"roles,omitempty"`                           // 角色列表
}

可以看到 RBAC 1.0 的还是偏于简单。 稍微花点时间就是菜单的树状结构管理上。 用户和角色基本就是 CURD。

最终用户中心还要支持 JWT 的签发和权限的校验

JWT

JWT 网上一搜一大把。推荐文章 我这里不做阅读理解了。
我分享一下我基于我那渣渣 VPS 作了小小的优化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (app *App) cacheSign(w http.ResponseWriter, uid string) {
	dur := time.Hour * 24 * 15
	exp := time.Now().Add(dur).Unix()
	tk, err := app.auth.GenJWT(&jwt.StandardClaims{
		ExpiresAt: exp,
		Issuer:    "fuRan",
		Audience:  uid,
	})

	if err != nil {
		responser.RetFail(w, err)
		return
	}

	sign := strings.Split(tk, ".")[2]
    /**
    / 我这里只截取signature,然后存到redis里。其它server直接读redis 没读到说明 登陆过期,不需要每次都解密jwt;可以省下不少CPU资源。
    / 另外其它server需求用JWT换UID,所以redis是已 signature -> uid 的形式存储。过期时间和jwt过期时间一致。解决实效性问题。
    **/
	cmd := app.rdb.SetEX(context.Background(), sign, uid, dur)

	if cmd.Err() != nil {
		responser.RetFail(w, cmd.Err())
		return
	}

	responser.RetOk(w, sign)
}

权限校验

我的 RPC 就是简单的 http 的 head 请求。 联表查出所有权限,找到返回 200 否则 403 简单粗暴。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// perm rpc
func (app *App) CheckPermRPC(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	p, err := app.getSetPerm(r.Header.Get("uid"))

	if err != nil {
		w.WriteHeader(http.StatusForbidden)
		return
	}

	for _, role := range p {
		if role == ps.ByName("perm") {
			return
		}
	}

	w.WriteHeader(http.StatusForbidden)
}

当然中间件也加了 redis 缓存

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
func (app *App) CheckPerm(perm string) func(httprouter.Handle) httprouter.Handle {
	return func(next httprouter.Handle) httprouter.Handle {
		//权限验证
		return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
			u := r.Header.Get("uid")
			k := u + ":perm"

			const (
				NONEXIST = 0
				EXIST    = 1
			)

			e, _ := app.rdb.Exists(context.Background(), k).Result()

			if e == EXIST {
				if b, _ := app.rdb.SIsMember(context.Background(), k, perm).Result(); b {
					next(w, r, ps)
					return
				}
				w.WriteHeader(http.StatusForbidden)
				return
			}

			if e == NONEXIST {
				p, err := app.getSetPerm(u)

				if err != nil {
					w.WriteHeader(http.StatusForbidden)
					return
				}

				for _, role := range p {
					if role == perm {
						next(w, r, ps)
						return
					}
				}

				w.WriteHeader(http.StatusForbidden)
			}
		}
	}
}

以上