游客发表
ASPIP 这套访问控制代码,问控我第一次读的制源时候差点被它绕进去。名字听着像个缩写,问控其实不是制源——它就是个类名,全称是问控 Access Security Protocol for IP,作者自己起的制源。核心逻辑就一个:每个请求进来,问控先查 IP 在不在白名单,制源再看这个 IP 对应哪个角色,问控最后根据角色决定能不能碰目标资源。制源
代码里最关键的问控入口是 checkAccess() 方法。它接收三个参数:客户端 IP、制源请求路径、问控请求方法。制源方法体第一行就是问控 $ip = $this->normalizeIP($ip);,这步不能省。因为 IPv6 和 IPv4 混用的时候,字符串比较会出大问题。我见过有人直接拿 $_SERVER['REMOTE_ADDR'] 去比对,结果 IPv6 地址带端口号,白名单里写的是纯地址,死活匹配不上。
private function normalizeIP($ip) { if (strpos($ip, '::ffff:') === 0) { $ip = substr($ip, 7); } return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : '0.0.0.0'; } 白名单存在一个二维数组里,结构是这样的:$this->allowList['admin'] = ['192.168.1.0/24', '10.0.0.5'];。注意 CIDR 写法是支持的,但代码里没有用现成的库,而是自己写了个 ipInRange()。这个函数把掩码转成整数再比较,逻辑没问题,但有个坑:它只处理 IPv4 的 CIDR。IPv6 的 /64 这种写法进去会直接返回 false。作者注释里写了“TODO: IPv6 CIDR”,但一直没补上。
角色判断那块用的是 array_search 加 in_array 的组合。先遍历 allowList 找 IP 属于哪个角色组,找到就 break。如果同一个 IP 出现在多个角色里,只认第一个。这个设计挺糙的,但实际用起来反而省事——管理员不会把同一个 IP 塞进两个组。我见过有人改成返回所有匹配角色,结果权限判断逻辑全乱了,因为后面的代码默认 $role 是字符串,不是数组。
资源匹配用的是正则。每个角色下面挂一个 deny 和 allow 列表,先查 deny 再查 allow。deny 优先级高于 allow,这点跟 Apache 的配置逻辑反着来。作者在 README 里专门用加粗字体写了这一条,但还是有人踩坑。我调试过一个案例:用户把 /admin/* 放进 allow,又把 /admin/delete 放进 deny,以为 delete 会被放行,结果被拦了。他来找我,我说你去看源码第 87 行,if ($this->matchDeny($path)) return false; 写在 matchAllow 前面。
缓存这块值得单独说。每次请求都重新解析白名单数组太慢,所以代码里加了个 $this->cache,用 md5($ip . $path) 做键。但缓存没有过期时间,也没有清除机制。IP 变了或者白名单更新了,得手动重启 PHP-FPM。生产环境上这么干肯定不行,但作者说他的场景是内网工具,一天重启一次无所谓。我后来自己加了个 filemtime 检查,白名单文件修改时间变了就清缓存,五块钱的改动解决了大问题。
日志记录用的是 error_log(),格式是 date('Y-m-d H:i:s') . ' | ' . $ip . ' | ' . $path . ' | ' . ($result ? 'ALLOW' : 'DENY')。没有日志级别,没有请求 ID,排查问题的时候只能靠时间戳和 IP 去 grep。有一次线上出问题,两个请求间隔 0.3 秒,日志顺序反了,查了半天才发现是并发写入导致的。后来换成 file_put_contents 加 LOCK_EX 才稳定。
这套代码总共不到 300 行,没有依赖任何框架。拿来学习访问控制的基本流程没问题,但直接上生产得补不少东西。IP 伪造的问题它完全没处理——REMOTE_ADDR 在反向代理后面拿到的可能是代理 IP,得配合 X-Forwarded-For 做校验,但代码里连这个头都没读。作者说他的部署环境是直连,不需要考虑。行吧,场景决定设计。
友情链接