作者:洛水石

阅读时间:约 12 分钟

关键词:HTTP 状态码、401、403、500、接口联调、前后端分离、JWT、CORS

---

目录

  1. [引言:联调即战场](#引言)
  2. [401 Unauthorized:认证失败的 5 种真相](#401-unauthorized)
  3. [403 Forbidden:权限问题的 4 层排查](#403-forbidden)
  4. [500 Internal Server Error:后端锅怎么甩](#500-internal-server-error)
  5. [联调效率工具与技巧](#联调效率工具与技巧)
  6. [前后端协作规范建议](#前后端协作规范建议)
  7. [总结速查表](#总结速查表)

---

引言:联调即战场

前后端分离开发模式下,接口联调是最频繁的"摩擦点"。一个 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

网关超时

提示请求超时

检查下游服务超时配置

联调黄金法则

  1. **401 先看请求头有没有 Token,再看 Token 过期没**
  2. **403 先确认角色权限,再确认数据权限,最后确认 IP 白名单**
  3. **500 先看日志,别猜,用 traceId 追踪全链路**
  4. **所有错误响应都要包含 errorId,方便后端定位**
  5. **联调前先对一遍接口文档,减少 50% 的问题**

---

更多硬核技术文章每周更新。

配图1:HTTP状态码联调排查决策树

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

— 作者:洛水石 | 前后端联调 | HTTP排查 —

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐