本节摘要:授权有两道关口:请求级规则拦路径,方法级注解拦调用。本节写两类规则并解释它们的分工边界;然后讨论身份凭证怎么在多次请求间保持——会话与令牌两种机制的原理、代价与选择。
请求级规则(6.1 节)按路径粒度判断,优点是集中、一眼看全,缺点是管不到方法内部的条件判断。方法级注解补上这一层:
@Service public class DocumentService { @PreAuthorize("hasRole('EDITOR') or #doc.ownerId == principal.id") public void edit(Document doc) { // 只有编辑角色或文档本人能改:路径规则表达不了这种条件 } @PostAuthorize("returnObject.ownerId == principal.id") public Document view(Long id) { // 返回后再核对:适合"查出来才知道归谁"的场景 } }
启用需要 @EnableMethodSecurity 注解。看到 #doc、principal 这些引用就该明白了:方法级授权能访问方法参数与返回值,表达力远超路径规则。它的实现正是第 4 章的代理——注解由方法安全拦截器在代理层核对,也因此自调用时方法级注解同样失效,这条在第 4.1 节立的规矩在安全域一样有效。
分工边界我的习惯用法:路径级拦大类(哪些 URL 家族需要登录),方法级管细节(这条业务规则谁满足)。路径规则薄、方法规则厚,比把所有角色判断堆进一份集中配置可维护得多。
认证一次之后,后续请求如何证明"还是我"?两种答案,先看会话式:
http.sessionManagement(session -> session .maximumSessions(1) // 同账号最多一处在线 .maxSessionsPreventsLogin(false)); // 新登录踢掉旧会话
服务端保存会话记录,浏览器携带会话标识。优点是天然支持主动失效(踢人下线)、配置简单;代价是服务端有状态——多实例部署要共享会话存储,移动端与跨域场景下小甜饼机制别扭。
令牌式则是无状态答案:认证通过后签发一个自带签名与过期时间的令牌,之后每个请求在头部携带,服务端验签即认身份,不存任何会话。代价也直接:无法主动作废一个已签发的令牌,只能等它过期;要实现"改密码后全端下线",必须引入令牌版本号或黑名单——状态又悄悄回来了。下表把取舍摆在一起:
| 维度 | 会话式 | 令牌式 |
|---|---|---|
| 状态位置 | 服务端 | 客户端持有 |
| 主动失效 | 容易(删会话即失效) | 难(需黑名单或版本号) |
| 水平扩展 | 需共享会话存储 | 天然友好 |
| 典型场景 | 传统网页应用 | 接口服务、移动端、微服务 |

💡 选型不必二选一到底:网页端用会话、开放接口用令牌,同一应用内两套过滤器按路径分流是常见做法,配置里配多条安全链即可。
背景:内部系统里"查看工资单"接口只做了前端隐藏入口,没有服务端校验,员工改地址栏就能看别人的工资单——水平越权的教科书案例。操作三步。第一步,给接口补方法级注解:
@PreAuthorize("#userId == principal.id or hasRole('HR')") public SalaryVO salaryOf(Long userId) { return repo.findByUserId(userId); // 参数与本人一致,或 HR 角色才放行 }
第二步,用两个测试账号验证:普通员工查自己通过、查别人被拒返回 403;HR 账号查任意人通过。第三步,回归前端:隐藏入口只是体验优化,安全校验永远以服务端为准。结果:越权路径封死。解读:路径规则写不了"参数等于本人"这种条件,这正是方法级注解的存在理由;条件里引用参数与当前身份的能力,让授权从"哪类接口"细化到"哪条数据"。
变式:把注解临时删掉、只在过滤器层做登录校验,重放越权请求——请求畅通无阻,直观体会"认证不等于授权,登录不等于越权检查"。顺带一提,这类注解在单元测试里可以脱容器验证:直接调用会绕过代理,必须用安全测试工具或切片测试模拟调用,这一点在第 8 章会接上。
身份体系的演进常常走着走着就混合了:网页端会话、移动端令牌、第三方回调各来一套,此时最容易出问题的不是某一套机制,而是它们对"当前用户"的表达不一致——同一概念在三处有三种字段名与三种取值口径。治理办法是尽早统一内部的主体模型,各端机制只做"解析凭证、还原主体"这一件事。另一个常被忽视的点:退出登录在令牌体系下只是客户端删凭证,服务端毫不知情,若业务要求"退出即失效",得靠短有效期加刷新令牌的组合,把损失窗口压到分钟级。安全章节学完的标准自检:拿自己的项目回答"哪个请求未经认证、哪个数据只做了前端隐藏",两问答得出,本章就没白学。
三问收束:为什么水平越权必须靠方法级注解而路径规则无能为力;改密码后要"全端下线",令牌方案要补什么设计;自调用场景下方法注解失效的根因与第 4 章哪一节呼应。最后一问答得上来,说明全书的代理主线已经串起来了。
补一个与本节主题相关的数字感:会话的有效期、令牌的有效期、刷新窗口三者构成身份体系的时间参数,默认值往往不适配业务——支付类应用把令牌压到分钟级加刷新,内容类应用放宽到天级。上线前把这三个参数列出来过一遍,是安全评审里耗时最短、性价比最高的一项检查。