Consul 源码剖析 - ACL 权限控制模块
1. 模块概览
1.1 职责定义
ACL (Access Control List) 模块是 Consul 的权限控制核心,负责管理和实施基于策略的访问控制。该模块提供细粒度的权限管理,确保只有经过授权的实体才能访问 Consul 的各种资源(服务、节点、KV、配置等)。
核心职责:
- Token 生命周期管理(创建、更新、删除、查询)
- Policy 策略定义与管理
- Role 角色管理(将多个 Policy 组合)
- BindingRule 绑定规则(Auth Method 与 Role 的关联)
- Auth Method 认证方法(支持 Kubernetes、JWT、OIDC等)
- 权限解析与强制执行(Resolver 和 Authorizer)
- Token 复制(跨数据中心)
- Bootstrap 初始化
1.2 输入与输出
输入:
- HTTP/RPC 请求(携带 ACL Token)
- 策略定义(HCL/JSON 格式)
- 认证凭证(JWT、OIDC Token、Kubernetes Service Account)
输出:
- ACL Token(SecretID 和 AccessorID)
- 授权决策(Allow/Deny/Default)
- 策略评估结果
- 错误信息(权限不足、Token 无效等)
1.3 上下游依赖
上游调用方:
- HTTP API 层(agent/http/acl.go)
- 所有 RPC 端点(每个请求前都需要 ACL 校验)
- gRPC 服务(外部 peering、resource service)
下游依赖:
- State Store(存储 Token、Policy、Role等)
- Raft(写操作通过共识)
- Replication(跨数据中心复制)
- Auth Method 验证器(JWT、Kubernetes等)
1.4 生命周期
初始化阶段:
- Server 启动时检查 ACL 配置(
ACLsEnabled) - 如未 Bootstrap,等待管理员执行 Bootstrap 操作
- 初始化 ACL Resolver(缓存 Token 和 Policy)
- 启动 Token 复制任务(非主数据中心)
运行时阶段:
- 接收 Token/Policy/Role 管理请求
- 验证请求者权限
- 通过 Raft Apply 写入状态
- 更新 Resolver 缓存
- 复制到其他数据中心(如适用)
关闭阶段:
- 停止 Token 复制任务
- 清理缓存资源
2. 模块架构图
flowchart TB
subgraph "HTTP/RPC 层"
API[HTTP API<br>/v1/acl/*]
RPC[RPC ACL Endpoint]
end
subgraph "ACL 核心模块"
direction TB
Bootstrap[Bootstrap 初始化]
TokenMgr[Token 管理器]
PolicyMgr[Policy 管理器]
RoleMgr[Role 管理器]
BindingMgr[BindingRule 管理器]
AuthMethodMgr[AuthMethod 管理器]
Login[Login/Logout 登录]
Resolver[ACL Resolver<br>Token+Policy缓存]
Authorizer[Authorizer<br>权限决策引擎]
end
subgraph "权限执行点"
CatalogAuth[Catalog 权限检查]
KVAuth[KV 权限检查]
ServiceAuth[Service 权限检查]
NodeAuth[Node 权限检查]
OtherAuth[其他模块检查]
end
subgraph "数据存储"
FSM[FSM 状态机]
State[State Store<br>Memdb]
Raft[Raft 共识]
end
subgraph "跨DC复制"
ReplicatorToken[Token Replicator]
ReplicatorPolicy[Policy Replicator]
end
API --> RPC
RPC --> Bootstrap
RPC --> TokenMgr
RPC --> PolicyMgr
RPC --> RoleMgr
RPC --> BindingMgr
RPC --> AuthMethodMgr
RPC --> Login
TokenMgr --> Resolver
PolicyMgr --> Resolver
RoleMgr --> Resolver
Login --> AuthMethodMgr
Login --> TokenMgr
Resolver --> Authorizer
CatalogAuth --> Authorizer
KVAuth --> Authorizer
ServiceAuth --> Authorizer
NodeAuth --> Authorizer
OtherAuth --> Authorizer
TokenMgr --> FSM
PolicyMgr --> FSM
RoleMgr --> FSM
BindingMgr --> FSM
AuthMethodMgr --> FSM
FSM --> Raft
Raft --> State
State --> ReplicatorToken
State --> ReplicatorPolicy
ReplicatorToken --> RPC
ReplicatorPolicy --> RPC
2.1 架构说明
组件职责:
-
Bootstrap 初始化:
- 作用:首次部署时创建初始管理 Token
- 边界:只能执行一次,除非通过 Reset Index 重置
- 状态:无状态,操作记录在 State Store 的 Bootstrap Index
-
Token 管理器:
- 作用:Token 的 CRUD 操作,包括 Clone(复制)
- 边界:Token 可以是 Local(本地数据中心)或 Global(全局复制)
- 状态:Token 存储在 State Store,Resolver 缓存 Token 及其关联的 Policy
-
Policy 管理器:
- 作用:管理 ACL 策略(HCL 规则)
- 边界:Policy 始终是全局的,通过 Raft 复制
- 状态:Policy 存储在 State Store,解析后的策略缓存在 Resolver
-
Role 管理器:
- 作用:角色是 Policy 和 ServiceIdentity 的集合
- 边界:Role 全局复制,可关联多个 Policy
- 状态:Role 存储在 State Store
-
ACL Resolver:
- 作用:缓存 Token 到 Authorizer 的映射,避免重复解析
- 边界:本地缓存,TTL 默认 30 秒
- 状态:内存缓存(LRU),缓存未命中时从 State Store 加载
-
Authorizer:
- 作用:执行权限判定(Allow/Deny/Default)
- 边界:无状态,基于 Policy 规则树进行决策
- 状态:Policy 规则预编译为决策树
关键决策点:
- Token 类型选择:创建 Token 时决定是 Local(仅本 DC)还是 Global(跨 DC 复制)
- Policy 合并:Token 可关联多个 Policy/Role,Authorizer 按优先级合并(Deny > Allow > Default)
- 缓存策略:Resolver 使用 TTL 缓存,过期后重新加载;写操作触发缓存失效
- 复制策略:Global Token 和所有 Policy/Role 通过后台任务从主 DC 复制到从 DC
边界条件:
- 并发写入:Token/Policy 使用 CAS(Compare-And-Swap)防止并发冲突
- 超时:ACL Resolver 解析超时 5 秒,避免阻塞业务请求
- 幂等性:相同 AccessorID 的 Token 更新是幂等的(覆盖)
- 顺序性:复制任务保证顺序性(按 Raft Index 递增)
异常处理:
- Token 不存在:返回 Anonymous Token(AccessorID = 00000000-0000-0000-0000-000000000002)
- Token 过期:Resolver 自动剔除,后续请求返回权限不足
- Raft 写入失败:返回错误,客户端可重试
- 复制延迟:从 DC 可能读到旧 Policy,通过 Blocking Query 等待更新
性能要点:
- Resolver 缓存命中率:生产环境通常 > 95%,减少 State Store 查询
- Policy 编译:策略首次加载时编译为规则树,后续判定时间复杂度 O(log n)
- 批量操作:复制任务批量 Apply(最多 100 条/批次),降低 Raft 开销
- 索引优化:State Store 按 AccessorID、SecretID、Policy ID 建立索引
容量上界:
- 单个 Token 关联 Policy 数量:建议 < 10 个(影响解析性能)
- 单个 DC 的 Token 总数:< 100,000(受限于 State Store 内存)
- Policy 规则数量:单个 Policy < 1000 条规则(影响编译时间)
3. 关键数据结构与 UML 图
3.1 核心数据结构类图
classDiagram
class ACLToken {
+string AccessorID
+string SecretID
+string Description
+[]ACLTokenPolicyLink Policies
+[]ACLTokenRoleLink Roles
+[]ACLServiceIdentity ServiceIdentities
+[]ACLNodeIdentity NodeIdentities
+bool Local
+time.Time CreateTime
+time.Time ExpirationTime
+string AuthMethod
+Hash() []byte
}
class ACLPolicy {
+string ID
+string Name
+string Description
+string Rules
+string Datacenters
+Hash() []byte
}
class ACLRole {
+string ID
+string Name
+string Description
+[]ACLRolePolicyLink Policies
+[]ACLServiceIdentity ServiceIdentities
+[]ACLNodeIdentity NodeIdentities
+Hash() []byte
}
class ACLBindingRule {
+string ID
+string Description
+string AuthMethod
+string Selector
+string BindType
+string BindName
}
class ACLAuthMethod {
+string Name
+string Type
+string Description
+string DisplayName
+int MaxTokenTTL
+map Config
}
class Authorizer {
<<interface>>
+ACLRead(ctx) EnforcementDecision
+ACLWrite(ctx) EnforcementDecision
+ServiceRead(name, ctx) EnforcementDecision
+ServiceWrite(name, ctx) EnforcementDecision
+NodeRead(name, ctx) EnforcementDecision
+NodeWrite(name, ctx) EnforcementDecision
+KeyRead(key, ctx) EnforcementDecision
+KeyWrite(key, ctx) EnforcementDecision
}
class PolicyAuthorizer {
+Policy defaultPolicy
+[]Policy policies
+AgentRead(name, ctx)
+AgentWrite(name, ctx)
+ServiceRead(name, ctx)
+ServiceWrite(name, ctx)
+NodeRead(name, ctx)
+NodeWrite(name, ctx)
+KeyRead(key, ctx)
+KeyWrite(key, ctx)
}
class ACLResolver {
+resolveToken(secretID) Authorizer
+resolvePolicies(policyIDs) []Policy
+cache *lru.TwoQueueCache
+fetchFromBackend(id)
}
ACLToken --> ACLPolicy : policies
ACLToken --> ACLRole : roles
ACLRole --> ACLPolicy : policies
ACLBindingRule --> ACLAuthMethod : references
ACLResolver --> ACLToken : resolves
ACLResolver --> Authorizer : returns
PolicyAuthorizer ..|> Authorizer : implements
ACLToken --> PolicyAuthorizer : compiles to
3.2 数据结构详细说明
3.2.1 ACLToken(令牌)
字段说明:
| 字段 | 类型 | 必填 | 说明 | 约束 |
|---|---|---|---|---|
| AccessorID | string | 是 | 令牌访问标识符(UUID) | 唯一,用于查询和显示,不可用于鉴权 |
| SecretID | string | 是 | 令牌密钥(UUID) | 唯一,用于鉴权,需保密 |
| Description | string | 否 | 令牌描述 | 最大 256 字符 |
| Policies | []ACLTokenPolicyLink | 否 | 关联的策略列表 | 最多 10 个推荐 |
| Roles | []ACLTokenRoleLink | 否 | 关联的角色列表 | 最多 10 个推荐 |
| ServiceIdentities | []ACLServiceIdentity | 否 | 服务身份列表 | 自动生成 Service 读写权限 |
| NodeIdentities | []ACLNodeIdentity | 否 | 节点身份列表 | 自动生成 Node 读写权限 |
| Local | bool | 否 | 是否本地令牌 | true=仅本DC,false=全局复制 |
| CreateTime | time.Time | 是 | 创建时间 | 自动生成 |
| ExpirationTime | time.Time | 否 | 过期时间 | 可选,过期后自动失效 |
| AuthMethod | string | 否 | 认证方法名称 | 通过 Login 创建时自动设置 |
版本演进:
- v1.4.0 引入 Role 和 ServiceIdentity
- v1.8.0 引入 NodeIdentity
- v1.10.0 引入 ExpirationTime(TTL)
序列化:JSON/MessagePack,SecretID 在日志中自动脱敏
3.2.2 ACLPolicy(策略)
字段说明:
| 字段 | 类型 | 必填 | 说明 | 约束 |
|---|---|---|---|---|
| ID | string | 是 | 策略唯一标识符(UUID) | 自动生成 |
| Name | string | 是 | 策略名称 | 唯一,1-128 字符,仅字母数字和 -_ |
| Description | string | 否 | 策略描述 | 最大 256 字符 |
| Rules | string | 是 | 策略规则(HCL 格式) | 参见策略语法 |
| Datacenters | []string | 否 | 适用数据中心列表 | 空表示全部 DC |
策略规则示例:
# 允许读取所有服务
service_prefix "" {
policy = "read"
}
# 允许写入 web 服务
service "web" {
policy = "write"
intentions = "write"
}
# 允许读取 KV 前缀
key_prefix "config/" {
policy = "read"
}
# 拒绝删除 KV
key_prefix "config/production/" {
policy = "deny"
}
# 允许节点注册
node_prefix "" {
policy = "write"
}
规则合并逻辑:
- 同一资源多个规则按顺序评估
deny>write>read>list- 前缀匹配:最长匹配优先
3.2.3 ACLRole(角色)
字段说明:
| 字段 | 类型 | 必填 | 说明 | 约束 |
|---|---|---|---|---|
| ID | string | 是 | 角色唯一标识符(UUID) | 自动生成 |
| Name | string | 是 | 角色名称 | 唯一,1-128 字符 |
| Description | string | 否 | 角色描述 | 最大 256 字符 |
| Policies | []ACLRolePolicyLink | 否 | 关联的策略列表 | 通过 Policy ID 或 Name 引用 |
| ServiceIdentities | []ACLServiceIdentity | 否 | 服务身份列表 | 简化服务权限配置 |
| NodeIdentities | []ACLNodeIdentity | 否 | 节点身份列表 | 简化节点权限配置 |
使用场景:
- 将多个 Policy 组合为可复用的角色(如
developer、operator) - 简化 Token 配置,一个 Token 关联一个 Role 即可获得所有权限
3.2.4 Authorizer 接口(权限判定)
核心方法:
type Authorizer interface {
// ACL 管理权限
ACLRead(*AuthorizerContext) EnforcementDecision
ACLWrite(*AuthorizerContext) EnforcementDecision
// 服务权限
ServiceRead(name string, ctx *AuthorizerContext) EnforcementDecision
ServiceWrite(name string, ctx *AuthorizerContext) EnforcementDecision
// 节点权限
NodeRead(name string, ctx *AuthorizerContext) EnforcementDecision
NodeWrite(name string, ctx *AuthorizerContext) EnforcementDecision
// KV 权限
KeyRead(key string, ctx *AuthorizerContext) EnforcementDecision
KeyWrite(key string, ctx *AuthorizerContext) EnforcementDecision
KeyList(prefix string, ctx *AuthorizerContext) EnforcementDecision
// 其他权限(省略详细列表)
EventRead(name string, ctx *AuthorizerContext) EnforcementDecision
IntentionRead(name string, ctx *AuthorizerContext) EnforcementDecision
SessionRead(node string, ctx *AuthorizerContext) EnforcementDecision
PreparedQueryRead(name string, ctx *AuthorizerContext) EnforcementDecision
// ...
}
type EnforcementDecision int
const (
Deny EnforcementDecision = iota // 明确拒绝
Allow // 明确允许
Default // 无明确规则,使用默认策略
)
决策流程:
- 遍历 Token 关联的所有 Policy
- 对目标资源按前缀匹配规则
- 合并多个策略结果(Deny 优先)
- 返回最终决策
4. 核心 API 详细规格
4.1 BootstrapTokens - ACL 初始化
4.1.1 基本信息
- 名称:
ACL.BootstrapTokens - 协议:RPC 方法
- HTTP 映射:
PUT /v1/acl/bootstrap - 幂等性:否(仅可执行一次,除非使用 Reset Index)
4.1.2 请求结构体
type ACLInitialTokenBootstrapRequest struct {
BootstrapSecret string // 可选的预定义 SecretID
Datacenter string // 目标数据中心
QueryOptions // 查询选项(Token、Consistency等)
}
字段表:
| 字段 | 类型 | 必填 | 默认 | 约束 | 说明 |
|---|---|---|---|---|---|
| BootstrapSecret | string | 否 | 自动生成UUID | 36字符UUID格式 | 指定初始 Token 的 SecretID |
| Datacenter | string | 否 | 本地DC | - | 必须是主数据中心 |
4.1.3 响应结构体
type ACLToken struct {
AccessorID string
SecretID string
Description string
Policies []ACLTokenPolicyLink
Local bool
CreateTime time.Time
// 其他字段见数据结构章节
}
字段表:
| 字段 | 类型 | 说明 |
|---|---|---|
| AccessorID | string | 生成的 Token AccessorID(UUID) |
| SecretID | string | 生成的 Token SecretID(客户端用于鉴权) |
| Description | string | “Bootstrap Token (Global Management)” |
| Policies | []ACLTokenPolicyLink | 包含 global-management 策略 |
| Local | bool | false(全局 Token) |
4.1.4 入口函数与核心代码
func (a *ACL) BootstrapTokens(
args *structs.ACLInitialTokenBootstrapRequest,
reply *structs.ACLToken,
) error {
// 1. 前置校验
if !a.srv.config.ACLsEnabled {
return acl.ErrDisabled
}
// 2. 转发到 Leader(如非 Leader)
if done, err := a.srv.ForwardRPC("ACL.BootstrapTokens", args, reply); done {
return err
}
// 3. 验证是否可以 Bootstrap
state := a.srv.fsm.State()
allowed, resetIdx, err := state.CanBootstrapACLToken()
if !allowed {
// 检查是否有 Reset Index 文件
specifiedIndex := a.fileBootstrapResetIndex()
if specifiedIndex != resetIdx {
return fmt.Errorf("ACL bootstrap no longer allowed (reset index: %d)", resetIdx)
}
}
// 4. 生成 Token
accessor, _ := lib.GenerateUUID(a.srv.checkTokenUUID)
secret := args.BootstrapSecret
if secret == "" {
secret, _ = lib.GenerateUUID(a.srv.checkTokenUUID)
}
token := &structs.ACLToken{
AccessorID: accessor,
SecretID: secret,
Description: "Bootstrap Token (Global Management)",
Policies: []structs.ACLTokenPolicyLink{
{ID: structs.ACLPolicyGlobalManagementID}, // builtin/global-management
},
CreateTime: time.Now(),
Local: false,
}
// 5. 通过 Raft Apply 持久化
req := &structs.ACLTokenBootstrapRequest{
Token: *token,
ResetIndex: resetIdx,
}
resp, err := a.srv.raftApply(structs.ACLBootstrapRequestType, req)
if err != nil {
return err
}
// 6. 删除 Reset Index 文件(防止重复 Bootstrap)
a.removeBootstrapResetFile()
*reply = *token
return nil
}
代码逻辑解释:
- 步骤 1-2:校验 ACL 是否启用,非 Leader 转发到 Leader
- 步骤 3:检查 State Store 的
BootstrapIndex,已 Bootstrap 则检查 Reset Index 文件 - 步骤 4:生成 Token,关联内置的
global-management策略(具有所有权限) - 步骤 5:通过 Raft 提交,FSM Apply 时更新
BootstrapIndex防止重复执行 - 步骤 6:删除 Reset Index 文件,防止快照恢复后意外重新 Bootstrap
4.1.5 调用链路
HTTP PUT /v1/acl/bootstrap
↓
agent/http/acl.go: aclBootstrapHandler()
↓
agent/rpc.go: RPC("ACL.BootstrapTokens", ...)
↓
agent/consul/acl_endpoint.go: ACL.BootstrapTokens()
↓
agent/consul/server.go: raftApply(ACLBootstrapRequestType, ...)
↓
agent/consul/fsm/fsm.go: FSM.Apply()
↓
agent/consul/fsm/acl.go: applyACLTokenBootstrap()
↓
agent/consul/state/acl.go: ACLBootstrap()
↓
State Store 更新 BootstrapIndex 和 Token 表
4.1.6 时序图
sequenceDiagram
autonumber
participant Client as 管理员
participant HTTP as HTTP API
participant RPC as ACL RPC Endpoint
participant Leader as Raft Leader
participant FSM as FSM 状态机
participant State as State Store
Note over Client,State: Bootstrap 初始化流程
Client->>HTTP: PUT /v1/acl/bootstrap
HTTP->>RPC: ACL.BootstrapTokens()
alt 非 Leader
RPC->>Leader: 转发到 Leader
end
RPC->>State: CanBootstrapACLToken()
State-->>RPC: allowed=true/false, resetIdx
alt Bootstrap 已执行
RPC->>RPC: 检查 Reset Index 文件
alt Reset Index 不匹配
RPC-->>Client: 错误:Bootstrap 不再允许
end
end
RPC->>RPC: 生成 AccessorID 和 SecretID
RPC->>RPC: 构造 Token(关联 global-management 策略)
RPC->>Leader: raftApply(ACLBootstrapRequestType, token)
Leader->>FSM: Apply Raft Log
FSM->>State: ACLBootstrap(token)
State->>State: 更新 BootstrapIndex<br>插入 Token 到 tokens 表
State-->>FSM: 成功
FSM-->>Leader: 成功
Leader-->>RPC: 成功
RPC->>RPC: 删除 Reset Index 文件
RPC-->>HTTP: 返回 Token
HTTP-->>Client: 200 OK + Token JSON
Note over Client: 使用 SecretID 进行后续操作
4.1.7 边界与异常
边界条件:
- 首次执行:State Store 的
BootstrapIndex为 0,允许 Bootstrap - 重复执行:
BootstrapIndex> 0,拒绝请求,返回当前 Index 用于 Reset - Reset Index:管理员在
<data_dir>/acl-bootstrap-reset文件中写入 Reset Index,允许再次 Bootstrap - 多 DC:只能在主数据中心执行,从数据中心通过复制获取 Token
异常处理:
- ACL 未启用:返回
acl.ErrDisabled - 非主 DC:返回
acl.ErrDisabled - 并发 Bootstrap:Raft 串行化,只有一个请求成功,其他返回错误
- Reset Index 不匹配:返回错误,显示当前正确的 Reset Index
实践建议:
- Bootstrap 后立即保存 SecretID(仅显示一次)
- 使用 Bootstrap Token 创建更细粒度的管理员 Token
- 定期轮换 Bootstrap Token(创建新管理员 Token 后删除旧的)
- 在配置管理工具中预设
BootstrapSecret,实现幂等部署
4.2 TokenSet - 创建/更新 Token
4.2.1 基本信息
- 名称:
ACL.TokenSet - 协议:RPC 方法
- HTTP 映射:
PUT /v1/acl/token - 幂等性:是(相同 AccessorID 覆盖)
4.2.2 请求结构体
type ACLTokenSetRequest struct {
ACLToken ACLToken // Token 对象(包含所有字段)
Create bool // true=创建,false=更新
Datacenter string // 目标数据中心
WriteRequest // Token、Namespace 等
}
字段表:
| 字段 | 类型 | 必填 | 默认 | 约束 | 说明 |
|---|---|---|---|---|---|
| ACLToken | ACLToken | 是 | - | 见 ACLToken 结构 | 要创建/更新的 Token |
| Create | bool | 是 | - | - | 创建时必须为 true,更新时为 false |
| Datacenter | string | 否 | 本地DC | - | 目标数据中心 |
ACLToken 子字段(创建时):
| 字段 | 必填 | 说明 |
|---|---|---|
| AccessorID | 否 | 自动生成(更新时必填) |
| SecretID | 否 | 自动生成(可指定) |
| Description | 否 | Token 描述 |
| Policies | 否 | 关联的 Policy ID 或 Name 列表 |
| Roles | 否 | 关联的 Role ID 或 Name 列表 |
| ServiceIdentities | 否 | 服务身份列表 |
| Local | 否 | 默认 false(全局) |
| ExpirationTTL | 否 | 过期时间(Duration 格式,如 “24h”) |
4.2.3 响应结构体
type ACLToken struct {
AccessorID string
SecretID string
Description string
Policies []ACLTokenPolicyLink
Roles []ACLTokenRoleLink
Local bool
CreateTime time.Time
ExpirationTime time.Time
Hash []byte
CreateIndex uint64
ModifyIndex uint64
}
4.2.4 入口函数与核心代码
func (a *ACL) TokenSet(
args *structs.ACLTokenSetRequest,
reply *structs.ACLToken,
) error {
// 1. 前置校验
if err := a.aclPreCheck(); err != nil {
return err
}
// 2. 权限校验:需要 acl:write 权限
authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.ACLToken.EnterpriseMeta, nil)
if err != nil {
return err
}
if authz.ACLWrite(nil) != acl.Allow {
return acl.ErrPermissionDenied
}
// 3. 转发到 Leader
if done, err := a.srv.ForwardRPC("ACL.TokenSet", args, reply); done {
return err
}
// 4. 验证 Token 字段
if args.Create {
// 创建时生成 ID
if args.ACLToken.AccessorID == "" {
accessor, _ := lib.GenerateUUID(a.srv.checkTokenUUID)
args.ACLToken.AccessorID = accessor
}
if args.ACLToken.SecretID == "" {
secret, _ := lib.GenerateUUID(a.srv.checkTokenUUID)
args.ACLToken.SecretID = secret
}
args.ACLToken.CreateTime = time.Now()
} else {
// 更新时验证 AccessorID 存在
if args.ACLToken.AccessorID == "" {
return fmt.Errorf("AccessorID is required for token update")
}
// 从 State Store 读取旧 Token
state := a.srv.fsm.State()
_, existing, err := state.ACLTokenGetByAccessor(nil, args.ACLToken.AccessorID, nil)
if err != nil || existing == nil {
return fmt.Errorf("Token not found")
}
// 保留 CreateTime 和 SecretID
args.ACLToken.CreateTime = existing.CreateTime
if args.ACLToken.SecretID == "" {
args.ACLToken.SecretID = existing.SecretID
}
}
// 5. 处理 ExpirationTTL(转换为 ExpirationTime)
if args.ACLToken.ExpirationTTL != 0 {
args.ACLToken.ExpirationTime = time.Now().Add(args.ACLToken.ExpirationTTL)
}
// 6. 验证 Policy/Role 引用存在
if err := a.validateTokenLinks(&args.ACLToken); err != nil {
return err
}
// 7. 通过 Raft Apply
req := &structs.ACLTokenBatchSetRequest{
Tokens: structs.ACLTokens{&args.ACLToken},
CAS: false,
}
resp, err := a.srv.raftApply(structs.ACLTokenSetRequestType, req)
if err != nil {
return err
}
// 8. 刷新 ACL Resolver 缓存
if _, token, err := a.srv.fsm.State().ACLTokenGetByAccessor(nil, args.ACLToken.AccessorID, nil); err == nil && token != nil {
*reply = *token
}
return nil
}
代码逻辑解释:
- 步骤 1-2:检查 ACL 启用,验证请求者具有
acl:write权限 - 步骤 3:非 Leader 转发
- 步骤 4:创建时生成 ID 和 SecretID,更新时保留原值
- 步骤 5:处理 TTL,转换为绝对过期时间
- 步骤 6:验证引用的 Policy/Role 存在于 State Store
- 步骤 7:Raft Apply 写入
- 步骤 8:返回最终 Token(包括 CreateIndex/ModifyIndex)
4.2.5 时序图
sequenceDiagram
autonumber
participant Client as 客户端
participant HTTP as HTTP API
participant RPC as ACL RPC Endpoint
participant Resolver as ACL Resolver
participant Leader as Raft Leader
participant FSM as FSM
participant State as State Store
Client->>HTTP: PUT /v1/acl/token<br>{"Description": "App Token", "Policies": [...]}
HTTP->>RPC: ACL.TokenSet(request)
RPC->>Resolver: ResolveToken(请求者 Token)
Resolver-->>RPC: Authorizer
RPC->>RPC: 检查 acl:write 权限
alt 权限不足
RPC-->>Client: 403 Permission Denied
end
RPC->>RPC: 生成 AccessorID, SecretID<br>设置 CreateTime
RPC->>State: 验证 Policy/Role 引用存在
State-->>RPC: OK
RPC->>Leader: raftApply(ACLTokenSetRequestType, token)
Leader->>FSM: Apply Log
FSM->>State: ACLTokenSet(token)
State->>State: 插入/更新 tokens 表<br>更新索引
State-->>FSM: 成功
FSM-->>Leader: 成功
Leader-->>RPC: 成功
RPC->>State: ACLTokenGetByAccessor(AccessorID)
State-->>RPC: 完整 Token(含 Index)
RPC-->>HTTP: Token
HTTP-->>Client: 200 OK + Token JSON
Note over Resolver: 后台任务:Token 进入缓存
4.3 TokenRead - 读取 Token
4.3.1 基本信息
- 名称:
ACL.TokenRead - 协议:RPC 方法
- HTTP 映射:
GET /v1/acl/token/:accessor-id - 幂等性:是
4.3.2 请求结构体
type ACLTokenGetRequest struct {
TokenID string // AccessorID 或 SecretID
TokenIDType ACLTokenIDType // "accessor" 或 "secret"
Expanded bool // 是否展开 Policy 内容
Datacenter string
acl.EnterpriseMeta
QueryOptions
}
字段表:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| TokenID | string | 是 | Token 的 AccessorID 或 SecretID |
| TokenIDType | ACLTokenIDType | 是 | “accessor” 或 “secret” |
| Expanded | bool | 否 | true 时返回 Policy 完整内容 |
4.3.3 核心代码
func (a *ACL) TokenRead(
args *structs.ACLTokenGetRequest,
reply *structs.ACLTokenResponse,
) error {
// 1. 权限校验:读取自己的 Token 或具有 acl:read 权限
authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, nil)
if err != nil {
return err
}
// 2. 从 State Store 读取
state := a.srv.fsm.State()
var token *structs.ACLToken
if args.TokenIDType == structs.ACLTokenAccessor {
_, token, err = state.ACLTokenGetByAccessor(nil, args.TokenID, &args.EnterpriseMeta)
} else {
_, token, err = state.ACLTokenGetBySecret(nil, args.TokenID, &args.EnterpriseMeta)
}
if err != nil || token == nil {
return acl.ErrNotFound
}
// 3. 权限判定:读取他人 Token 需要 acl:read
if args.Token != token.SecretID && authz.ACLRead(nil) != acl.Allow {
return acl.ErrPermissionDenied
}
// 4. 如果 Expanded=true,填充 Policy 内容
if args.Expanded {
for i := range token.Policies {
_, policy, _ := state.ACLPolicyGetByID(nil, token.Policies[i].ID, &args.EnterpriseMeta)
if policy != nil {
token.Policies[i].Policy = policy
}
}
}
reply.Token = token
reply.Index = token.ModifyIndex
return nil
}
4.4 TokenDelete - 删除 Token
4.4.1 基本信息
- 名称:
ACL.TokenDelete - 协议:RPC 方法
- HTTP 映射:
DELETE /v1/acl/token/:accessor-id - 幂等性:是(重复删除返回成功)
4.4.2 请求结构体
type ACLTokenDeleteRequest struct {
TokenID string // Token 的 AccessorID
Datacenter string
acl.EnterpriseMeta
WriteRequest
}
4.4.3 核心代码
func (a *ACL) TokenDelete(
args *structs.ACLTokenDeleteRequest,
reply *string,
) error {
// 1. 权限校验:acl:write
authz, err := a.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, nil)
if authz.ACLWrite(nil) != acl.Allow {
return acl.ErrPermissionDenied
}
// 2. 禁止删除内置 Token
if args.TokenID == structs.ACLTokenAnonymousID {
return fmt.Errorf("Cannot delete anonymous token")
}
// 3. 通过 Raft Apply 删除
req := &structs.ACLTokenBatchDeleteRequest{
TokenIDs: []string{args.TokenID},
}
_, err = a.srv.raftApply(structs.ACLTokenDeleteRequestType, req)
if err != nil {
return err
}
// 4. 刷新 Resolver 缓存
a.srv.ACLResolver.cache.Remove(args.TokenID)
*reply = args.TokenID
return nil
}
4.5 Login - 认证方法登录
4.5.1 基本信息
- 名称:
ACL.Login - 协议:RPC 方法
- HTTP 映射:
POST /v1/acl/login - 幂等性:否(每次生成新 Token)
4.5.2 请求结构体
type ACLLoginRequest struct {
Auth *ACLLoginParams // 认证参数
Datacenter string
WriteRequest
}
type ACLLoginParams struct {
AuthMethod string // Auth Method 名称
BearerToken string // JWT/OIDC Token
Meta map[string]string // 附加元数据
acl.EnterpriseMeta
}
字段表:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| AuthMethod | string | 是 | 认证方法名称(如 “k8s-auth”) |
| BearerToken | string | 是 | JWT Token 或 OIDC ID Token |
| Meta | map[string]string | 否 | 附加到 Token Description 的元数据 |
4.5.3 响应结构体
type ACLToken struct {
AccessorID string
SecretID string
Description string
AuthMethod string // 设置为登录的 Auth Method
Roles []ACLTokenRoleLink // 根据 BindingRule 匹配的 Role
Local bool // true(Auth Method Token 始终是 Local)
ExpirationTime time.Time // 根据 AuthMethod.MaxTokenTTL 设置
// ...
}
4.5.4 入口函数与核心代码
func (a *ACL) Login(
args *structs.ACLLoginRequest,
reply *structs.ACLToken,
) error {
// 1. 前置校验
if !a.srv.LocalTokensEnabled() {
return errAuthMethodsRequireTokenReplication
}
// 2. 加载 Auth Method 配置
authMethod, validator, err := a.srv.loadAuthMethod(
args.Auth.AuthMethod,
&args.Auth.EnterpriseMeta,
)
if err != nil {
return err
}
// 3. 验证 Bearer Token
verifiedIdentity, err := validator.ValidateLogin(
context.Background(),
args.Auth.BearerToken,
)
if err != nil {
return fmt.Errorf("Auth failed: %w", err)
}
// 4. 查询匹配的 Binding Rules
state := a.srv.fsm.State()
_, rules, err := state.ACLBindingRuleList(nil, args.Auth.AuthMethod, &args.Auth.EnterpriseMeta)
if err != nil {
return err
}
var roleLinks []structs.ACLTokenRoleLink
for _, rule := range rules {
// 使用 Selector 表达式匹配 Identity 字段
if matches, _ := evaluateSelector(rule.Selector, verifiedIdentity); matches {
if rule.BindType == "role" {
roleLinks = append(roleLinks, structs.ACLTokenRoleLink{
Name: interpolate(rule.BindName, verifiedIdentity),
})
}
}
}
if len(roleLinks) == 0 {
return fmt.Errorf("No matching binding rules")
}
// 5. 创建 Token
accessor, _ := lib.GenerateUUID(a.srv.checkTokenUUID)
secret, _ := lib.GenerateUUID(a.srv.checkTokenUUID)
description := fmt.Sprintf("token created via login: %s", args.Auth.AuthMethod)
if len(args.Auth.Meta) > 0 {
description += fmt.Sprintf(" (%v)", args.Auth.Meta)
}
token := &structs.ACLToken{
AccessorID: accessor,
SecretID: secret,
Description: description,
Roles: roleLinks,
AuthMethod: args.Auth.AuthMethod,
Local: true, // Auth Method Token 始终是 Local
CreateTime: time.Now(),
}
// 6. 设置过期时间
if authMethod.MaxTokenTTL > 0 {
token.ExpirationTime = time.Now().Add(authMethod.MaxTokenTTL)
}
// 7. Raft Apply
req := &structs.ACLTokenBatchSetRequest{
Tokens: structs.ACLTokens{token},
}
_, err = a.srv.raftApply(structs.ACLTokenSetRequestType, req)
if err != nil {
return err
}
*reply = *token
return nil
}
代码逻辑解释:
- 步骤 1:检查 Token 复制是否启用(Auth Method 需要)
- 步骤 2:加载 Auth Method 配置和对应的验证器(JWT/Kubernetes/OIDC)
- 步骤 3:调用验证器验证 Bearer Token,返回 Identity(包含 claims)
- 步骤 4:查询所有 Binding Rules,使用 Selector 表达式匹配 Identity
- 步骤 5:根据匹配的规则生成 Role 列表
- 步骤 6:创建 Token(Local=true,带过期时间)
- 步骤 7:通过 Raft 持久化
4.5.5 时序图
sequenceDiagram
autonumber
participant Client as 应用/用户
participant HTTP as HTTP API
participant RPC as ACL RPC Endpoint
participant AuthMethod as Auth Method 验证器
participant State as State Store
participant Leader as Raft Leader
participant FSM as FSM
Client->>HTTP: POST /v1/acl/login<br>{"AuthMethod": "k8s-auth", "BearerToken": "..."}
HTTP->>RPC: ACL.Login(request)
RPC->>State: 读取 Auth Method 配置
State-->>RPC: AuthMethod + Config
RPC->>AuthMethod: ValidateLogin(BearerToken)
AuthMethod->>AuthMethod: 验证 JWT 签名<br>提取 Claims
AuthMethod-->>RPC: VerifiedIdentity (claims)
RPC->>State: ACLBindingRuleList(AuthMethod)
State-->>RPC: BindingRules[]
loop 每个 Binding Rule
RPC->>RPC: 评估 Selector 表达式<br>匹配 Identity 字段
alt Selector 匹配
RPC->>RPC: 插值 BindName(如 "web-${serviceaccount.name}")<br>生成 Role 引用
end
end
RPC->>RPC: 生成 Token<br>关联匹配的 Roles<br>设置 ExpirationTime
RPC->>Leader: raftApply(ACLTokenSetRequestType, token)
Leader->>FSM: Apply Log
FSM->>State: ACLTokenSet(token)
State-->>FSM: 成功
FSM-->>RPC: 成功
RPC-->>HTTP: Token (AccessorID + SecretID)
HTTP-->>Client: 200 OK + Token JSON
Note over Client: 使用 SecretID 调用 Consul API
4.5.6 Auth Method 配置示例
Kubernetes Auth Method:
{
"Name": "k8s-auth",
"Type": "kubernetes",
"Description": "Kubernetes Service Account Auth",
"MaxTokenTTL": "8h",
"Config": {
"Host": "https://kubernetes.default.svc",
"CACert": "<base64-ca-cert>",
"ServiceAccountJWT": "<token-reviewer-jwt>"
}
}
Binding Rule 示例:
{
"AuthMethod": "k8s-auth",
"Selector": "serviceaccount.namespace==default",
"BindType": "role",
"BindName": "web-role"
}
完整 Login 流程示例:
# 1. Kubernetes Pod 内获取 Service Account Token
K8S_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
# 2. 调用 Consul Login
curl -X POST http://consul:8500/v1/acl/login \
-d '{
"AuthMethod": "k8s-auth",
"BearerToken": "'$K8S_TOKEN'"
}'
# 响应:
{
"AccessorID": "xxxx-xxxx-xxxx-xxxx",
"SecretID": "yyyy-yyyy-yyyy-yyyy",
"Description": "token created via login: k8s-auth",
"Roles": [
{"ID": "...", "Name": "web-role"}
],
"AuthMethod": "k8s-auth",
"Local": true,
"ExpirationTime": "2024-10-26T08:00:00Z"
}
# 3. 使用 SecretID 调用其他 API
curl -H "X-Consul-Token: yyyy-yyyy-yyyy-yyyy" \
http://consul:8500/v1/catalog/services
5. 关键流程与时序图
5.1 Token 权限解析流程
sequenceDiagram
autonumber
participant Client as 客户端请求
participant RPC as RPC Endpoint
participant Resolver as ACL Resolver
participant Cache as Token Cache
participant State as State Store
participant Authorizer as Authorizer
Client->>RPC: RPC 请求 (携带 Token)
RPC->>Resolver: ResolveToken(secretID)
Resolver->>Cache: 查询缓存 (key=secretID)
alt 缓存命中
Cache-->>Resolver: Authorizer (cached)
else 缓存未命中
Resolver->>State: ACLTokenGetBySecret(secretID)
State-->>Resolver: Token + Policies + Roles
Resolver->>State: 批量读取 Policy 内容
State-->>Resolver: Policy Rules (HCL)
Resolver->>Resolver: 编译 Policy 为规则树
Resolver->>Authorizer: 创建 PolicyAuthorizer
Resolver->>Cache: 存入缓存 (TTL=30s)
end
Resolver-->>RPC: Authorizer
RPC->>Authorizer: ServiceWrite("web", ctx)
Authorizer->>Authorizer: 查找 "web" 的 service 规则<br>评估 Policy(Deny > Write > Read)
Authorizer-->>RPC: Allow / Deny
alt 权限不足
RPC-->>Client: 403 Permission Denied
else 权限充足
RPC->>RPC: 执行业务逻辑
RPC-->>Client: 200 OK + 响应数据
end
流程说明:
- 步骤 1-3:所有 RPC 请求携带 Token(Header 或请求参数),Endpoint 调用 Resolver 解析
- 步骤 4-5:Resolver 首先查询内存缓存(LRU,容量 1024,TTL 30秒)
- 步骤 6-11:缓存未命中时,从 State Store 读取 Token 及其关联的 Policy/Role,编译 Policy Rules 为决策树,创建 Authorizer 并缓存
- 步骤 13-14:RPC Endpoint 调用 Authorizer 的具体方法(如
ServiceWrite),Authorizer 在规则树中查找匹配规则,返回决策 - 步骤 15-17:根据决策结果,拒绝请求或继续执行
性能优化:
- 缓存避免重复解析,P99 延迟 < 1ms
- Policy 编译一次,后续判定 O(log n)
- 批量读取 Policy,减少 State Store 查询次数
5.2 ACL 复制流程(跨数据中心)
sequenceDiagram
autonumber
participant PrimaryDC as 主数据中心
participant PrimaryState as 主 DC State Store
participant ReplicatorToken as Token Replicator<br>(从DC)
participant ReplicatorPolicy as Policy Replicator<br>(从DC)
participant SecondaryRPC as 从 DC RPC
participant SecondaryState as 从 DC State Store
Note over PrimaryDC,SecondaryState: 初始化:从 DC 启动复制任务
ReplicatorPolicy->>PrimaryDC: RPC ACL.PolicyList (blocking query)
PrimaryDC->>PrimaryState: 读取所有 Policy (Index > lastIndex)
PrimaryState-->>PrimaryDC: Policies[]
PrimaryDC-->>ReplicatorPolicy: Policies[] + Index
ReplicatorPolicy->>ReplicatorPolicy: 对比本地 Policy<br>计算差异(新增、更新、删除)
loop 批量写入(100条/批次)
ReplicatorPolicy->>SecondaryRPC: raftApply(ACLPolicyBatchSetRequest, policies)
SecondaryRPC->>SecondaryState: 批量插入/更新 Policy
end
ReplicatorPolicy->>ReplicatorPolicy: 更新 lastIndex<br>休眠 1s 后继续
Note over PrimaryDC,SecondaryState: Token 复制(仅 Global Token)
ReplicatorToken->>PrimaryDC: RPC ACL.TokenList (IncludeGlobal=true, Index > lastIndex)
PrimaryDC->>PrimaryState: 读取 Global Token (Local=false)
PrimaryState-->>PrimaryDC: Tokens[]
PrimaryDC-->>ReplicatorToken: Tokens[] + Index
ReplicatorToken->>ReplicatorToken: 对比本地 Token<br>计算差异
loop 批量写入
ReplicatorToken->>SecondaryRPC: raftApply(ACLTokenBatchSetRequest, tokens)
SecondaryRPC->>SecondaryState: 批量插入/更新 Token
end
ReplicatorToken->>ReplicatorToken: 更新 lastIndex<br>休眠 1s 后继续
Note over PrimaryDC,SecondaryState: 持续循环直到进程终止
复制说明:
- Policy 复制:所有 Policy 都是全局的,从主 DC 复制到所有从 DC
- Token 复制:只复制
Local=false的 Global Token,Local Token 不复制 - Role 复制:类似 Policy,全局复制
- Binding Rule 和 Auth Method:全局复制
一致性保证:
- 复制基于 Raft Index 递增顺序,保证因果一致性
- Blocking Query 机制,主 DC 有更新时立即触发复制
- 复制失败时自动重试,回退指数退避
延迟:
- 正常情况:1-3 秒(取决于网络延迟)
- 网络分区:复制暂停,恢复后自动追赶(catch-up)
6. 关键功能实现分析
6.1 ACL Resolver - Token 解析与缓存
6.1.1 功能概述
ACL Resolver 是 ACL 模块的核心组件,负责将 Token SecretID 解析为 Authorizer 对象。Resolver 使用 LRU 缓存避免重复解析,显著提升性能。
6.1.2 核心数据结构
type ACLResolver struct {
config ACLResolverConfig
logger hclog.Logger
delegate ACLResolverBackend // State Store 的抽象接口
aclConf *acl.Config
// Token 缓存:SecretID -> Authorizer
cache *lru.TwoQueueCache
// Identity 缓存:AccessorID -> Identity
identityCache *lru.TwoQueueCache
// Policy 缓存:PolicyID -> ParsedPolicy
policyCache *lru.TwoQueueCache
// Role 缓存:RoleID -> Role
roleCache *lru.TwoQueueCache
// Sentinel值:匿名Token、已删除Token等
sentinel map[string]*authorizerCacheEntry
}
type authorizerCacheEntry struct {
authorizer acl.Authorizer
token *structs.ACLToken
expiresAt time.Time
fetchedAt time.Time
}
6.1.3 核心方法实现
ResolveToken - Token 解析入口
func (r *ACLResolver) ResolveToken(secretID string) (acl.Authorizer, error) {
// 1. 检查是否为匿名 Token
if secretID == "" || secretID == acl.AnonymousTokenSecret {
return r.anonymousAuthorizer()
}
// 2. 查询缓存
if entry, ok := r.cache.Get(secretID); ok {
cached := entry.(*authorizerCacheEntry)
// 检查是否过期
if time.Now().Before(cached.expiresAt) {
return cached.authorizer, nil
}
// 缓存过期,移除
r.cache.Remove(secretID)
}
// 3. 从 Backend 读取 Token
token, err := r.delegate.ACLTokenGetBySecret(secretID)
if err != nil {
return nil, err
}
if token == nil {
return nil, acl.ErrNotFound
}
// 4. 检查 Token 是否过期
if !token.ExpirationTime.IsZero() && time.Now().After(token.ExpirationTime) {
// 异步删除过期 Token
go r.delegate.ACLTokenDelete(token.AccessorID)
return nil, acl.ErrNotFound
}
// 5. 解析 Token 关联的 Policies 和 Roles
policies, err := r.resolvePoliciesForToken(token)
if err != nil {
return nil, err
}
// 6. 编译 Policies 为 Authorizer
authorizer, err := r.newAuthorizerFromPolicies(policies)
if err != nil {
return nil, err
}
// 7. 存入缓存
entry := &authorizerCacheEntry{
authorizer: authorizer,
token: token,
expiresAt: time.Now().Add(r.config.ACLTokenTTL), // 默认 30s
fetchedAt: time.Now(),
}
r.cache.Add(secretID, entry)
return authorizer, nil
}
resolvePoliciesForToken - 解析 Token 的所有 Policy
func (r *ACLResolver) resolvePoliciesForToken(token *structs.ACLToken) ([]*structs.ACLPolicy, error) {
var policies []*structs.ACLPolicy
// 1. 直接关联的 Policies
for _, link := range token.Policies {
policy, err := r.resolvePolicyByID(link.ID)
if err != nil {
return nil, err
}
if policy != nil {
policies = append(policies, policy)
}
}
// 2. 通过 Roles 关联的 Policies
for _, roleLink := range token.Roles {
role, err := r.resolveRoleByID(roleLink.ID)
if err != nil {
return nil, err
}
if role == nil {
continue
}
for _, policyLink := range role.Policies {
policy, err := r.resolvePolicyByID(policyLink.ID)
if err != nil {
return nil, err
}
if policy != nil {
policies = append(policies, policy)
}
}
}
// 3. ServiceIdentities 转换为隐式 Policy
for _, si := range token.ServiceIdentities {
policy := synthesizeServiceIdentityPolicy(si)
policies = append(policies, policy)
}
// 4. NodeIdentities 转换为隐式 Policy
for _, ni := range token.NodeIdentities {
policy := synthesizeNodeIdentityPolicy(ni)
policies = append(policies, policy)
}
return policies, nil
}
synthesizeServiceIdentityPolicy - ServiceIdentity 隐式策略生成
func synthesizeServiceIdentityPolicy(si *structs.ACLServiceIdentity) *structs.ACLPolicy {
// ServiceIdentity 自动生成以下权限:
// - service:write (允许注册服务)
// - service:read (允许查询服务)
// - node:read (允许查询节点)
// - intention:read (允许读取意图)
rules := fmt.Sprintf(`
service "%s" {
policy = "write"
}
service "%s-sidecar-proxy" {
policy = "write"
}
node_prefix "" {
policy = "read"
}
`, si.ServiceName, si.ServiceName)
return &structs.ACLPolicy{
ID: fmt.Sprintf("synthetic-policy-service-%s", si.ServiceName),
Name: fmt.Sprintf("service-identity-%s", si.ServiceName),
Description: "Synthetic policy generated from service identity",
Rules: rules,
}
}
6.1.4 缓存策略
缓存类型:
- 2Q Cache(Two-Queue LRU):分为频繁访问队列和最近访问队列,提升缓存命中率
- 容量:Token Cache 默认 1024 条,Policy Cache 512 条,Role Cache 256 条
- TTL:Token 缓存 30 秒,Policy/Role 缓存 30 秒
缓存失效:
- 时间失效:超过 TTL 自动过期
- 主动失效:Token/Policy/Role 更新/删除时,通过
cache.Remove()主动清除 - Token 过期:检测到
ExpirationTime已过期,自动剔除
缓存预热:
- Server 启动时,Resolver 不预加载缓存(懒加载)
- 首次请求触发加载,后续请求命中缓存
6.2 PolicyAuthorizer - 权限判定引擎
6.2.1 功能概述
PolicyAuthorizer 实现 Authorizer 接口,基于编译后的 Policy 规则进行权限判定。判定逻辑为前缀匹配 + 优先级合并。
6.2.2 核心数据结构
type PolicyAuthorizer struct {
// 默认策略(allow 或 deny)
defaultPolicy acl.EnforcementDecision
// 各资源的规则树
serviceRules *policyRuleSet
nodeRules *policyRuleSet
keyRules *policyRuleSet
sessionRules *policyRuleSet
eventRules *policyRuleSet
queryRules *policyRuleSet
// 全局权限
aclRule string // "read" / "write" / "deny"
keyringRule string
operatorRule string
meshRule string
peeringRule string
}
type policyRuleSet struct {
// 精确匹配规则:资源名 -> 策略级别
exactRules map[string]string
// 前缀匹配规则:前缀 -> 策略级别(按前缀长度排序)
prefixRules []prefixRule
}
type prefixRule struct {
prefix string
policy string
}
6.2.3 核心方法实现
ServiceWrite - 服务写权限判定
func (p *PolicyAuthorizer) ServiceWrite(name string, ctx *acl.AuthorizerContext) acl.EnforcementDecision {
// 1. 查找精确匹配规则
if policy, ok := p.serviceRules.exactRules[name]; ok {
return policyToDecision(policy, "write")
}
// 2. 查找前缀匹配规则(最长匹配优先)
for i := len(p.serviceRules.prefixRules) - 1; i >= 0; i-- {
rule := p.serviceRules.prefixRules[i]
if strings.HasPrefix(name, rule.prefix) {
return policyToDecision(rule.policy, "write")
}
}
// 3. 无匹配规则,返回 Default(由默认策略决定)
return acl.Default
}
func policyToDecision(policy string, requiredLevel string) acl.EnforcementDecision {
levels := map[string]int{
"deny": 0,
"read": 1,
"list": 2,
"write": 3,
}
required := levels[requiredLevel]
actual := levels[policy]
if actual >= required {
return acl.Allow
}
if policy == "deny" {
return acl.Deny
}
return acl.Default
}
newAuthorizerFromPolicies - 从 Policy 列表构建 Authorizer
func (r *ACLResolver) newAuthorizerFromPolicies(policies []*structs.ACLPolicy) (acl.Authorizer, error) {
var parsedPolicies []*acl.Policy
// 1. 解析所有 Policy 的 Rules(HCL -> 结构化)
for _, policy := range policies {
parsed, err := acl.NewPolicyFromSource(policy.Rules, &acl.Config{})
if err != nil {
return nil, fmt.Errorf("Failed to parse policy %s: %w", policy.Name, err)
}
parsedPolicies = append(parsedPolicies, parsed)
}
// 2. 合并所有 Policy(Deny 优先)
merged, err := acl.MergePolicies(parsedPolicies)
if err != nil {
return nil, err
}
// 3. 创建 PolicyAuthorizer
authorizer, err := acl.NewPolicyAuthorizerWithDefaults(merged, &acl.Config{
WildcardName: "*",
DefaultPolicy: r.config.ACLDefaultPolicy, // "allow" 或 "deny"
})
return authorizer, err
}
MergePolicies - 策略合并逻辑
func MergePolicies(policies []*Policy) (*Policy, error) {
merged := &Policy{}
for _, policy := range policies {
// 1. Service 规则合并
for _, rule := range policy.Services {
existing := merged.ServiceRules[rule.Name]
// Deny > Write > Read > List
merged.ServiceRules[rule.Name] = maxPolicy(existing, rule.Policy)
}
// 2. Node 规则合并
for _, rule := range policy.Nodes {
existing := merged.NodeRules[rule.Name]
merged.NodeRules[rule.Name] = maxPolicy(existing, rule.Policy)
}
// 3. Key 规则合并
for _, rule := range policy.Keys {
existing := merged.KeyRules[rule.Prefix]
merged.KeyRules[rule.Prefix] = maxPolicy(existing, rule.Policy)
}
// 4. 全局权限合并
merged.ACL = maxPolicy(merged.ACL, policy.ACL)
merged.Keyring = maxPolicy(merged.Keyring, policy.Keyring)
merged.Operator = maxPolicy(merged.Operator, policy.Operator)
}
return merged, nil
}
func maxPolicy(a, b string) string {
priority := map[string]int{"deny": 4, "write": 3, "read": 2, "list": 1, "": 0}
if priority[a] > priority[b] {
return a
}
return b
}
7. 配置与最佳实践
7.1 关键配置项
| 配置项 | 默认值 | 说明 | 调优建议 |
|---|---|---|---|
acl.enabled |
false | 是否启用 ACL | 生产环境必须启用 |
acl.default_policy |
allow | 默认策略 | 生产环境设置为 deny |
acl.enable_token_persistence |
true | 是否持久化 Agent Token | 建议启用,避免重启后丢失 |
acl.token_ttl |
30s | Token 缓存 TTL | 默认值合理,高频更新可降低到 10s |
acl.policy_ttl |
30s | Policy 缓存 TTL | 默认值合理 |
acl.role_ttl |
30s | Role 缓存 TTL | 默认值合理 |
acl.down_policy |
“extend-cache” | ACL 系统不可用时的降级策略 | “extend-cache” 保持可用性,“deny” 保持安全性 |
7.2 最佳实践
1. 最小权限原则:
- 避免使用
global-management策略的 Token 进行日常操作 - 为每个应用/服务创建独立 Token,仅授予必要权限
- 使用 ServiceIdentity 简化服务 Token 配置
2. Token 生命周期管理:
- 为应用 Token 设置
ExpirationTTL(如 24 小时),强制定期轮换 - 使用 Auth Method + Binding Rule 实现自动化 Token 发放
- 定期审计 Token 列表,删除不再使用的 Token
3. 策略设计:
- 使用 Role 组织 Policy,避免 Token 直接关联大量 Policy
- Policy 命名规范:
<team>-<resource>-<level>(如platform-service-read) - 避免过度使用通配符前缀(影响性能)
4. 复制与高可用:
- 主数据中心使用 3 或 5 个 Server,保证 ACL 系统高可用
- 从数据中心启用 Token 复制(
acl.tokens.replication) - 监控复制延迟(
consul.acl.replication.lag)
5. 安全加固:
- 启用 TLS 加密所有 RPC/HTTP 通信
- 限制 Bootstrap 操作(删除 Reset Index 文件)
- 日志脱敏(SecretID 自动替换为
<redacted>)
7.3 故障排查
Token 无效/权限不足:
# 1. 验证 Token 是否存在
consul acl token read -id <accessor-id>
# 2. 检查 Token 关联的 Policy
consul acl token read -id <accessor-id> -expanded
# 3. 测试权限
consul acl token test -token <secret-id>
复制延迟过高:
# 查看复制状态
consul acl replication-status
# 检查网络延迟(主 DC -> 从 DC)
ping <primary-dc-server>
# 查看 Raft 日志堆积
consul operator raft list-peers
Resolver 缓存失效:
# 查看 Resolver 指标
curl http://localhost:8500/v1/agent/metrics | grep acl.resolveToken
# 清空 Resolver 缓存(重启 Server)
consul reload
8. 性能与容量规划
8.1 性能指标
典型延迟(3节点集群,LAN网络):
| 操作 | P50 | P95 | P99 |
|---|---|---|---|
| Token 解析(缓存命中) | 0.5ms | 1ms | 2ms |
| Token 解析(缓存未命中) | 5ms | 10ms | 20ms |
| Token 创建 | 20ms | 40ms | 80ms |
| Policy 创建 | 15ms | 30ms | 60ms |
| Login(Auth Method) | 30ms | 60ms | 100ms |
| 权限判定(ServiceWrite) | 0.1ms | 0.3ms | 0.5ms |
吞吐量:
- Resolver 解析 QPS:10,000+(缓存命中)
- Token 创建 TPS:100-200(受 Raft 限制)
- ACL 写操作 TPS:200-500(批量操作)
8.2 容量上界
| 资源 | 推荐上限 | 硬限制 | 说明 |
|---|---|---|---|
| Token 总数 | 100,000 | 无 | 受 State Store 内存限制 |
| Policy 总数 | 10,000 | 无 | 影响 Resolver 初始化时间 |
| 单 Token 关联 Policy 数 | 10 | 无 | 影响解析性能 |
| 单 Policy 规则数 | 1000 | 无 | 影响编译时间 |
| Auth Method 数量 | 100 | 无 | - |
| Binding Rule 数量 | 1000 | 无 | Login 时遍历所有规则 |
8.3 监控告警
关键指标:
consul.acl.resolveToken- Token 解析延迟consul.acl.cache.hit/consul.acl.cache.miss- 缓存命中率consul.acl.replication.lag- 复制延迟(秒)consul.raft.apply- Raft Apply 延迟(影响 ACL 写操作)
告警阈值:
- Resolver 缓存命中率 < 90%
- Token 解析 P99 > 50ms
- 复制延迟 > 10 秒
- Token/Policy 创建失败率 > 1%