跳到主要内容

认证与权限

企智通的安全模型围绕**「你是谁」(认证)「你能做什么」(授权)两层展开。认证基于 JWT 双令牌机制,授权基于 Casbin RBAC,并叠加数据权限范围**控制。本文详细介绍这套机制。

整体模型概览

┌─────────────────────────────────────────────────────────┐
│ 认证 (Authentication) │
│ "你是谁" —— 通过账号密码 / 扫码证明身份 │
│ │
│ 登录成功 → 签发 access_token + refresh_token (JWT) │
└────────────────────────────┬────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 授权 (Authorization) │
│ "你能做什么" —— 三层校验 │
│ │
│ 1. API 访问权限 (Casbin RBAC: 角色 → 路径 → 方法) │
│ 2. 菜单/按钮权限 (权限码: crm:customer:create) │
│ 3. 数据范围权限 (data_scope: 全部/本部门/仅本人) │
└─────────────────────────────────────────────────────────┘

一、认证:JWT 双令牌

企智通使用 JWT(JSON Web Token) 作为无状态认证凭证。为兼顾安全用户体验,采用双令牌机制。

双令牌设计

令牌作用有效期存储位置
access_token访问业务 API 的凭证短(如 30 分钟)前端内存 / sessionStorage
refresh_token用于换取新的 access_token长(如 7 天)前端 localStorage(加密)

为什么用双令牌

  • access_token 短效:即使泄露,攻击窗口很小(30 分钟内)
  • refresh_token 长效:用户无需频繁重新登录,体验好
  • 可吊销:refresh_token 存储在服务端(Redis),可主动失效(改密、踢下线、注销)

登录流程

用户提交账号 + 密码


后端校验:
· 用户名是否存在
· 密码是否正确 (bcrypt 比对)
· 账号是否锁定 / 停用


签发 access_token (payload: user_id, roles, exp)
签发 refresh_token (payload: user_id, exp)
│ refresh_token 同时写入 Redis (key: user_id, 可踢出)

返回前端 { accessToken, refreshToken, userInfo, permissions }

令牌刷新流程

前端发起请求 (携带 access_token)


后端校验 access_token:
· 过期? → 返回 401 (token expired)
· 有效? → 放行


前端拦截器收到 401 (expired)


前端用 refresh_token 调用 POST /api/v1/auth/refresh


后端校验 refresh_token:
· 是否过期?
· 是否在 Redis 中 (未被吊销)?


签发新的 access_token (+ 可能轮换 refresh_token)


前端用新 token 重放原请求 (用户无感知)

若 refresh_token 也过期,则前端跳转登录页。

密码安全

  • 密码使用 bcrypt 哈希存储(自带盐,抗彩虹表)
  • 密码强度策略:最小长度、大小写 + 数字 + 符号组合
  • 登录失败锁定:连续失败 N 次锁定账号 M 分钟
  • 初始密码 / 重置密码后强制首次登录修改

第三方登录

企智通支持企业微信扫码登录(OAuth2),可作为账号密码登录的补充:

  • 配置企业 CorpID、AgentID、Secret、回调地址
  • 用户扫码后,后端用企业微信 code 换取用户身份
  • 将企业微信 userid 与系统 user 绑定(首次扫码自动创建或关联已有账号)
  • 绑定后,扫码即等价于登录,同样签发 JWT 双令牌

二、授权:Casbin RBAC

认证解决「你是谁」,授权解决「你能做什么」。企智通的授权模型基于 CasbinRBAC(基于角色的访问控制)。

权限四要素

用户(user) ──属于──▶ 角色(role) ──拥有──▶ 菜单(menu) ──绑定──▶ 权限码(permission) + API
概念说明示例
用户系统账号张三(user_id=10)
角色权限的集合,用户与权限的中介销售经理、财务、HR、超管
菜单前端导航项,关联一个页面与若干按钮权限码「客户管理」菜单
权限码细粒度动作标识,按钮级控制crm:customer:createcrm:customer:export
API后端路由(路径 + 方法),菜单注册时声明POST /api/v1/crm/customers

角色 → 菜单 → API → 权限码 的映射

  1. 管理员在后台配置角色:勾选该角色能访问哪些菜单
  2. 菜单注册时声明其包含的 API 与权限码(由模块 RegisterMenus() 定义)
  3. 用户被赋予一个或多个角色:用户的有效权限 = 所有角色权限的并集

API 级校验(Casbin)

每个请求经过 CasbinRBAC 中间件时:

输入: (当前用户的角色集合, 请求路径, HTTP方法)


Casbin 策略匹配:
· 角色 → 菜单 → API 的关联是否包含当前 (路径, 方法)?


匹配成功 → 放行
匹配失败 → 403 Forbidden

Casbin 的策略存储在 sys_menus(菜单与其绑定的 API),并同步加载到内存中的 enforcer,匹配是 O(1) 级别的高效查表。

菜单与按钮级权限(前端)

前端拿到当前用户的权限码集合后:

  • 菜单渲染:只渲染用户有权限的菜单项(动态路由)
  • 按钮控制:用 <Access code="crm:customer:create"> 包裹按钮,无权限码则不渲染
<Access code="crm:customer:create">
<Button onClick={handleCreate}>新增客户</Button>
</Access>

注意:前端权限控制只是体验优化(不显示无权限的按钮),真正的安全防线在后端 Casbin。即使绕过前端直接调 API,后端也会 403。

三、数据权限(data_scope)

API 权限控制「能不能调用这个接口」,数据权限控制「调用时能看到哪些数据」。同样的「客户列表」接口,销售员只能看自己的客户,销售总监能看整个部门的客户。

数据范围(data_scope)

每个角色配置一个 data_scope,取值:

data_scope含义看到的数据
全部全部数据所有客户 / 订单
本部门仅本部门创建人部门 = 当前用户部门
本部门及子部门本部门 + 下级部门创建人部门 ∈ 当前用户部门及子部门
仅本人仅自己创建创建人 = 当前用户
自定义指定部门集合创建人部门 ∈ 配置的部门列表

实现机制

数据权限在 Service 层注入到查询条件中:

// 伪代码
func (s *CustomerService) List(ctx context.Context, query Query) {
scope := s.getDataScope(ctx.CurrentUser())
// scope 自动追加 WHERE 条件
// 本部门: WHERE owner_dept_id = ?
// 本部门及子: WHERE owner_dept_id IN (?)
// 仅本人: WHERE owner_id = ?
db.Model(&Customer{}).Where(scope).Find(&list)
}

数据权限是横向隔离,与 API 权限(纵向能否访问)正交,两者叠加构成完整的授权。

四、超级管理员旁路

系统预设一个超级管理员角色(通常是 user_id=1 或标记 is_super=1),拥有全部权限跳过所有校验

  • Casbin 校验:检测到超管直接放行,不查策略
  • 数据权限:超管 data_scope 强制为「全部」
  • 操作日志:照常记录(超管的操作更需要审计)
  • 受限操作:数据重置等高危功能仅超管可见

超管旁路保证了系统永远有一个可用的最高权限账号,避免因权限配置错误导致系统无法管理。但生产环境应严格控制超管账号数量与使用。

安全最佳实践

场景建议
生产部署立即修改默认超管密码,禁用 demo 账号
令牌存储access_token 放内存,避免 localStorage(防 XSS)
HTTPS生产环境强制 HTTPS,防止令牌被中间人窃取
操作审计关键操作开启操作日志,定期巡检
最小权限按岗位精准配置角色,避免赋予「全部数据」范围
定期巡检检查异常登录、权限变更、超管使用记录

扩展阅读