You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让Spring Security返回JSON格式401错误响应而非HTML?

Spring Security:将未授权请求的HTML 401响应改为指定JSON格式

现状与需求

当前使用Spring Security做权限控制时,未授权请求会返回HTML格式的401错误页面,示例如下:

<!doctype html>
<html lang="en">

<head>
    <title>HTTP Status 401 – Unauthorized</title>
    <style type="text/css">
        body {
            font-family: Tahoma, Arial, sans-serif;
        }

        h1,
        h2,
        h3,
        b {
            color: white;
            background-color: #525D76;
        }

        h1 {
            font-size: 22px;
        }

        h2 {
            font-size: 16px;
        }

        h3 {
            font-size: 14px;
        }

        p {
            font-size: 12px;
        }

        a {
            color: black;
        }

        .line {
            height: 1px;
            background-color: #525D76;
            border: none;
        }
    </style>
</head>

<body>
    <h1>HTTP Status 401 – Unauthorized</h1>
</body>

</html>

需要将其改为返回指定结构的JSON响应:

{
    "errors": [
        {
            "status": "401",
            "title": "UNAUTHORIZED",
            "detail": "xyz ..."
        }
    ]
}

现有代码

SecurityConfiguration配置类

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
   @Override
   protected void configure(HttpSecurity httpSecurity) throws Exception
   {
      // @formatter:off
      httpSecurity
               .csrf()
               .disable()
               .authorizeRequests()
               .antMatchers(HttpMethod.GET).permitAll()
               .anyRequest()
               .authenticated()
               .and()
               .httpBasic()
               .and()
               .exceptionHandling()
               .authenticationEntryPoint(new CustomAuthenticationEntryPoint());
      // @formatter:on
   }
}

CustomAuthenticationEntryPoint实现类

@Component
class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint
{
   @Override
   public void commence(HttpServletRequest request, HttpServletResponse response,
                        AuthenticationException authException) throws IOException
   {
      Collection<String> authorities = response.getHeaders("Authorization");
      response.addHeader("access_denied_reason", "authentication_required");
      response.sendError(HttpStatus.UNAUTHORIZED.value(), "Unauthorized");
   }
}

修改方案

问题核心在于原代码中response.sendError()会触发容器默认的错误页面生成逻辑,导致返回HTML。我们需要直接向响应流写入JSON数据,具体修改如下:

1. 基础修改(手动拼接JSON)

直接修改CustomAuthenticationEntryPoint的commence方法,跳过容器默认错误页面流程:

@Component
class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint
{
   @Override
   public void commence(HttpServletRequest request, HttpServletResponse response,
                        AuthenticationException authException) throws IOException
   {
      // 设置响应格式与状态码
      response.setContentType("application/json;charset=UTF-8");
      response.setStatus(HttpStatus.UNAUTHORIZED.value());
      response.addHeader("access_denied_reason", "authentication_required");
      
      // 构造符合要求的JSON字符串
      String errorJson = "{\"errors\": [{\"status\": \"401\", \"title\": \"UNAUTHORIZED\", \"detail\": \"" 
                        + authException.getMessage() + "\"}]}";
      
      // 写入响应体
      response.getWriter().write(errorJson);
      response.getWriter().flush();
   }
}

2. 优化方案(使用Jackson序列化)

如果项目中已经引入Jackson,建议用实体类序列化的方式生成JSON,避免手动拼接出错:

@Component
class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint
{
    private final ObjectMapper objectMapper;

    // 注入Jackson的ObjectMapper
    public CustomAuthenticationEntryPoint(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException
    {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.addHeader("access_denied_reason", "authentication_required");

        // 构造错误实体
        ErrorResponse errorResponse = new ErrorResponse();
        ErrorDetail errorDetail = new ErrorDetail();
        errorDetail.setStatus("401");
        errorDetail.setTitle("UNAUTHORIZED");
        errorDetail.setDetail(authException.getMessage());
        errorResponse.setErrors(List.of(errorDetail));

        // 序列化为JSON并写入响应
        objectMapper.writeValue(response.getWriter(), errorResponse);
    }

    // 定义对应实体类
    static class ErrorResponse {
        private List<ErrorDetail> errors;

        public List<ErrorDetail> getErrors() { return errors; }
        public void setErrors(List<ErrorDetail> errors) { this.errors = errors; }
    }

    static class ErrorDetail {
        private String status;
        private String title;
        private String detail;

        public String getStatus() { return status; }
        public void setStatus(String status) { this.status = status; }
        public String getTitle() { return title; }
        public void setTitle(String title) { this.title = title; }
        public String getDetail() { return detail; }
        public void setDetail(String detail) { this.detail = detail; }
    }
}

注意事项

  • 不需要修改SecurityConfiguration,原配置已经正确绑定了自定义的AuthenticationEntryPoint
  • 如果需要自定义detail字段内容,直接替换authException.getMessage()为指定字符串即可

内容的提问来源于stack exchange,提问作者emoleumassi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 23:15:44