黑白名单权限控制
规则配置
规则创建
- 创建一个
AuthorityRule
规则对象 - 三个关键要素
setStrategy
: 黑白名单类型setResource
: 规则和资源的绑定关系setLimitApp
: 限制的来源
- 调用
AuthorityRuleManager.loadRules()
加载规则
监听器实例化和管理
AuthorityPropertyListener
监听器来感知黑白名单规则的变化, 将此监听器放入 SentinelProperty
中进行管理
现有疑惑
- 没有看到创建监听器
AuthorityPropertyListener
的地方 - 没有看到将监听器添加到监听器管理者的地方, 即调用
SentinelProperty#addListener
方法 - 只看到了一句
AuthorityRuleManager.loadRules()
猜测是否创建监听器和将监听器添加到监听器管理者两个动作都在AuthorityRuleManager.loadRules()
中
查验代码发现确实如此
public final class AuthorityRuleManager {
// 其它代码...
// 创建监听器动作
private static final RulePropertyListener LISTENER = new RulePropertyListener();
// 将监听器添加到监听器管理者
static {
// 将黑白名单 Listener 放到 SentinelProperty 当中去管理
currentProperty.addListener(LISTENER);
}
// 其它代码...
}
具体详细代码如下
public final class AuthorityRuleManager {
// 资源名称 -> 资源对应的规则
private static volatile Map<String, Set<AuthorityRule>> authorityRules = new ConcurrentHashMap<>();
// 饿汉式单例模式实例化黑白名单 Listener 对象
private static final RulePropertyListener LISTENER = new RulePropertyListener();
// Listener对象的管理者
private static SentinelProperty<List<AuthorityRule>> currentProperty = new DynamicSentinelProperty<>();
static {
// 将黑白名单 Listener 放到 SentinelProperty 当中去管理
currentProperty.addListener(LISTENER);
}
// 静态内部类的方式实现 黑白名单Listener
private static class RulePropertyListener implements PropertyListener<List<AuthorityRule>> {
// 规则初始化
@Override
public synchronized void configLoad(List<AuthorityRule> value) {}
// 规则变更
@Override
public synchronized void configUpdate(List<AuthorityRule> conf) {}
}
}
初始化规则
####初始化规则位置
上述代码已经实例化了黑白名单监听器,并且已经将监听器交由 SentinelProperty 进行管理, 我们知道监听器监听的是规则, 那么还需要初始化规则
通常情况下,在调用 currentProperty.addListener(LISTENER)
之后,我们会再执行一条初始化规则的代码.
但是sentinel没有这么做, 为什么? 因为没必要, 看下述案例, 发现本质都是一样的, 换汤不换药罢了
// 方式一: 调用addListener后, 再调用初始化规则代码
static {
// 将监听器交给SentinelProperty管理, 这里的addListener只有添加监听器逻辑
currentProperty.addListener(LISTENER);
// 初始化规则
listener.configLoad(value)
}
addListener(...) {
// 添加监听器
listeners.add(listener);
}
// ------------------
// 方式二: 将初始化规则代码合并到addListener中
static {
// 将监听器交给SentinelProperty管理, 里边方法
currentProperty.addListener(LISTENER);
}
addListener(...) {
// 添加监听器
listeners.add(listener);
// 初始化规则
listener.configLoad(value);
}
sentinnel真正的做法如下, 将初始化规则动作合并到addListener()
, 只要调用 addListener()
方法就会进行规则的初始化, 具体的方法实现如下
public class DynamicSentinelProperty<T> implements SentinelProperty<T> {
protected Set<PropertyListener<T>> listeners = new CopyOnWriteArraySet<>();
private T value = null;
@Override
public void addListener(PropertyListener<T> listener) {
listeners.add(listener);
// 调用黑白名单的初始化规则方法
listener.configLoad(value);
}
}
此时黑名单规则初始化的流程就明朗了, 如下图所示
- AuthorityRuleManager初始化时, 调用addListener()
- 注册监听器
- 初始化规则
初始化规则逻辑configLoad()
DynamicSentinelProperty#addListener()
中的configLoad()
实际上调用的是AuthorityRuleManager.RulePropertyListener#configLoad()
, 也就是下边这块代码
public final class AuthorityRuleManager {
// 资源名称 -> 资源对应的规则
private static volatile Map<String, Set<AuthorityRule>> authorityRules = new ConcurrentHashMap<>();
// 省略上面代码...
// 静态内部类的方式实现 黑白名单Listener
private static class RulePropertyListener implements PropertyListener<List<AuthorityRule>> {
// 规则初始化
@Override
public synchronized void configLoad(List<AuthorityRule> value) {
authorityRules.updateRules(loadAuthorityConf(value));
RecordLog.info("[AuthorityRuleManager] Authority rules loaded: {}", authorityRules);
}
// 规则变更
@Override
public synchronized void configUpdate(List<AuthorityRule> conf) {
authorityRules.updateRules(loadAuthorityConf(conf));
RecordLog.info("[AuthorityRuleManager] Authority rules received: {}", authorityRules);
}
// 加载规则, 这里将资源和资源对应的规则列表放到Map中管理
private Map<String, List<AuthorityRule>> loadAuthorityConf(List<AuthorityRule> list) {
Map<String, List<AuthorityRule>> newRuleMap = new ConcurrentHashMap<>();
if (list == null || list.isEmpty()) {
return newRuleMap;
}
// 遍历每个规则
for (AuthorityRule rule : list) {
if (!isValidRule(rule)) {
RecordLog.warn("[AuthorityRuleManager] Ignoring invalid authority rule when loading new rules: {}", rule);
continue;
}
if (StringUtil.isBlank(rule.getLimitApp())) {
rule.setLimitApp(RuleConstant.LIMIT_APP_DEFAULT);
}
// 获取规则对应的资源名称
String identity = rule.getResource();
List<AuthorityRule> ruleSet = newRuleMap.get(identity);
// 将规则放到 Map 当中
if (ruleSet == null) {
ruleSet = new ArrayList<>();
ruleSet.add(rule);
newRuleMap.put(identity, ruleSet);
} else {
// 一个资源最多只能有一个权限规则,所以忽略多余的规则即可
RecordLog.warn("[AuthorityRuleManager] Ignoring redundant rule: {}", rule.toString());
}
}
return newRuleMap;
}
}
}
我们又知道手动初始化规则的代码是AuthorityRuleManager.loadRules(ruleList)
, 其实调用
public final class AuthorityRuleManager {
// 发现currentProperty其实指向的就是DynamicSentinelProperty, 即上边分析的
private static SentinelProperty<List<AuthorityRule>> currentProperty = new DynamicSentinelProperty<>();
// 初始化调用的就是这个
public static void loadRules(List<AuthorityRule> rules) {
// 调用监听器的 updateValue() 方法来通知每一个监听者的 configUpdate() 方法
currentProperty.updateValue(rules);
}
}
public class DynamicSentinelProperty<T> implements SentinelProperty<T> {
// 省略其它代码...
@Override
public boolean updateValue(T newValue) {
if (isEqual(value, newValue)) {
return false;
}
RecordLog.info("[DynamicSentinelProperty] Config will be updated to: {}", newValue);
// 将传入的规则赋值给value
value = newValue;
// 遍历通知所有监听者
for (PropertyListener<T> listener : listeners) {
// 这里调用了configUpdate, 即上边分析的configUpdate()
// 具体全类名如下com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager.RulePropertyListener#configUpdate
listener.configUpdate(newValue);
}
return true;
}
}
大家可能会产生一个疑问:静态代码块里不是已经将规则初始化完成了吗?为什么这里调用 loadRules()
方法调用 updateValue()
来通知监听者说规则变更了呢
因为执行静态代码块里的 listener.configLoad(value)
时, 这里的全局变量value初始默认为null
, 首次调用 listener.configLoad(value)
进行规则初始化是不会成功的, 所以这里又调用loadRules()
, 将规则集合参数携带过去, 最终才能正常进入 for 循环遍历规则集合,将其组装成 Map 结构
如下图所示
到此为止, 规则已经初始化完成且将资源和规则的映射关系
放到了Map中存储, 接下来就是对规则的校验
规则验证
黑白名单规则验证是我们责任链中的第五个Slot
, 负责校验黑白名单
上边初始化得到一个资源和规则的映射关系
的Map, 那么这里来就可以遍历这个map验证是否有访问权限
public class AuthoritySlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)
throws Throwable {
// 规则校验
checkBlackWhiteAuthority(resourceWrapper, context);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}
void checkBlackWhiteAuthority(ResourceWrapper resource, Context context) throws AuthorityException {
// 通过AuthorityRuleManager获取获取当前资源的规则集合
List<AuthorityRule> rules = AuthorityRuleManager.getRules(resource.getName());
if (rules == null) {
return;
}
// 遍历规则
for (AuthorityRule rule : rules) {
// passCheck进行校验, 如果不通过就抛出AuthorityException
if (!AuthorityRuleChecker.passCheck(rule, context)) {
throw new AuthorityException(context.getOrigin(), rule);
}
}
}
}
可以看到核心就是AuthorityRuleChecker.passCheck()
, 下边分析一下
final class AuthorityRuleChecker {
static boolean passCheck(AuthorityRule rule, Context context) {
// 获取origin
String requester = context.getOrigin();
// 如果没设置来源,或者没限制app,那直接放行就好了,相当于不做规则限制
if (StringUtil.isEmpty(requester) || StringUtil.isEmpty(rule.getLimitApp())) {
return true;
}
// 判断此次请求的来源是不是在limitApp里,注意这里用的是近似精确匹配,但不是绝对精确,
// 比如limitApp写的是a,b。然后资源名称假设是",b",那么就出问题了,因为limitApp是按照逗号隔开的,但是资源却包含了逗号
// 这样的话下面算法就是 contain=true,这显然是不对的
int pos = rule.getLimitApp().indexOf(requester);
// 这里判断是都大于-1, 进而得到limitapp是否包含origin
boolean contain = pos > -1;
// 如果近似精确匹配成功的话,在进行精确匹配
if (contain) {
boolean exactlyMatch = false;
// 使用英文逗号进行切割limitapp(可以设置多个limitapp,之间是用逗号分隔的)
String[] appArray = rule.getLimitApp().split(",");
for (String app : appArray) {
if (requester.equals(app)) {
exactlyMatch = true;
break;
}
}
contain = exactlyMatch;
}
int strategy = rule.getStrategy();
// 如果是黑名单,并且此次请求的来源在limitApp里
if (strategy == RuleConstant.AUTHORITY_BLACK && contain) {
// 返回false, 表示限流
return false;
}
// 如果配置是白名单, 并且origin不在limitApp
if (strategy == RuleConstant.AUTHORITY_WHITE && !contain) {
// 返回false, 表示限流
return false;
}
// 执行到这里说明, 就通过了校验, 放行
// 1. 如果是黑名单, 那么origin就不在limitApp内
// 2. 如果是白名单, 那么origin在limitApp内
return true;
}
private AuthorityRuleChecker() {}
}
验证流程图如下
仅当调用源不为空且规则配置了黑名单或白名单时,才会执行黑白名单的筛选逻辑。这表明,实现黑白名单限流的前提条件是,每个客户端在发起请求时都必须将自己服务唯一标志放到 Context 的 origin 里
context.getOrigin()
方法,因此在做黑白名单规则控制的时候,我们需要先定义好一个 origin,这个 origin 可以是userId
,也可以是IP地址
,还可以是项目名称
等,比如我们将 userId 为 1 和 2 的用户加入黑名单,那么我们就需要在每次请求此资源时在Context的origin里添加上userId,这个实现起来也很简单,可以搞个AOP每次都从header 或其他地方获取userId, 然后放到 Context 的origin里即可
参考资料
通关 Sentinel 流量治理框架 - 编程界的小學生 )