访问控制
一个周一的上午,我收到一封企业客户的邮件:
“你好,我们的内部文档链接,不想让外部人员访问。能不能加个密码?”
另外,有些链接只想让公司内部的人访问,可以按IP限制吗?
我意识到,短链接服务已经不能只是简单的”点击即跳转”了。企业用户有强烈的安全需求——他们需要访问控制。
访问控制(Access Control)是保护资源不被未授权用户访问的机制。在短链接场景中,我需要实现多层防护:
- 密码保护:只有知道密码的人才能访问
- IP白名单:只允许特定IP地址访问
- 访问次数限制:只允许前N个人访问
- 时间窗口限制:只在特定时间段内可访问
让我逐一实现这些功能。
第 6 版:安全治理平台
短链公开传播后,审核、封禁、申诉和缓存刷新必须形成闭环。
创建
访问
治理
数据
安全治理字段
| 字段 | 类型 | 说明 |
|---|---|---|
id | BIGINT | 全局 ID |
short_code | VARCHAR(16) | 短码,唯一索引 |
long_url | VARCHAR(2048) | 原始 URL |
status | VARCHAR(20) | 跳转状态 |
risk_level | VARCHAR(20) | 风险等级 |
audit_status | VARCHAR(20) | 审核状态 |
blocked_reason | TEXT | 封禁原因 |
访问控制上线后的治理指标
secure恶意链接和异常来源
进入人工复核队列
封禁后尽快影响跳转
为什么需要密码保护?
密码保护是最基础也是最常用的访问控制方式。企业客户用它来保护:
- 内部文档链接
- 限时优惠链接
- 私密分享的文件
- 测试环境的入口
我面临的第一个问题是:如何安全地存储密码?
数据库设计
我需要在 urls 表中添加密码相关字段:
数据设计要点
urls表在这一步补上密码相关字段:密码哈希和 salt,明文密码永远不进库。- 哈希字段存的是 bcrypt 的结果和生成它的 salt,验证时用同一套参数重新计算再比对。
密码哈希:为什么不用 MD5?
在实现密码存储时,我需要考虑安全性。很多人会用 MD5 或 SHA1 来哈希密码:
落地思路
- MD5/SHA1 的设计目标是快速校验和,不是安全存储密码:算得越快,撞库越省事。
- 彩虹表是预先算好的「常见密码 → 哈希」对照表,一旦库里用的是可预测的哈希,一次查表就还原原文。
- 关键差别在「快」:快算法让攻击者一秒能试几十亿次,慢算法把每一次尝试的成本抬到不可接受。
为什么不这样做?
MD5 和 SHA1 是快速哈希算法,设计初衷是快速计算校验和,而非安全存储密码。攻击者可以:
- 彩虹表攻击:预先计算常见密码的哈希值(如 “123456”、“password”),然后用查表方式破解
- 暴力破解:因为计算速度快,一秒钟可以尝试数十亿次
使用 bcrypt:正确的做法
我选择了 bcrypt——专门为密码哈希设计的算法:
落地思路
- 用 bcrypt 存密码,数据库里只有哈希值和 salt,永远不存明文。
- 每个密码独立生成随机 salt,相同密码的哈希也不同,彩虹表直接失效。
- work factor 可调:每加 1,计算时间大约翻一倍,用来把暴力破解成本抬到可接受区间。
- 兜底是失败次数限制加递增延迟,撞库拿到的哈希再多也解不动。
bcrypt 的优势:
- 自动加盐:每次生成的哈希都不同,防止彩虹表攻击
- 计算成本可调:通过 work factor 参数控制计算时间,增加暴力破解成本
- 抗GPU攻击:算法设计使得GPU并行计算优势不明显
创建带密码的短链接
落地思路
- 创建时把密码一起提交,服务端用 bcrypt 算出哈希后随链接一起写入
urls。- 明文密码只在请求里出现一次,服务端不记录、不回显、不写日志。
- 返回给用户的是短链本身,不含密码;密码要重新设置也只能整体重写哈希,不能反查原值。
访问流程:密码验证
当用户访问带密码的短链接时,我需要:
- 检查链接是否设置了密码
- 检查用户是否已经通过验证(Session)
- 如果未验证,显示密码输入页面
- 验证密码正确性,防止暴力破解
落地思路
- 命中带密码的链接时,先看会话里有没有该短码的验证通过标记,有就直接跳 302。
- 没有就返回密码输入页,并在会话里记下尝试次数和首次尝试时间。
- 校验用 bcrypt 逐个比对哈希;同一短码连续失败达到上限就临时锁定该短码的密码尝试。
- 每次校验都要重新算一遍 bcrypt,这是有意保留的成本——它正是让暴力破解变慢的原因。
密码输入页面
落地思路
- 页面只回显短码,不回显目标长 URL——否则密码没输对就把目标地址泄露了。
- 表单只提交密码字段,不带任何隐藏的长链接参数,避免用户改参数跳过校验。
- 密码框不回显、不预填;输错只提示「密码不正确」,不区分「短码不存在」和「密码错」。
- 校验通过后才写入会话标记并 302 跳转到目标地址。
需求场景
密码保护可以防止未授权访问,但企业客户有更高的安全需求:
“我们的内部链接,只允许公司网络访问。能不能按IP限制?”
IP 白名单是更严格的访问控制方式,常用于:
- 内网资源保护:只允许公司内网IP访问
- 地域限制:只允许特定国家/地区的IP访问
- API访问控制:只允许合作伙伴服务器的IP调用
IP 地址的挑战
实现 IP 白名单有几个技术难点:
- 获取真实IP:需要处理代理、负载均衡器的情况
- IP范围匹配:单个IP不够,需要支持CIDR格式(如
192.168.1.0/24) - IPv4和IPv6兼容:需要同时支持两种协议
获取真实 IP
在 Flask 中,直接使用 request.remote_addr 可能不准确:
落地思路
- request.remote_addr 拿到的是 TCP 连接的来源 IP:前面有 Nginx 或负载均衡时,它只会是代理的地址。
- 真实客户端 IP 要从 X-Forwarded-For 这类代理头里取。
- 这类头由客户端可以伪造,只有在确认请求经过自己的 Nginx 时才可信。
正确做法是检查 X-Forwarded-For 头:
落地思路
- X-Forwarded-For 的格式是「客户端IP, 代理1, 代理2」,越靠左越接近真实来源。
- 取最左边一个作为客户端 IP;自己这一跳由 Nginx 以 X-Real-IP 补齐,避免信任头被链上其他节点伪造。
- 头缺失时退回 request.remote_addr,宁可取到代理 IP,也不要凭空造一个。
- 白名单判断前先过这一步,否则整条 IP 规则会退化成「只允许代理 IP」。
CIDR 匹配算法
企业客户不会只允许单个IP,而是整个IP段。CIDR(无类别域间路由)是标准表示法:
192.168.1.0/24:表示 192.168.1.0 到 192.168.1.255(256个IP)10.0.0.0/8:表示 10.0.0.0 到 10.255.255.255(1600万个IP)
我需要实现CIDR匹配算法:
落地思路
- 先把候选 IP 和规则里的网段都转成整数(IPv4 转无符号 32 位),再按掩码长度做按位与。
- 命中条件是 (ip & mask) == (network & mask),这样 192.168.1.0/24 能覆盖整段地址。
- IPv6 走同一套按位逻辑,只是位宽从 32 位换成 128 位。
- 规则数量不多时逐条匹配即可;规则多了再按前缀长度建索引,不必一开始就上复杂结构。
数据库设计
数据设计要点
- 核心是在
urls里保存业务事实,而不是把规则散落在应用逻辑里。ip_logs记录每次访问的判定结果,allowed字段区分放行和拦截,审计和调规则都靠它。- 索引服务于高频查询:
idx_ip_logs_short_code支撑按短码查访问明细,idx_ip_logs_time支撑按时间范围做窗口统计。- 关键字段包括
id、short_code、ip_address、access_time、allowed,它们决定了后续查询和管理能力。
IP 白名单实现
落地思路
- 白名单规则随链接配置一起存放,一条链接对应一组允许访问的网段。
- 命中任意一条即放行,全部不命中就返回拦截页——默认拒绝,不能默认放行。
- 拦截页只说明「该链接限制了访问范围」,不回显命中的具体规则。
- 每次判定结果都落一条访问记录,写明该次是放行还是拦截,后面才能审计和调规则。
创建带IP限制的短链接
落地思路
- 创建接口接收网段列表,服务端先校验每个网段格式合法(IP 合法且掩码长度在允许范围内),再落库。
- 非法的网段整单拒绝并指出是哪一条不合法,不做静默丢弃。
- 规则存成规范化的网段形式而不是原始字符串,避免同一段网因写法不同被当成两条。
- 至少留一条规则:一条没有任何允许网段的链接,等于把自己锁死。
IP 访问统计
落地思路
- 统计口径是按链接和来源 IP 聚合:放行次数、拦截次数、命中的规则分别计数。
- 明细落在
ip_logs表,字段包括id、short_code、ip_address、access_time、allowed。- 查询走
idx_ip_logs_short_code和idx_ip_logs_time两个索引,避免全表扫描。- 统计查询走异步或预聚合,绝不放在跳转路径上。
需求场景
一个营销客户找到我:
“我们要发限时优惠券,只给前100名用户。能不能实现’前100人可以访问,其他人不行’?”
这就是访问次数限制的需求:
- 限量抢购:只允许前N人访问
- 试用期限制:免费版只能访问100次
- 配额管理:防止资源被过度消耗
数据库设计
我在密码保护部分已经添加了相关字段:
数据设计要点
- 次数限制的字段和密码字段一起加在
urls上:允许的最大次数,以及当前已用次数。- 两个字段必须一起写入,避免配额和实际用量对不上,也避免默认值被后续改动带偏。
访问次数限制实现
落地思路
- 次数限制有两个字段:允许的最大次数,以及当前已用次数。
- 每次访问在同一事务里先读已用次数、判上限、再自增写入,并发下靠这层事务保证不会超发。
- 计数达到上限后的状态是「已用尽」,此时返回限制提示页而不是 302。
- 兜底是计数不能只存在缓存里:缓存重启归零就等于限次失效,计数必须落在数据库。
限制页面模板
落地思路
- 页面要说明限制原因:已用次数、允许的总次数,以及该链接是否还能继续使用。
- 不显示目标长 URL——限次链接的信息同样不该在通过校验前泄露出去。
- 提示文案给出可执行的下一步(换链接或联系创建者),而不是只说一句「访问次数已用完」。
- 页面上的计数器从数据库读,和跳转路径上的判定用同一份数据,不能各算各的。
创建限次访问的短链接
落地思路
- 创建时同时写入允许次数和已用次数(初始为 0),两个字段一并入库,避免默认值和实际配置不一致。
- 已用次数必须显式写入,不能依赖数据库默认值——默认值一旦被改动,历史链接的配额会跟着变。
- 创建后校验一次:立即访问一次,确认计数从 0 变成 1,说明计数链路是通的。
- 允许次数为 0 的链接在创建时就要拦下来,而不是创建一个天然不可用的链接。
实时访问计数查询
落地思路
- 实时计数用 Redis 计数器,数据库里的已用次数是最终事实来源。
- 每次访问先 INCR Redis 计数,再异步把增量落库;Redis 计数只作为加速读,落库失败时以数据库为准。
- 判定「是否还有剩余次数」读数据库的已用次数,避免缓存与事实不一致导致超发。
- Redis 计数器要设 TTL 并在链接失效时清掉,否则过期链接的计数会一直占着内存。
需求场景
又有一个企业客户提出需求:
“我们的内部链接,只在工作时间可以访问,下班后不行。”
这就是时间窗口限制:
- 工作时间限制:只在周一到周五的9:00-18:00可访问
- 活动时间限制:限时促销,只在一个日期范围内有效
- 试用期限制:新用户注册后7天内可访问
数据库设计
数据设计要点
- 时间窗口的字段也加在
urls上:生效起点、失效终点,以及每周生效时段的配置。- 时间统一存 UTC,比较时再按配置时区换算,避免服务器换时区后窗口整体偏移。
时间范围限制
落地思路
- 时间窗口用两个时间戳表达:生效起点和失效终点。
- 每次访问把当前时间与这两个时间戳比较:在窗口内才 302,窗口外走拦截分支。
- 两个时间戳都存 UTC,比较时统一时区,避免服务器换时区后窗口整体偏移。
- 只有起点没有终点的链接表示长期有效,终点可空但语义要明确,不能靠 NULL 隐式区分。
每周时间段限制
落地思路
- 每周时间段比时间范围多两个条件:星期几和每天的起止时刻。
- 判断顺序是先看日期是否落在指定星期,再看当前时刻是否在当天的起止区间内,两个条件都满足才放行。
- 跨零点的时段(比如 22:00–02:00)要拆成两段判断,否则凌晨那一段会被判成不在窗口内。
- 时区同样存成明确的配置项,服务器时区和用户时区不一致时按配置换算。
创建时间限制的短链接
落地思路
- 创建时把生效和失效时间一并写入
urls上的限制字段,和短码在同一次插入里落库。- 失效时间早于生效时间的配置在创建时就被拒绝,不允许写进库再靠访问时兜底。
- 到期后链接不删除,只把状态置为「已过期」——保留记录才能解释历史点击统计。
- 过期链接的访问返回过期提示页而不是 404,避免用户以为链接写错了。
完整的时间检查流程
落地思路
- 完整判断链:取链接配置 → 判断是否受限 → 未受限直接 302 → 受限则依次检查密码、IP、次数、时间四类规则。
- 每一类规则命中拦截就立即返回对应的提示页,不再往下走后面的判断。
- 所有规则都通过才返回 302,并按本次通过情况写一条访问记录。
- 判断链要在跳转路径上完成,所以每一步只能查一次已取到的数据,不能为每条规则单独回库。
我已经实现了四种访问控制方案,每种都有其适用场景:
| 方案 | 实现难度 | 安全性 | 性能影响 | 适用场景 |
|---|---|---|---|---|
| 密码保护 | ⭐⭐ 中等 | ⭐⭐⭐ 中等 | ⭐⭐ 低 | 内部文档、私密分享 |
| IP白名单 | ⭐⭐ 中等 | ⭐⭐⭐⭐ 高 | ⭐⭐ 低 | 企业内网、地域限制 |
| 访问次数限制 | ⭐ 简单 | ⭐⭐ 低 | ⭐ 低 | 限量抢购、试用版 |
| 时间窗口限制 | ⭐⭐⭐ 较难 | ⭐⭐ 低 | ⭐ 低 | 限时活动、工作时间 |
组合使用
这些方案可以组合使用:
落地思路
- 组合的顺序就是优先级:先校验密码(最便宜),再判 IP 和次数,最后才做时间窗口换算。
- 短码命中后一次取出该链接的全部限制配置,四类规则共用这份数据,避免每类规则各查一次。
- 校验通过后把结果写进会话,后续访问同一链接不必重复校验密码。
- 任一规则不通过都返回各自的提示页,但跳转响应本身的结构保持一致。
性能考虑
访问控制会增加响应延迟,我做了优化:
- 数据库索引:在
short_code上建索引,快速查询 - 缓存Session:密码验证后缓存,避免重复查询
- IP匹配优化:使用高效的CIDR匹配算法
- 异步日志:访问日志异步写入,不影响主流程
单个链接的访问控制已经实现了。但企业客户又提出新问题:
“我们有1000个内部链接,能不能批量设置IP白名单?” “每个月都要更新密码,能不能批量操作?”
单个链接管理已经不够了,我需要实现批量管理功能。
下一节,我会实现:
- 批量设置访问控制
- 批量更新密码
- 批量导入导出
- 权限组管理
(继续下一节:批量操作)
发现内容有问题?
告诉我们哪里需要改进。
