前端接口联调 401/403/500,快速定位与修复
本文聚焦三种最常见的 HTTP 错误码,从现象到根因,从前端到后端,给出系统化的排查路径和修复代码。前后端分离开发模式下,接口联调是最频繁的"摩擦点"。Authorization: token_abc123// 缺少 Bearer 前缀。关键词:HTTP 状态码、401、403、500、接口联调、前后端分离、JWT、CORS。// refresh 也失败了,跳转登录页。SERVER_ERROR(5
作者:洛水石
阅读时间:约 12 分钟
关键词:HTTP 状态码、401、403、500、接口联调、前后端分离、JWT、CORS
---
目录
- [引言:联调即战场](#引言)
- [401 Unauthorized:认证失败的 5 种真相](#401-unauthorized)
- [403 Forbidden:权限问题的 4 层排查](#403-forbidden)
- [500 Internal Server Error:后端锅怎么甩](#500-internal-server-error)
- [联调效率工具与技巧](#联调效率工具与技巧)
- [前后端协作规范建议](#前后端协作规范建议)
- [总结速查表](#总结速查表)
---
引言:联调即战场
前后端分离开发模式下,接口联调是最频繁的"摩擦点"。一个 401 可能浪费前端 2 小时,一个 500 可能让后端查一晚上日志。
本文聚焦三种最常见的 HTTP 错误码,从现象到根因,从前端到后端,给出系统化的排查路径和修复代码。读完这篇,联调效率提升 50%。
---
401 Unauthorized:认证失败的 5 种真相
真相 1:Token 压根没传
前端自查:
// 错误:忘记在请求头中添加 token
fetch('/api/user/info', {
method: 'GET'
// 缺少 Authorization 头!
});
// 正确:每次请求都带上 token
const token = localStorage.getItem('token');
fetch('/api/user/info', {
method: 'GET',
headers: {
'Authorization': Bearer ${token},
'Content-Type': 'application/json'
}
});
真相 2:Token 过期了
前端处理:
// axios 拦截器自动刷新 token
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// 使用 refresh token 换取新 token
const { data } = await axios.post('/api/auth/refresh', {
refreshToken: localStorage.getItem('refreshToken')
});
localStorage.setItem('token', data.token);
originalRequest.headers['Authorization'] = Bearer ${data.token};
return axios(originalRequest);
} catch (refreshError) {
// refresh 也失败了,跳转登录页
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
后端处理(Spring Boot):
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String token = getTokenFromRequest(request);
if (StringUtils.hasText(token)) {
try {
if (tokenProvider.validateToken(token)) {
Authentication auth = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (ExpiredJwtException e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\":\"Token expired\"}");
return;
}
}
chain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
真相 3:Token 格式不对
错误格式:
Authorization: token_abc123 // 缺少 Bearer 前缀
Authorization: Bearer token abc123 // 多了空格
authorization: bearer abc123 // 大小写敏感(部分服务器)
正确格式:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
真相 4:CORS 预检请求不带 Token
问题: OPTIONS 预检请求不会携带自定义头,导致后端拒绝。
后端修复(Spring Boot):
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addExposedHeader("Authorization");
config.setAllowCredentials(true);
config.setMaxAge(3600L); // 缓存预检结果 1 小时
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
真相 5:服务端重启导致 Session 失效
解决: 使用无状态的 JWT 替代 Session,或配置 Redis 共享 Session。
---
403 Forbidden:权限问题的 4 层排查
第 1 层:接口级权限
后端(Spring Security):
@RestController
@RequestMapping("/api/admin")
@PreAuthorize("hasRole('ADMIN')")
public class AdminController {
@GetMapping("/users")
@PreAuthorize("hasAuthority('user:read')")
public List<User> getUsers() {
return userService.findAll();
}
}
错误排查: 检查用户角色是否包含 ROLE_ADMIN,注意 Spring Security 默认加 ROLE_ 前缀。
第 2 层:数据级权限
@Service
public class OrderService {
public Order getOrder(Long orderId, Long currentUserId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new ResourceNotFoundException("Order not found"));
// 数据级权限校验:只能查看自己的订单
if (!order.getUserId().equals(currentUserId)) {
throw new AccessDeniedException("You can only access your own orders");
}
return order;
}
}
第 3 层:菜单/按钮级权限
前端权限指令(Vue3):
// directives/permission.js
export const permission = {
mounted(el, binding) {
const { value } = binding;
const permissions = store.getters.permissions;
if (value && value instanceof Array) {
const hasPermission = permissions.some(p => value.includes(p));
if (!hasPermission) {
el.parentNode?.removeChild(el);
}
}
}
};
// 使用
<button v-permission="['order:export']">导出订单</button>
第 4 层:IP/设备白名单
@Component
public class IpWhitelistFilter extends OncePerRequestFilter {
@Value("${security.whitelist:}")
private List<String> whitelist;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String clientIp = getClientIp(request);
if (!whitelist.isEmpty() && !whitelist.contains(clientIp)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("{\"error\":\"IP not whitelisted\"}");
return;
}
chain.doFilter(request, response);
}
}
---
500 Internal Server Error:后端锅怎么甩
第一步:先看日志,别猜
实时查看日志
tail -f /var/log/app/application.log | grep -E "ERROR|Exception"
按 traceId 追踪完整链路
grep "traceId=abc123" /var/log/app/*.log
第二步:常见 500 根因速查
|
错误类型 |
典型堆栈 |
修复方案 |
|
空指针 |
`NullPointerException` |
使用 `Optional`,入参校验 |
|
数据库连接池耗尽 |
`CannotGetJdbcConnectionException` |
增大连接池,检查慢 SQL |
|
事务超时 |
`TransactionTimedOutException` |
拆分大事务,加索引 |
|
序列化失败 |
`JsonProcessingException` |
检查循环引用,加 `@JsonIgnore` |
|
内存溢出 |
`OutOfMemoryError` |
分析 heap dump,优化缓存 |
|
第三方服务挂了 |
`ResourceAccessException` |
加熔断降级,设置超时 |
|
空指针 |
`NullPointerException` |
使用 `Optional`,入参校验 |
|
数据库连接池耗尽 |
`CannotGetJdbcConnectionException` |
增大连接池,检查慢 SQL |
|
事务超时 |
`TransactionTimedOutException` |
拆分大事务,加索引 |
|
序列化失败 |
`JsonProcessingException` |
检查循环引用,加 `@JsonIgnore` |
|
内存溢出 |
`OutOfMemoryError` |
分析 heap dump,优化缓存 |
|
第三方服务挂了 |
`ResourceAccessException` |
加熔断降级,设置超时 |
|
数据库连接池耗尽 |
`CannotGetJdbcConnectionException` |
增大连接池,检查慢 SQL |
|
事务超时 |
`TransactionTimedOutException` |
拆分大事务,加索引 |
|
序列化失败 |
`JsonProcessingException` |
检查循环引用,加 `@JsonIgnore` |
|
内存溢出 |
`OutOfMemoryError` |
分析 heap dump,优化缓存 |
|
第三方服务挂了 |
`ResourceAccessException` |
加熔断降级,设置超时 |
|
事务超时 |
`TransactionTimedOutException` |
拆分大事务,加索引 |
|
序列化失败 |
`JsonProcessingException` |
检查循环引用,加 `@JsonIgnore` |
|
内存溢出 |
`OutOfMemoryError` |
分析 heap dump,优化缓存 |
|
第三方服务挂了 |
`ResourceAccessException` |
加熔断降级,设置超时 |
|
序列化失败 |
`JsonProcessingException` |
检查循环引用,加 `@JsonIgnore` |
|
内存溢出 |
`OutOfMemoryError` |
分析 heap dump,优化缓存 |
|
第三方服务挂了 |
`ResourceAccessException` |
加熔断降级,设置超时 |
|
内存溢出 |
`OutOfMemoryError` |
分析 heap dump,优化缓存 |
|
第三方服务挂了 |
`ResourceAccessException` |
加熔断降级,设置超时 |
第三步:统一异常处理
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<?>> handleBusiness(BusinessException e) {
log.warn("Business error: {}", e.getMessage());
return ResponseEntity.ok(ApiResponse.fail(e.getCode(), e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<?>> handleUnknown(Exception e) {
String errorId = UUID.randomUUID().toString();
log.error("[{}] Unexpected error: ", errorId, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.fail(500, "System error, please contact support with ID: " + errorId));
}
}
第四步:给前端友好的错误提示
@Data
@Builder
public class ApiResponse<T> {
private int code;
private String message;
private T data;
private String errorId; // 用于后端追踪
public static <T> ApiResponse<T> fail(int code, String message) {
return ApiResponse.<T>builder()
.code(code)
.message(message)
.build();
}
}
---
联调效率工具与技巧
1. 接口文档自动生成
Spring Boot + Knife4j:
@Configuration
@EnableKnife4j
public class Knife4jConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
.title("API 文档")
.version("1.0")
.build())
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.controller"))
.paths(PathSelectors.any())
.build();
}
}
2. Mock 数据平台
前端使用 Mock.js:
import Mock from 'mockjs';
Mock.mock('/api/user/list', 'get', {
'data|10': [{
'id': '@id',
'name': '@cname',
'email': '@email',
'status|1': [0, 1]
}]
});
3. 接口对比工具
使用 diff 或专业工具(如 Apifox)对比前后端接口定义是否一致:
对比两个 OpenAPI 规范文件
npx @apidevtools/swagger-diff openapi-frontend.json openapi-backend.json
4. 网络请求拦截分析
浏览器 DevTools 技巧:
- Network 面板过滤 `status-code:401`
- 右键请求 → Copy → Copy as cURL,直接复现
- 使用 `Replay XHR` 快速重发请求
---
前后端协作规范建议
规范 1:统一响应格式
{
"code": 200,
"message": "success",
"data": {},
"timestamp": 1714992000000,
"traceId": "abc123"
}
规范 2:统一错误码
public enum ErrorCode {
SUCCESS(200, "成功"),
PARAM_ERROR(400, "参数错误"),
UNAUTHORIZED(401, "未认证"),
FORBIDDEN(403, "无权限"),
NOT_FOUND(404, "资源不存在"),
SERVER_ERROR(500, "服务器内部错误"),
SERVICE_UNAVAILABLE(503, "服务暂不可用");
private final int code;
private final String message;
}
规范 3:联调 checklist
后端提供:
- [ ] 完整的接口文档(含请求/响应示例)
- [ ] 测试环境的 Token 和测试账号
- [ ] 错误码对照表
- [ ] 日志查询权限
前端提供:
- [ ] 页面操作流程说明
- [ ] 复现问题的最小步骤
- [ ] 浏览器 Network HAR 文件
- [ ] 控制台报错截图
---
总结速查表
HTTP 状态码速查
|
状态码 |
含义 |
前端处理 |
后端处理 |
|
401 |
未认证 |
检查 token,跳转登录 |
校验 JWT/Session |
|
403 |
无权限 |
隐藏无权限按钮 |
校验角色/数据权限 |
|
404 |
资源不存在 |
检查 URL |
检查路由/Controller |
|
500 |
服务器错误 |
展示友好提示 |
查日志,抛业务异常 |
|
502/503 |
网关/服务不可用 |
提示服务维护 |
检查服务健康状态 |
|
504 |
网关超时 |
提示请求超时 |
检查下游服务超时配置 |
|
401 |
未认证 |
检查 token,跳转登录 |
校验 JWT/Session |
|
403 |
无权限 |
隐藏无权限按钮 |
校验角色/数据权限 |
|
404 |
资源不存在 |
检查 URL |
检查路由/Controller |
|
500 |
服务器错误 |
展示友好提示 |
查日志,抛业务异常 |
|
502/503 |
网关/服务不可用 |
提示服务维护 |
检查服务健康状态 |
|
504 |
网关超时 |
提示请求超时 |
检查下游服务超时配置 |
|
403 |
无权限 |
隐藏无权限按钮 |
校验角色/数据权限 |
|
404 |
资源不存在 |
检查 URL |
检查路由/Controller |
|
500 |
服务器错误 |
展示友好提示 |
查日志,抛业务异常 |
|
502/503 |
网关/服务不可用 |
提示服务维护 |
检查服务健康状态 |
|
504 |
网关超时 |
提示请求超时 |
检查下游服务超时配置 |
|
404 |
资源不存在 |
检查 URL |
检查路由/Controller |
|
500 |
服务器错误 |
展示友好提示 |
查日志,抛业务异常 |
|
502/503 |
网关/服务不可用 |
提示服务维护 |
检查服务健康状态 |
|
504 |
网关超时 |
提示请求超时 |
检查下游服务超时配置 |
|
500 |
服务器错误 |
展示友好提示 |
查日志,抛业务异常 |
|
502/503 |
网关/服务不可用 |
提示服务维护 |
检查服务健康状态 |
|
504 |
网关超时 |
提示请求超时 |
检查下游服务超时配置 |
|
502/503 |
网关/服务不可用 |
提示服务维护 |
检查服务健康状态 |
|
504 |
网关超时 |
提示请求超时 |
检查下游服务超时配置 |
联调黄金法则
- **401 先看请求头有没有 Token,再看 Token 过期没**
- **403 先确认角色权限,再确认数据权限,最后确认 IP 白名单**
- **500 先看日志,别猜,用 traceId 追踪全链路**
- **所有错误响应都要包含 errorId,方便后端定位**
- **联调前先对一遍接口文档,减少 50% 的问题**
---
更多硬核技术文章每周更新。
配图1:HTTP状态码联调排查决策树

配图2:JWT Token自动刷新机制

— 作者:洛水石 | 前后端联调 | HTTP排查 —
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)