主题
用户注册流程
注册入口先过两道校验:一是注册策略——系统支持开放、邀请码、关闭三种模式,关闭模式直接 403;邀请码模式下用悲观行锁取码,依次校验有效性、过期时间、使用次数,通过后使用数加一——整个注册在一个事务里,后面任何步骤失败,次数加一也会回滚,邀请码不会被白烧。二是密码强度校验。然后查用户名重复,重复返回 400。通过后创建用户:BCrypt 加密密码、角色默认 USER,第一次落库生成用户 ID;接着创建用户的私人组织标签——PRIVATE_ 加用户名,写入组织标签表、属主是本人——这是私人空间的数据边界;随后给用户分配两个标签:系统默认组织加私人标签,并把私人标签设为主组织,第二次落库。最后把标签和主组织预热进 Redis 缓存——登录签 JWT、检索取有效标签时直接命中,不用回源。响应注册成功。
两个人同时用一个只剩一次机会的邀请码,会怎样?
不会都成功。consume 用 findByCodeForUpdate 拿的是悲观行锁——第二个事务会在取码那一步阻塞,等第一个事务提交后拿到的是已加一的 usedCount,次数校验不通过直接拒绝。加上注册本身在一个事务里、失败全回滚,所以不存在‘码被用超’或‘码被白烧’的组合。这也是为什么取码用的是带锁的专用查询而不是普通查询。
PRIVATE 标签为什么要写进组织标签表,而不是只在用户的标签串里存个字符串?
因为私人空间也需要‘资源’语义。标签表里这行记录的属主是用户本人,OrgTag 过滤器判资源权限时,PRIVATE_ 前缀 + 属主两条规则就能形成完整的私人空间闭环——其他任何人的标签集合里都不可能包含这个前缀标签,天然隔离。而且标签表让私人空间和普通组织走同一套判定代码,不需要为私人空间写特殊分支。注册时唯一要保证的是命名不冲突——用户名唯一,PRIVATE_username 就唯一。
用户登录流程
登录接口做的第一件事其实是防爆破:从代理头解析出真实客户端 IP,做 IP 级限流,超限直接 429 并告诉客户端多少秒后重试。然后是空参校验和认证:按用户名查库,BCrypt 比对密码——这里有个刻意的细节,‘用户不存在’和‘密码错误’返回完全相同的错误消息,防止攻击者靠提示枚举有效用户名。认证通过后签发双令牌:访问令牌 1 小时,里面装着 tokenId、角色、userId、组织标签、主组织——后续每个请求的权限判定都从它取,不用查库;刷新令牌 7 天,配合无感刷新机制做续期。签发的同时把 tokenId 写进 Redis 状态表——JWT 本身是无状态的,但这张表让它可撤销:登出或封禁时让 tokenId 即时失效,不用等自然过期。最后访问令牌和刷新令牌一起返回,登录成功。
JWT 是无状态的,那用户登出或者 token 被偷了怎么办?
纯无状态的 JWT 确实撤不回,所以我们给每个 token 生成唯一 tokenId 并在 Redis 里维护一张状态表,校验时先查这张表再验签名。登出就是删掉对应 tokenId——即时生效;管理员封禁账号同理。代价是每次请求多一次 Redis 查询,但它把 JWT 从‘不可撤销’变成了‘秒级可撤销’,对多租户系统这个代价必须付。另外访问令牌只有 1 小时寿命,泄露的爆炸半径被时间限制了,刷新令牌 7 天但只在刷新端点使用。
限流为什么按 IP?攻击者换 IP 呢?
IP 限流挡的是最便宜的攻击形态——单点高频爆破。从 CDN 和代理头解析真实 IP 是为了不被 X-Forwarded-For 伪造绕过。分布式慢速爆破确实能绕开单 IP 限制,所以它是纵深防御的第一层而不是唯一一层:后面还有账号维度的限流、BCrypt 的计算成本(每次校验约 100ms,爆破吞吐量被天然压死)、统一错误消息防枚举、以及 token 状态表的即时封禁能力。每层单独都可绕,叠起来攻击成本就高到不划算了。
组织标签管理流程
组织标签管理分创建、分配、使用三段。创建是管理员操作:设置名称描述,可指定父标签形成层级,还可以附带组织级上传配额;因为层级关系变了可能影响所有人,创建后会把全员的有效标签缓存清掉。分配也是管理员操作:给用户一组标签,系统做两层保护——先校验所有标签真实存在,再强制保留用户的私人标签(PRIVATE_ 加用户名),管理员传不传都会自动加回去,私人空间不可能被分配操作剥掉。落库后做精确缓存失效:只删这个用户的原始标签和有效标签两层缓存,不动别人。使用时有个展开动作:用户的有效标签 = 直接标签加上全部祖先标签再加上默认组织——递归向上收集,算完缓存起来。方向性是‘子标签继承父标签的可见性’:子部门成员天然能看父部门的文件,反之不行。这套有效标签就是检索过滤和文件列表共用的那份数据——缓存异常时兜底返回默认组织,宁可降级成只见公共空间,也不放大权限。
层级展开为什么在写入/读取时做,而不是查询文件时用 SQL 递归做?
把层级计算从每次查询挪到一次缓存构建。文件查询(列表和检索)是高频路径,带着 IN (有效标签) 一个条件就能走索引;如果查询时递归展开,每条查询都要沿 parent 链回溯组织表。代价是标签结构变更时要全员失效缓存——但标签变更是低频管理操作,用低频操作的全量失效换高频查询的零递归,这笔账是划算的。失效还分了粒度:结构变更清全员,分配变更只清单用户。
为什么 DEFAULT 标签要强制塞进有效集合,缓存坏了为什么返回 default 而不是报错?
DEFAULT 是公共空间的钥匙——强制包含保证任何用户至少能访问公开资料,注册即用零配置。计算异常时兜底返回 default 是 fail-safe 的方向选择:退化为‘只能看公共内容’是权限收敛,安全;如果兜底返回全量标签就是权限放大,绝对不行。宁可低权限报错,不冒进放权。
权限验证流程
权限验证在两个过滤器里完成,先认证后授权。第一层认证:解析 Bearer token,验证分两步——先查 Redis 里的 tokenId 状态表,再验 JWT 签名,这样登出和封禁能让令牌秒级失效,JWT 从不可撤销变成可撤销。验完还有一个特色机制:剩余寿命不足五分钟就自动签发新 token,放在 New-Token 响应头里给前端静默替换——活跃用户永不掉线;刚过期十分钟以内的还有一次宽限刷新。第二层授权分两类。功能权限看角色:SecurityConfig 的 URL 规则限定 USER 和 ADMIN 各自能到的端点。数据权限走资源级五连判:资源不存在返回 404——分片上传首传是特例,直接放行;公开或默认组织放行;属主放行;管理员放行;私人标签资源直接拒绝;最后才做用户标签与资源标签的匹配。第三层在查询里:检索的 ES 过滤和列表的 SQL 条件带着同样的三元组,越权数据在结果集里根本不出现。四层下来,从‘你是谁’到‘能到哪些端点’到‘能碰哪个资源’到‘能看到哪些数据’,粒度逐层收窄。
权限信息放在 token 里,管理员改了用户标签,要等多久生效?
分两条路。资源级授权用的 orgTags 来自 token——签发时的快照,改标签后要等 token 刷新(活跃用户几分钟内被 New-Token 换新)才带上新标签。但数据级是实时的:有效标签的权威来源在 Redis 缓存,管理员改标签时会清缓存,下次检索和列表立即按新标签过滤。所以一个用户标签被移除后,他的历史 token 短时间内还能过资源过滤器,但立刻搜不到、看不到新受限的内容——敏感方向(数据可见性)实时收敛,非敏感方向(功能入口)分钟级收敛。这是快照换性能时我会主动交代的取舍。
分片上传首传为什么放行?会不会被利用?
首传时 file_upload 记录还没建,资源级校验天然无对象可查,放行是必须的——否则上传功能整体不可用。被利用的面很窄:攻击者还是要过 JWT 认证,能传的也只进了自己的私人空间(orgTag 取自己),占用的是配额内的存储;合并时有四道复核和属主校验兜底。真正要防的是‘往别人的 fileMd5 里塞分片’——而存储路径按 md5 寻址、写入有唯一约束,塞不进去也污染不了别人的合并。