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」的学习范围组织,正文与示例均为本站原创整理。