Spring Security 中认证和授权有什么区别?
简化版
认证(Authentication)解决「你是谁」——确认请求来自哪个用户(校验用户名密码、Token 等)。授权(Authorization)解决「你能做什么」——已确认身份的用户能不能访问某个资源。顺序上先认证后授权:Spring Security 先通过认证得到一个 Authentication 对象(存进 SecurityContext),再基于其中的权限(authorities)+ 角色 + 表达式规则做访问决策。HTTP 语义:未认证返回 401,已认证但权限不足返回 403。核心易错点:「已经登录」不等于「允许操作」——很多越权漏洞就是把认证当成了授权。
详细版
认证 vs 授权对比:
| 维度 | 认证(Authentication) | 授权(Authorization) |
|---|---|---|
| 回答 | 你是谁 | 你能做什么 |
| 时机 | 先 | 后(依赖认证结果) |
| 手段 | 用户名密码、短信、OAuth2、JWT | 角色/权限、@PreAuthorize、URL 规则 |
| 结果 | 得到 Authentication | 允许/拒绝访问 |
| HTTP | 失败 → 401 | 不足 → 403 |
Authentication 对象的核心字段:
public interface Authentication {
Object getPrincipal(); // 用户主体(如 UserDetails)
Object getCredentials(); // 凭证(密码等,认证后常被擦除)
Collection<? extends GrantedAuthority> getAuthorities(); // 权限集合
boolean isAuthenticated(); // 是否已认证
}
授权可发生在多层:
// ① URL 授权(接口入口)
http.authorizeHttpRequests(a -> a
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated());
// ② 方法授权(服务层)
@PreAuthorize("hasAuthority('order:refund')")
public void refund(Long orderId) { ... }
// ③ 数据授权(只能看自己的数据)——需业务查询条件配合
完整版教学
一、先分清两个问题
- 认证是「身份确认」:证明这个请求属于某个已知用户(张三登录了)。
- 授权是「权限判断」:这个已知用户能不能做某件事(张三能不能退款)。
关键认知:登录成功只证明「请求属于某个主体」,不代表「它能访问所有接口」。很多安全漏洞的根源就是把「已经登录」误当成「允许操作」——比如普通用户登录后,直接调用管理员接口,如果只检查了「是否登录」而没检查「是否有权限」,就发生越权。认证是门槛,授权是分权,两者缺一不可。
| 问题 | Spring Security 产物 | 失败时 HTTP 语义 | 典型风险 |
|---|---|---|---|
| 你是谁 | Authentication / SecurityContext | 401 | 未登录、Token 无效 |
| 你能做什么 | GrantedAuthority / 授权规则 | 403 | 已登录但越权 |
这题的记忆钩子是“先认人,再分钱”:认证确认主体,授权决定这个主体能操作哪些资源。
二、Authentication 里有什么
认证成功后,Spring Security 用一个 Authentication 对象表示当前用户,存在 SecurityContext 里,核心字段:
principal:用户主体(通常是UserDetails,含用户名、账号状态等)。credentials:凭证(如密码)。认证成功后通常被擦除(置空)——避免敏感凭证长期留在内存里被窃取。authorities:授予的权限集合(GrantedAuthority),授权就靠它。authenticated:是否已认证。
后续任何地方都能通过 SecurityContextHolder.getContext().getAuthentication() 拿到当前用户。
三、认证流程的位置
认证发生在过滤器链里(见「过滤器链」专题):
- 请求进入过滤器链,认证过滤器提取凭证(表单的用户名密码、请求头的 Token)。
- 交给
AuthenticationManager,它委托给合适的AuthenticationProvider校验(如DaoAuthenticationProvider查数据库比对密码)。 - 校验成功 → 构造已认证的
Authentication,写入SecurityContext。 - 之后授权组件和业务代码才能知道「当前用户是谁、有什么权限」。
必须先完成认证、写入 SecurityContext,授权才有依据。
请求
-> 认证过滤器提取凭证
-> AuthenticationManager 校验
-> SecurityContext 保存 Authentication
-> 授权规则读取 authorities 决策
四、授权可以发生在多层
授权不是只在一个地方做,可以分层防御:
- URL 授权(接口入口层):
requestMatchers("/admin/**").hasRole("ADMIN")——控制哪些路径需要什么权限。适合粗粒度的入口控制。 - 方法授权(服务层):
@PreAuthorize("hasAuthority('order:refund')")——保护具体的服务方法。即使 URL 层漏了,方法层还能兜住。 - 数据授权(最细):「只能查看自己部门/自己的数据」——这种不能只靠 URL/方法规则,必须在业务查询里加条件(
where user_id = 当前用户),因为它依赖具体数据的归属。
多层授权能形成纵深防御,尤其数据级授权是很多越权漏洞的重灾区。
五、401 和 403 的区别(要区分清楚)
- 401 Unauthorized:没有有效认证——没登录、Token 过期/无效。语义是「你需要先登录/重新提交凭证」。
- 403 Forbidden:身份已确认,但权限不足——你登录了,但这个操作你没权限。语义是「你是谁我知道,但你不能做这个」。
API 设计中区分二者很重要:前端拿到 401 应该跳转登录(重新认证),拿到 403 应该提示「无权限」(不用重新登录)。混淆会导致「权限不足却一直弹登录框」之类的糟糕体验,也不利于排查。
六、角色和权限的建模
- 角色(Role) 通常是权限的集合,粗粒度:
ADMIN、USER、MANAGER。 - 权限(Authority/Permission) 是具体的动作,细粒度:
order:read、order:refund、user:delete。
系统越复杂,越应该让授权逻辑面向「权限点」而非「角色名」。原因:如果代码里到处硬编码 hasRole("ADMIN"),将来新增一个「财务」角色也需要退款权限时,要改所有相关代码。而如果代码写 hasAuthority('order:refund'),只要把这个权限点分配给财务角色即可,代码不用动。角色管「分给谁」,权限点管「能做什么」,代码依赖权限点更灵活(这是 RBAC 的核心,见相关专题)。
七、测试授权规则(要测反向场景)
安全测试不能只测「有权限的能访问」,更要测反向场景:
- 未登录访问 → 应 401。
- 登录但权限不足访问 → 应 403。
- 权限正确访问 → 成功。
- 跨租户/跨用户访问(张三尝试访问李四的数据)→ 应被拒绝。
「管理员能访问」测了没用——关键是**「普通用户不能越权」。越权漏洞恰恰藏在「没测反向场景」里。安全测试的重点是证明该拒绝的确实被拒绝了**。
八、常见误区与追问
- 误区:用户已经登录就可以访问所有接口。 登录只说明身份被确认,是否能访问还要看角色、权限点和数据范围。
- 误区:认证失败和授权失败都返回同一个状态码即可。 未认证应返回 401,已认证但权限不足应返回 403,前端和排障语义都不同。
- 误区:只做 URL 授权就能覆盖所有越权。 服务方法可能被多个入口调用,数据级权限还要在业务查询条件里落实。
- 追问:
Authentication中哪些字段和授权最相关?principal表示主体,authorities表示权限集合,授权决策主要依赖这些信息。 - 追问:为什么复杂系统更推荐判断权限点而不是角色名? 权限点表达具体动作,新增角色时只改角色-权限关系,不需要到处改代码里的
hasRole。 - 追问:数据授权为什么不能只靠
@PreAuthorize? 数据归属依赖具体记录,如租户、部门、创建人,必须进入查询条件或业务校验。
九、加强记忆
认证(Authentication)= 确认「你是谁」(用户名密码/短信/OAuth2/JWT,成功得到 Authentication 存入 SecurityContext,凭证认证后擦除);授权(Authorization)= 判断「你能做什么」(基于 authorities/角色/表达式)。先认证后授权——「已登录」≠「允许操作」,混淆导致越权。授权分层:URL 授权(入口)、方法授权(@PreAuthorize)、数据授权(业务查询加条件,最细)。401(未认证,需登录)vs 403(已认证但权限不足) 要区分(前端处理不同)。建模:角色是权限集合、权限点是具体动作,复杂系统代码依赖权限点(order:refund)而非角色名,更灵活。安全测试必须覆盖反向场景(未登录 401、越权 403、跨用户拒绝)——重点是「该拒绝的被拒绝」。