给 SPA 友好的 401 和 403 响应
浏览器里的单页应用往往统一拦截 JSON 里的业务码。认证失败也应返回与业务接口一致的错误信封,前端才能用同一套逻辑弹窗或跳登录。
背景与问题
SPA 拦截器常见约定是:读 code 和 message,再决定弹窗或跳登录。ASP.NET Core 默认的 Challenge 和 Forbid 不一定走同一套序列化管道。未登录时可能拿到空 body 或重定向 HTML,前端当成网络错误;已登录无权限时,文案又和业务失败混在一起。
历史项目里还有「业务失败仍返回 HTTP 200,由信封里的码区分」的约定。网关按状态码做监控和限流时,这种约定会把认证失败统计成成功请求,告警和容量判断都会偏。
做法
自定义认证事件,重写 Challenge 和 Forbid:写出统一错误信封,文案走请求语言参数做本地化。未登录与无权限返回不同文案。OnChallenge 里要调用 HandleResponse(),否则框架可能继续写默认头或二次写 body。
csharp
options.Events = new JwtBearerEvents
{
OnChallenge = async ctx =>
{
ctx.HandleResponse();
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
ctx.Response.ContentType = "application/json";
await ctx.Response.WriteAsJsonAsync(new
{
code = 401,
message = Localize(ctx.HttpContext, "未登录或令牌已失效"),
fields = Array.Empty<string>()
});
},
OnForbidden = async ctx =>
{
ctx.Response.StatusCode = StatusCodes.Status403Forbidden;
ctx.Response.ContentType = "application/json";
await ctx.Response.WriteAsJsonAsync(new
{
code = 403,
message = Localize(ctx.HttpContext, "无权限执行此操作"),
fields = Array.Empty<string>()
});
}
};
若必须兼容旧前端,把「是否用真实 HTTP 错误码」做成配置:允许业务失败仍返回 200、由信封区分。这个行为应是显式开关,写在配置里,便于网关和前端同时知晓。
取舍
一律返回 200 会削弱网关按状态码做限流和监控的能力。新项目更适合返回真实的 401 和 403,由前端拦截器统一弹消息。本地化键缺失时要有兜底文案,避免把资源键原样甩给用户。
匿名对象在这里只用于错误信封的形状示例。若这条路径日志量很大,应换成具名错误类型,避免信息日志被编译器生成的类型名刷屏。
结语
给 SPA 的 401 和 403 应返回与业务接口同构的 JSON 信封,并区分未登录与无权限。