Spring Security 导致 POST 请求返回 HTTP 405
结论
/salecomplete 的 POST 映射大概率没有问题。内部请求能够成功,说明 Spring MVC 可以找到对应的处理方法。
问题通常在 Spring Security 层。银行回调是来自外部、未经认证的跨站 POST 请求,不会携带系统页面生成的 CSRF Token,也可能没有有效的登录会话。请求被安全过滤器拦截后,可能又被转发到登录页、拒绝访问页,或者其他只支持 GET 的地址,最终由 Spring MVC 报出:
Request method 'POST' not supported
因此,405 通常只是表面现象。真正需要排查的是认证、CSRF 和安全异常的处理流程。
为什么内部 POST 正常
系统内部提交一般具备这些条件:
- 浏览器已经登录,并携带有效的 Session Cookie。
- 表单包含 Spring Security 生成的 CSRF Token。
- 请求来源、路径和安全规则符合预期。
银行 IPG 回调的情况不同:
- 回调由银行页面或银行服务器发起。
- 银行无法获取系统生成的 CSRF Token。
- 回调不应依赖用户是否仍处于登录状态。
- 跨站
POST还可能受到 Cookie 的SameSite属性影响。 - 银行可能通过浏览器表单回传,也可能由服务器直接调用,具体方式需要查看银行接口文档。
所以,同一个 Controller 地址能在系统内部访问,不代表外部回调也能通过 Spring Security。
安全配置方式
支付回调地址通常需要允许匿名访问,并且只对这个地址忽略 CSRF 检查。不要因为一个支付回调而全局关闭 CSRF。
Spring Security 6 / Spring Boot 3 风格的配置示例:
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.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/salecomplete").permitAll()
.anyRequest().authenticated()
)
.csrf(csrf -> csrf
.ignoringRequestMatchers("/salecomplete")
);
return http.build();
}
}
如果项目使用较早版本的 Spring Security,可以采用等价配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/salecomplete").permitAll()
.anyRequest().authenticated()
.and()
.csrf()
.ignoringAntMatchers("/salecomplete");
}
XML 配置可以根据项目版本使用类似规则:
<http>
<intercept-url pattern="/salecomplete" access="permitAll"/>
<csrf request-matcher-ref="csrfRequestMatcher"/>
</http>
各个 Spring Security 版本支持的 API 并不完全相同,应以项目实际使用的版本为准。
Controller 建议
可以继续使用 POST 映射,但最好显式声明参数,也不要仅凭浏览器会话确认订单:
@PostMapping("/salecomplete")
public String salecomplete(
@RequestParam("orderId") String orderId,
@RequestParam("status") String status,
@RequestParam("signature") String signature,
Model model) {
// 1. 根据银行协议验证 signature
// 2. 向银行服务端查询或验证交易状态
// 3. 校验金额、商户号、订单号和交易号
// 4. 以幂等方式更新订单状态
model.addAttribute("orderId", orderId);
return "salecomplete";
}
如果银行要求返回固定文本,而不是渲染页面,可以使用:
@PostMapping("/salecomplete")
@ResponseBody
public String salecomplete(HttpServletRequest request) {
// 验签、核对交易并更新订单
return "OK";
}
具体返回内容必须符合银行 IPG 的协议,不能自行认定银行会接受 "OK"。
如何确认请求在哪里被拦截
先在浏览器开发者工具的 Network 面板中查看完整的请求链,重点检查:
- 银行实际提交的地址是不是
/salecomplete。 - 请求是否先收到
302,然后跳转到登录页。 - 405 对应的地址是
/salecomplete,还是/login、/error、/access-denied等其他地址。 - 请求地址是否包含应用所需的 context path。
- 回调过程中是否发生了 HTTP 到 HTTPS、域名或端口跳转。
- 表单的
method是否确实为POST。
也可以临时开启安全日志:
logging.level.org.springframework.security=DEBUG
logging.level.org.springframework.web=DEBUG
如果日志显示 CSRF 校验失败,常见响应通常是 403 Forbidden。现在返回 405,可能是因为失败请求又被自定义的 AccessDeniedHandler、错误页或转发规则送到了一个不接受 POST 的 Controller。需要检查这类配置:
.exceptionHandling(exception -> exception
.accessDeniedPage("/access-denied")
)
同时确认 /error、/login 和 /access-denied 对应的映射是否只支持 GET。
支付回调的安全注意事项
允许匿名访问并忽略 CSRF,不等于可以直接信任回调中的参数。支付回调必须使用银行协议规定的安全校验机制,例如:
- 验证数字签名、MAC 或银行提供的 Token。
- 核对商户号、订单号、支付金额和币种。
- 必要时主动调用银行查询接口,确认交易状态。
- 使用银行交易流水号进行幂等处理。
- 防止同一个回调被重复提交。
- 不要仅凭请求中的
status=success就把订单标记为已支付。 - 不要依赖当前登录用户或浏览器 Session 判断订单归属。
可以先把 /salecomplete 配置为允许匿名访问,并且只对这个回调路径忽略 CSRF。然后结合完整请求链和 Spring Security 日志,继续排查登录跳转、错误转发或回调 URL 不准确等问题。
备注:内容仅供参考。
