qinghuan
← 工程实践 / 全部笔记

单元 53 / 工程实践

P0 · 必会主线L1 · 基础

认证与授权:登录成功之后仍要检查每个资源

用本人、他人和管理员访问订单的例子,分清登录身份与资源权限,再理解 Session 和 JWT 的作用。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能把匿名、本人、他人与管理员的访问结果写成权限矩阵,并逐项验证。

学习理由、准备要求与知识点

为什么学它

识别用户与允许操作是两步;缺少资源所有权校验会让合法登录者访问他人的数据。

先读这些内容

知识点清单

  • 认证与授权
  • 默认拒绝
  • 角色与对象所有权
  • Session Cookie
  • JWT 签名
  • 令牌生命周期
  • CSRF 条件
  • 权限矩阵

运行前先确认完整算法使用 Java 17 标准库;JDK 源码基线为 OpenJDK 17 GA。示例是内存中的机制模型,外部服务接入需要另外配置和验证。

01先弄懂它是什么

登录成功只回答“你是谁”,访问资源还要回答“你能不能做这件事”。前者叫认证,后者叫授权。一个有效用户可以查自己的订单,并不因此可以查别人的订单。角色、所属租户、资源归属、当前状态和操作类型都可能影响决定,权限检查要覆盖实际业务入口。

会话把多次 HTTP 请求与登录结果连起来。服务端 Session 通常用难以预测的随机 ID 查状态;JWT 通常在令牌里携带带签名的声明。签名用于验证内容未被非法更改,不等于加密,不能把秘密直接放进载荷。检查 JWT 还要核对算法策略、签发者、接收方和时间条件,能解码不等于验证通过。

登录凭据不只要能创建,还要规定怎样更新、何时过期、退出后如何撤销。登录后换一个会话 ID,可以降低攻击者预先指定 ID 后继续冒用的风险;权限改变后,也要决定旧会话还能用多久。Cookie 的 Secure 通常要求通过 HTTPS 发送,HttpOnly 阻止页面脚本直接读取,SameSite 控制跨站请求是否携带它。

它们分工不同,不能替代后端权限检查;浏览器自动带凭据时,还需按场景防止其他网站诱导用户发出未授权操作,也就是 CSRF 防护。

文中这几个词,先认一认

对象授权
针对这一条具体记录检查权限,例如当前用户是否有权读取订单 101。
会话固定
让用户登录后仍沿用攻击者知道的会话 ID,使攻击者有机会继续利用这份会话。
角色继承
一个角色包含另一个角色的权限,要管理好继承关系以及权限修改后的生效方式。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

本题把每个角色“自己拥有的权限”和“从其他角色继承的权限”合在一起,得到完整权限集合,这叫权限闭包。边 A 到 B 表示 B 继承 A。计算时先处理父角色,再把它已经汇总好的权限传给子角色;拓扑排序用来保证这个先后关系。程序用一组二进制位表示有哪些权限。如果角色互相绕回去形成环,就拒绝这份配置。

例如读者有读取权限,编辑者有修改权限且继承读者,管理员继承编辑者并有管理权限,则三者权限集合依次扩大。这里使用纯允许规则;如果加入显式拒绝、资源归属或职责分离,不能继续只取并集,需要另外设计冲突优先级。

权限闭包适合预计算角色能力,但真正请求仍需检查资源和动作上下文。缓存闭包应带配置版本,角色撤权后旧缓存与会话权限快照必须按约定失效。一次角色计算正确不能保证每个Controller、导出接口和后台任务都调用了授权服务。

正在加载流程图…

图中状态只描述本篇可执行机制;业务调用仍需由外层应用负责。
查看流程图源码(Mermaid)
flowchart TD
  C["请求凭据"] --> A["认证与会话生命周期"]
  A --> I["可信身份"]
  R["版本化角色权限"] --> P["动作与对象授权"]
  I --> P
  P --> D["允许字段与业务操作"]

按顺序看,每一步做了什么

  1. 列出匿名、本人、他人、管理员对每种资源动作的规则。
  2. 使用成熟认证机制并确定会话与凭据生命周期。
  3. 检查角色继承无环,计算版本化权限集合。
  4. 在业务入口同时验证身份、角色能力和对象范围。
  5. 测试撤权、退出、跨用户访问和并发会话行为。

03对照实现代码

OpenJDK 17 SecureRandom 源码 (新标签页)↗

OpenJDK 17 GA,jdk-17+35 · SecureRandom.nextBytes

下方为原创 Java 教学片段,调用已核验的 JDK API 说明局部机制;不是该类原样源码,不代表数据库或容器内核实现。完整程序见算法模块。

核心机制代码 · java
byte[] secret = new byte[32];
new SecureRandom().nextBytes(secret); // 使用密码学随机源生成会话标识材料。
String token = Base64.getUrlEncoder().withoutPadding().encodeToString(secret); // 编码便于传输,不增加熵。
storeSessionDigest(token, authenticatedUser, expiresAt); // 服务端保存关联和期限,日志不记录原令牌。

SecureRandom通过安全提供者获得随机字节,适用于不可预测标识;普通Random或时间戳不应承担会话令牌的不可猜测要求。

三十二字节仅说明随机材料位数,实际安全还依赖随机源、传输、存储与验证。Base64是编码而非加密,也不会使弱随机输入变强。

片段中的storeSessionDigest是应用自定义接口,示意保存摘要和会话状态,不是JDK方法。生产应优先采用框架会话支持,避免自行拼装完整认证协议。

本篇完整算法计算角色权限集合,不生成真实凭据。JDK随机API为会话身份关联提供源码桥梁,不能代表Spring Security完整过滤器链与安全配置已验证。

代码先调用谁,再调用谁

  1. 认证成功得到可信用户
  2. 安全随机源产生新标识
  3. 服务端存储会话关联和期限
  4. 后续请求验证会话
  5. 业务层检查对象与动作授权

04动手写一个例子

角色继承的权限闭包

输入各角色直接权限位集合与父角色到子角色的继承边,输出每个角色有效权限;有环、非法权限位或边拒绝。Java17纯内存自测。

输入 1
读者{0},编辑{1}继承读者
输出 1
编辑{0,1}
输入 2
A继承B且B继承A
输出 2
拒绝环

为每个角色复制直接权限,避免传播修改调用者输入。入度统计尚未传播的父角色数量,只有全部父角色完成才允许该角色出队。

父角色向子角色执行位集合并集,所有祖先信息随着拓扑顺序传递。多条路径重复带来同一权限时,并集幂等,不会产生重复权限。

处理节点数少于角色数说明有环或依赖环的节点无法完成,整个配置拒绝。不能只返回已处理子图,否则缺少权限的配置可能在上线后难以诊断。

为什么这样写是对的角色出队时有效权限恰等于其直接权限与所有祖先直接权限的并集;已传播边只增加允许位,输入集合保持不变。

时间 O((V+E)·W)额外空间 O(V·W+V+E)

W为权限位集合所需机器字数,空集合时至少按一计,BitSet并集按字处理;拓扑结构线性,不能把大权限集合并集当作绝对常数。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
    static BitSet bits(int... ids){BitSet b=new BitSet();for(int id:ids)b.set(id);return b;}
    static BitSet[] permissions(BitSet[] direct,int[][] edges,int count){
        if(count<0||count>10000)throw new IllegalArgumentException("permission count");int n=direct.length;
        BitSet[] out=new BitSet[n];List<List<Integer>> g=new ArrayList<>();int[] degree=new int[n];
        for(int i=0;i<n;i++){if(direct[i]==null||direct[i].length()>count)throw new IllegalArgumentException("permission");out[i]=(BitSet)direct[i].clone();g.add(new ArrayList<>());} // 复制防止改变调用者权限。
        for(int[] e:edges){if(e.length!=2||e[0]<0||e[1]<0||e[0]>=n||e[1]>=n)throw new IllegalArgumentException("edge");g.get(e[0]).add(e[1]);degree[e[1]]++;}
        Deque<Integer> q=new ArrayDeque<>();for(int i=0;i<n;i++)if(degree[i]==0)q.add(i);int done=0;
        while(!q.isEmpty()){int u=q.removeFirst();done++;for(int v:g.get(u)){out[v].or(out[u]);if(--degree[v]==0)q.addLast(v);}} // 所有父角色都传播后才继续。
        if(done!=n)throw new IllegalArgumentException("cycle"); // 环配置整体拒绝,避免静默缺权限。
        return out;
    }
    static void eq(BitSet e,BitSet a){if(!e.equals(a))throw new AssertionError(a);}
    public static void main(String[] args){
        BitSet[] input={bits(0),bits(1),bits(2)};BitSet[] r=permissions(input,new int[][]{{0,1},{1,2}},3);eq(bits(0,1,2),r[2]);eq(bits(1),input[1]);
        eq(bits(0,1),permissions(new BitSet[]{bits(0),bits(1),bits()},new int[][]{{0,2},{1,2}},2)[2]);
        if(permissions(new BitSet[]{},new int[][]{},0).length!=0)throw new AssertionError();
        eq(bits(),permissions(new BitSet[]{bits()},new int[][]{},0)[0]);
        try{permissions(new BitSet[]{bits(),bits()},new int[][]{{0,1},{1,0}},0);throw new AssertionError();}catch(IllegalArgumentException expected){}
        try{permissions(new BitSet[]{bits(3)},new int[][]{},3);throw new AssertionError();}catch(IllegalArgumentException expected){}
        System.out.println("PASS 6 tests");
    }
}

这些特殊情况也要试一试

  • 本题只有允许权限,不包含拒绝规则。
  • 角色权限正确仍需逐资源鉴权。
  • 撤权应关联配置和会话缓存版本。
进阶选读 · 后续再读:角色爆炸与属性授权

场景角色组合过多,授权还取决于租户、归属、时间和资源状态。

做法保留有限角色能力,将对象属性和上下文交给显式策略,维护策略测试矩阵与决策日志;缓存键纳入所有影响决策的属性版本。

复杂度策略求值成本取决于规则表达能力;一般复杂逻辑组合不应直接套用本题线性图传播结论。

好处与代价更灵活策略提高表达能力,也增加解释与审计难度,默认拒绝和可追踪决策仍是基础。

05项目里怎么用

权限测试应覆盖详情、列表、修改、删除、导出和异步入口。查询可同时绑定资源ID与当前用户,返回字段再做属性范围限制;前端隐藏按钮只改善体验,攻击者可以直接构造HTTP请求,因此后端必须独立执行规则。

会话要有期限和并发规则,退出时撤销相应状态,敏感权限变化可要求重新认证。服务端 Session 便于统一撤销,但依赖存储服务;自包含令牌便于各服务自行验证,不过要快速作废,通常还得配合短有效期、撤销列表或版本检查。

浏览器自动携带Cookie的场景需要依据请求性质设计CSRF防护,跨站策略不能仅靠口头约定。使用TLS保护传输,敏感操作检查近期认证,日志只保存安全关联标识而非原令牌。默认配置必须对照实际框架版本,不能把其他大版本行为照搬。

容易踩的坑

  • 有效令牌被误当作访问任意资源的许可。
  • JWT签名被误认为载荷加密。
  • 普通随机数或时间戳直接生成会话令牌。
  • 撤权只更新角色表,旧权限缓存长期生效。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
服务端Session集中撤销和状态管理直接依赖共享存储与可用性常见浏览器应用
自包含签名令牌验证可分布执行撤销和密钥轮换更复杂有明确受众的服务调用
角色加对象属性规则可解释且控制范围细需要一致执行与缓存失效多用户和多租户资源

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01认证和授权为何分开?

展开参考答案与推导

身份真实不代表所有动作都允许,授权还依赖资源、动作和业务状态。

02角色闭包为何拒绝环?

展开参考答案与推导

本题要求可解释无环继承,环可能来自误配置;不能静默返回部分权限。

03HttpOnly防止什么?

展开参考答案与推导

限制脚本直接读取Cookie,不保证没有XSS,也不阻止浏览器自动发送凭据。

换个情况,还会处理吗

01普通用户修改URL读到他人订单,缺哪层?

展开参考答案与推导

缺对象级授权,应在资源查询或服务层绑定可信用户范围,并检查所有类似入口。

02退出后JWT仍可用,为什么?

展开参考答案与推导

签名令牌在有效期内可能仍可被独立验证,需按设计加入撤销或版本机制,删除浏览器副本并不使所有副本失效。

03管理员被撤权但后台任务仍有旧权限,怎么办?

展开参考答案与推导

检查任务是否缓存权限快照,按敏感操作要求重新校验当前授权版本与资源状态。

写代码试一试

01多父角色如何合并?

输入 / 条件
父一{0},父二{1}
输出 / 预期
子角色包含{0,1}

提示 位集合并集。

展开参考答案与推导

父角色各传播一次,入度归零后子角色才能继续向后传播。

02如何检查输入没被改?

输入 / 条件
直接权限编辑者{1}
输出 / 预期
计算后仍为{1}

提示 克隆集合。

展开参考答案与推导

断言原BitSet不变,结果集合另含继承权限,防止测试互相污染。

03显式拒绝能直接并集吗?

输入 / 条件
父允许读,子拒绝读
输出 / 预期
不能套用本题

提示 定义冲突规则。

展开参考答案与推导

需要区分允许与拒绝并规定优先级,本题只有允许位,不能宣称支持完整策略语言。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能把匿名、本人、他人与管理员的访问结果写成权限矩阵,并逐项验证。

本篇重点加练

权限关系和令牌寿命共同决定撤权能多快生效,需要检验并发会话与委托执行。

  • 角色与对象所有权
  • 令牌生命周期

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    文件服务有公开预览和私有下载接口,身份系统偶发不可用。定义认证失败、未认证与已认证无权限三类状态及默认决策。

    提交内容
    访问决策表
    怎样算完成
    • 明确失败原因和响应口径
    • 未知身份不被默认为高权限

    练到的知识点认证与授权 · 默认拒绝

  2. 作业 02代码推演

    普通用户拥有部分订单,客服可查看授权区域订单;浏览器使用 Session Cookie。构造用户、角色、对象三维样本并追踪服务端会话读取。

    提交内容
    授权矩阵与会话链图
    怎样算完成
    • 角色与所有权分别核验
    • 包含失效会话

    练到的知识点角色与对象所有权 · Session Cookie

  3. 作业 03代码推演

    一个用户通过两个角色分别获得权限,其中一条权限来自临时代理。代理到期时评审权限合成、对象范围与显式限制之间的约定。

    提交内容
    权限来源及到期矩阵
    怎样算完成
    • 区分同名权限的适用范围
    • 不能只合并字符串集合

    练到的知识点角色与对象所有权

综合应用

  1. 作业 04设计评审

    移动端使用 JWT,要求可刷新、可退出且丢失设备可撤销。评审令牌字段、签名验证和生命周期状态,不涉及真实密钥。

    提交内容
    生命周期图与字段清单
    怎样算完成
    • 区分访问令牌与刷新凭据
    • 包含时钟偏差及撤销窗口

    练到的知识点JWT 签名 · 令牌生命周期

  2. 作业 05面试场景

    同一 API 同时被浏览器 Cookie 客户端和显式 Authorization 客户端调用。建立威胁与权限矩阵,判断各请求需要哪些实际防护验证。

    提交内容
    请求类型及权限表
    怎样算完成
    • 说明凭据是否自动附带
    • 不能只按是否使用 JWT 判断风险

    练到的知识点CSRF 条件 · 权限矩阵

  3. 作业 06编程实践

    搭建本地文档 API,用测试用户执行读、改、删请求,覆盖匿名、过期凭据、跨租户和角色变化;保存决策证据。

    提交内容
    工程、请求集与访问报告
    怎样算完成
    • 仅对本地自建目标测试
    • 敏感凭据不进入日志

    练到的知识点认证与授权 · 默认拒绝 · 角色与对象所有权 · Session Cookie · JWT 签名 · 令牌生命周期 · CSRF 条件 · 权限矩阵

  4. 作业 07设计评审

    修改密码后产品要求部分设备立即退出,但正在完成的文件下载可继续。为会话、刷新令牌和在途动作分别定义撤销语义。

    提交内容
    凭据与动作生命周期
    怎样算完成
    • 覆盖刷新请求竞争
    • 说明已开始动作的处置边界

    练到的知识点令牌生命周期

  5. 作业 08编程实践

    搭建本地权限判定模型,用可控时钟测试临时代理到期、设备撤销与角色变化;按每次决策记录当时采用的权限版本。

    提交内容
    代码与时间化决策表
    怎样算完成
    • 使用虚构身份
    • 不宣称模型验证了实际 Cookie 或 JWT 中间件

    练到的知识点角色与对象所有权 · 令牌生命周期

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    管理员把一批文档转交给另一个团队,旧权限缓存尚未失效。定义生效时点和操作期间的访问规则,并设计并发验收。

    提交内容
    权限迁移方案
    怎样算完成
    • 覆盖已开始的下载与新请求
    • 区分身份稳定和权限变化

    练到的知识点默认拒绝 · 角色与对象所有权 · 令牌生命周期 · 权限矩阵

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 条件变化

    平台要求离线设备在一天内继续工作,同时管理员撤权须一分钟内生效。分析信息传递前提并提出有明确边界的产品契约。

    提交内容
    需求冲突与可验收承诺
    怎样算完成
    • 说明离线时无法获得的信息
    • 不默认所有目标可同时无条件满足

    练到的知识点角色与对象所有权 · 令牌生命周期

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗