黑白名单权限控制
规则配置
规则创建
- 创建一个
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);}// 静态内部类的方式实现 黑白名单Listenerprivate static class RulePropertyListener implements PropertyListener<List<AuthorityRule>> {// 规则初始化@Overridepublic synchronized void configLoad(List<AuthorityRule> value) {}// 规则变更@Overridepublic 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;@Overridepublic 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<>();// 省略上面代码...// 静态内部类的方式实现 黑白名单Listenerprivate static class RulePropertyListener implements PropertyListener<List<AuthorityRule>> {// 规则初始化@Overridepublic synchronized void configLoad(List<AuthorityRule> value) {authorityRules.updateRules(loadAuthorityConf(value));RecordLog.info("[AuthorityRuleManager] Authority rules loaded: {}", authorityRules);}// 规则变更@Overridepublic 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> {// 省略其它代码...@Overridepublic boolean updateValue(T newValue) {if (isEqual(value, newValue)) {return false;}RecordLog.info("[DynamicSentinelProperty] Config will be updated to: {}", newValue);// 将传入的规则赋值给valuevalue = newValue;// 遍历通知所有监听者for (PropertyListener<T> listener : listeners) {// 这里调用了configUpdate, 即上边分析的configUpdate()// 具体全类名如下com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager.RulePropertyListener#configUpdatelistener.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> {@Overridepublic 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);}@Overridepublic 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进行校验, 如果不通过就抛出AuthorityExceptionif (!AuthorityRuleChecker.passCheck(rule, context)) {throw new AuthorityException(context.getOrigin(), rule);}}}
}
可以看到核心就是AuthorityRuleChecker.passCheck()
, 下边分析一下
final class AuthorityRuleChecker {static boolean passCheck(AuthorityRule rule, Context context) {// 获取originString 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是否包含originboolean 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不在limitAppif (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 流量治理框架 - 编程界的小學生 )