← 返回 Java 后端知识路线
阶段 04Spring 与后端

Spring Security、JWT、CSRF 与 CORS:把凭证和浏览器边界分开

用无状态 API 配置理解 SecurityFilterChain、JWT 验证、CSRF 与跨域的不同责任,不把安全概念混成一个开关。

第 31 / 35 篇
Spring SecurityJWTCSRFCORS认证

先看这一课值不值得学

学完后,你手里多了哪些代码积木

在请求进入业务前完成身份验证、授权和浏览器安全控制。

本课正式新增

语法 / API / 命令你必须会到什么程度
SecurityFilterChain配置 Spring Security 过滤链
Authentication / GrantedAuthority表达当前身份和权限
JWT claims / signature签发并验证令牌
csrf / cors控制跨站请求与跨域访问

本课只借用,先别硬背

  • Refresh Token 数据模型只在项目落点展开

学完必须能独立写

  • 实现受保护的文章管理接口
  • 区分 401、403、CSRF 和 CORS 失败
本课目录
  1. 1. 现实问题:把 JWT 放进请求并不等于完成安全
  2. 2. 最小可运行示例:资源服务器验证 Bearer JWT
  3. 3. 调用链与对象变化
  4. 4. 为什么这样设计
  5. 5. 项目落点:认证只给身份,业务决定资源权限
  6. 6. 易错排查
  7. 7. 一页复习

1. 现实问题:把 JWT 放进请求并不等于完成安全

登录成功后客户端得到 token,后续请求带上 Authorization: Bearer ...,服务验证签名、过期时间和权限。浏览器跨域还会触发 preflight,Cookie 场景要防 CSRF。JWT、CSRF、CORS 处理的是不同问题:认证“你是谁”,授权“能做什么”,CSRF“浏览器是否被诱导带上凭证”,CORS“浏览器是否允许脚本读取跨域响应”。

下面只展示语义一致的 SecurityFilterChain 配置片段,不在课程仓库放真实密钥。Spring Security 7 的当前 API 与 Boot 4.1 依赖由官方文档和项目 BOM 约束,Java 21 作为运行基线。

2. 最小可运行示例:资源服务器验证 Bearer JWT

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
class SecurityConfig {
    @Bean
    SecurityFilterChain api(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .cors(cors -> {})
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health", "/api/auth/login").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .oauth2ResourceServer(oauth -> oauth.jwt(jwt -> {}));
        return http.build();
    }
}

csrf().disable() 只适合经过审查的纯 Bearer Header、无浏览器 Cookie 会话的 API,并不适合所有 Web 应用。若认证凭证放在 Cookie,保留 CSRF 防护或采用明确的 token 传递方案;CORS 允许的 origin、method、header 要列白名单,不能用 * 配合凭证。

3. 调用链与对象变化

请求先进入 SecurityFilterChain,BearerTokenAuthenticationFilter 从 Authorization 头提取 token,JwtDecoder 验证签名、issuer、audience 和时间,成功后创建 Authentication 放进 SecurityContext。后续授权规则读取角色/权限,Controller 看到的是已认证上下文而不是原始 token 字符串。

浏览器跨域时先发送 OPTIONS preflight,CORS 配置决定是否返回允许的 origin 和 header;真正请求再携带 Bearer。CSRF 检查的是修改请求是否带有服务器期望的 CSRF token,和 JWT 载荷的签名无关。响应离开过滤器链时,未认证通常 401,已认证但无权限 403。

4. 为什么这样设计

JWT 是签名的声明,不是加密的密码;不要把秘密、完整个人信息或可随意修改的权限放进 payload。验证签名和标准声明后,还要在 Service 层检查资源归属和业务权限。无状态不等于无撤销,短过期、刷新令牌、黑名单或密钥轮换要按威胁模型选择。

CSRF 利用浏览器自动携带 Cookie,Bearer Header 通常需要脚本主动设置,因此风险模型不同。CORS 只是浏览器读取策略,不是服务器授权;curl 不受 CORS 限制,但仍必须通过 Security。配置任何一个开关前都要画凭证从哪里产生、保存、发送和失效的时序。

5. 项目落点:认证只给身份,业务决定资源权限

博客系统允许公开读取已发布文章,草稿只允许作者和管理员;Security 先识别用户,ArticleService 再检查 article.authorId 和权限。登录接口限流,密钥来自环境或密钥系统,日志只记录 subject、issuer 和结果摘要。前端把 401 处理为重新登录,把 403 处理为无权限提示。

练习:为 CORS 配置一个本地前端 origin 和一个被拒绝 origin,验证 preflight;为 /api/drafts/** 写作者权限测试,验证伪造 userId 不能越权。再过期 JWT 并观察过滤器日志与响应状态,确认未认证请求不会进入业务。

6. 易错排查

  • CORS 配了仍失败:看 OPTIONS 是否被安全链拦截、origin/headers 是否精确匹配、是否携带 credentials。
  • JWT 能解码但被拒:检查 issuer、audience、签名算法、时钟偏差和 authority 映射。
  • 把 403 当登录失败:区分认证失败和授权失败,前端提示不能混淆。
  • 全局禁用 CSRF:先确认凭证不是 Cookie,且没有浏览器会话;否则会留下真实漏洞。

7. 一页复习

SecurityFilterChain 先取凭证、验证 JWT、建立 Authentication,再做请求授权;CORS 管浏览器跨域读取,CSRF 管浏览器自动带凭证的伪造请求。JWT 不替代资源权限和密钥治理。安全配置必须用真实请求、401/403、preflight 和越权测试证明。

凭证状态可以画成一条链:登录接口验证密码并签发 token,客户端保存并在请求头带出;过滤器拿到原始字符串,Decoder 验证签名和标准声明,Authentication 保存 subject/authorities,授权规则决定是否放行;Controller 只看到认证上下文。token 过期、issuer 不匹配、签名 key 不对和权限不足是不同输出,分别对应 401 或 403。

项目文件可把 SecurityConfig 放 config,JWT decoder/issuer 放 secret/config,资源权限判断放 application service,登录与刷新放 auth 模块。CORS 的 allowed origins/methods/headers 由环境配置白名单,CSRF 策略按 Cookie 或 Bearer 的真实凭证存储决定。SecurityFilterChain 负责入口安全,但文章作者归属仍要在 Service 用 article.authorId 比较。

排错时浏览器 preflight 失败查 OPTIONS 是否被 Security 拒绝和响应头,token 能解码却 401 查 issuer/audience/时钟,认证成功却 403 查 authority 前缀映射,跨域 curl 成功但浏览器失败查 CORS 而非数据库。CSRF 全局关闭前要证明没有 Cookie 会话;伪造 userId 的请求要专门测试,不能把 token subject 当作所有权限。

练习是建立公开文章、作者草稿和管理员接口三个请求样例,分别记录无 token、过期 token、正确作者、错误作者和 admin 的状态码;再用浏览器发送 preflight 和真实 POST,观察请求头、响应头和过滤器日志。最后轮换测试 key,确认旧 token 的失效策略有证据。

安全请求的对象变化是:原始 Authorization 头是字符串,Bearer 解析器得到 token,JwtDecoder 验证后得到 claims,转换器把 scope/role 变成 authorities,SecurityContext 保存 Authentication,授权器再决定是否进入 Controller。token 不能直接作为 actor id,Service 还要读取 subject、租户和资源归属并做业务检查。

凭证存储决定 CSRF:浏览器自动带 Cookie 时,恶意页面可能诱导修改请求,需保留 CSRF token/同源策略;Bearer 放在脚本主动设置的 Header 中,风险不同,但 XSS、token 泄露和刷新治理仍然存在。CORS 只允许浏览器脚本读取响应,不会替服务器授权,curl 成功不能证明浏览器跨域配置正确。

项目文件可把 SecurityFilterChain 和 CORS 配置放 config,JWT issuer/key 属性放 security properties,登录/刷新放 auth,资源作者权限放 application。允许公开的 health、登录和公开文章应逐条列出,管理员路径和草稿路径用 role/authority 规则保护。密钥轮换、过期、撤销和 refresh token 不应该靠一个永久 secret。

排错时 OPTIONS 被拒查 preflight 是否在 Security 前得到响应,token 401 查签名/issuer/audience/时钟,认证成功 403 查 authority 前缀,作者越权查 Service 是否只信 subject,Cookie 会话禁用 CSRF 查真实凭证模式。记录 subject、issuer、结果和 traceId,不记录 token 原文。练习是为公开读、作者写、管理员写和过期 token 各保存请求/响应/过滤器证据。

验证清单:为公开文章、作者草稿、管理员接口各准备无 token、过期 token、签名错误 token、正确作者、错误作者和管理员六类请求,记录 Authorization 是否存在、JwtDecoder 结果、Authentication subject/authority、Service 资源判断和最终 401/403/2xx。再用浏览器发送 OPTIONS 与真实 POST,保存 Origin、Access-Control-Request-Headers、Allow-Origin、Allow-Credentials 和安全链日志;curl 成功只能证明服务端收到了请求,不能证明浏览器允许脚本读取。

对象变化复盘要按凭证边界进行:Authorization 头是原始字符串,Bearer 解析得到 token,解码器得到受验签的 claims,转换器生成 authorities,SecurityContext 保存 Authentication,授权器才决定是否进入 Controller。进入 Service 后还要读取租户和资源 owner,不能把 subject 直接当作可编辑 articleId。Cookie 自动携带时保留 CSRF 防护;Bearer 主动设置 Header 的风险模型不同,但 XSS、泄露、刷新和撤销仍需治理。

安全验收还应覆盖配置轮换:换 key 或 kid 后旧 token 的结果要符合失效策略,issuer/audience 错误不能降级为匿名,权限前缀映射要在测试中锁定。记录 subject、issuer、authority、decision 和 traceId,永远不记录 token 原文。若 OPTIONS 被拒绝,先定位 SecurityFilterChain 与 CORS 处理顺序;若认证成功却 403,先查 authority 映射和 Service owner 判断。

再做一次安全验收:同一资源分别用公开读、作者写、错误作者写和管理员写请求,记录 token claims、Authentication、资源 owner、authority 和最终数据库副作用;错误作者必须在 Service 归属判断处失败,即使过滤器已经认证成功。浏览器端把 preflight、真实请求、Cookie/Bearer 和响应头分开保存,确认 CORS 放行不等于授权放行,CSRF 策略也与实际凭证存储方式一致。

进阶附录:刷新令牌与密钥轮换

刷新令牌应有更严格存储、轮换和撤销策略,访问令牌短时有效。JWKS 轮换时资源服务器要缓存公钥并处理 kid 变化,不能每次请求无界访问远程地址。安全事件中要能按 subject、token id 或 key version 追踪和失效。

本课按「Spring Security 7 / Spring Boot 4.1 JWT、CSRF 与 CORS」的学习范围组织,正文与示例均为本站原创整理。