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

Spring Boot API返回JSON而非HTML的问题求助

问题描述

我使用Spring Boot 2.7.18版本结合Spring Security搭建了一个简单应用,WebSecurityConfig配置如下:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.ldapAuthentication()
        .userDnPatterns("uid={0},ou=users")
        .contextSource(contextSource())
        .passwordCompare()
        .passwordAttribute("userPassword");
  }

  @Bean
  public DefaultSpringSecurityContextSource contextSource() {
    return new DefaultSpringSecurityContextSource("ldap://localhost:8389/dc=example,dc=com");
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.cors()
        .configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues())
        .and()
        .csrf()
        .disable()
        .authorizeRequests()
        .antMatchers(HttpMethod.OPTIONS, "/**")
        .permitAll()
        .antMatchers("/actuator/**")
        .permitAll() // Adjust permissions as needed
        .antMatchers("/v1/createUser/**")
        .permitAll() // Exclude /v1/user endpoint from authentication
        .antMatchers("/v1/login/**")
        .permitAll() // Exclude /v1/create endpoint from authentication
        .anyRequest()
        .authenticated()
        .and()
        .formLogin();
  }
}

对应的控制器方法如下:

@RestController
@RequestMapping("/v1")
public class UserController {
@GetMapping(value = "/user", produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<User> getUserByUsername(
      @RequestParam String username,
      @RequestHeader(AuthenticationControllerConstants.AUTHORIZATION_NAME)
          String authorizationToken) {
    if (jwtTokenUtil.validateToken(authorizationToken)) {
      User user = userService.findByUsername(username);
      if (user != null) {
        return ResponseEntity.status(HttpStatus.OK).body(user);
      } else {
        return ResponseEntity.notFound().build();
      }
    } else {
      return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null);
    }
  }
}

应用通过Docker部署,使用docker compose up命令启动。

遇到的问题

使用Postman请求http://localhost:8080/v1/user端点时,始终返回HTML格式的登录页面(如下所示),即使在控制器方法中添加了produces = MediaType.APPLICATION_JSON_VALUE也无法解决。希望能获取JSON格式的响应。

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

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Please sign in</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
    <link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet"
        crossorigin="anonymous" />
</head>

<body>
    <div class="container">
        <form class="form-signin" method="post" action="/login">
            <h2 class="form-signin-heading">Please sign in</h2>
            <p>
                <label for="username" class="sr-only">Username</label>
                <input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
            </p>
            <p>
                <label for="password" class="sr-only">Password</label>
                <input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
            </p>
            <input name="_csrf" type="hidden" value="407f9921-a9ee-46b4-bab7-6f50a4fb735f" />
            <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
        </form>
    </div>
</body>

</html>
解决方案

原因分析

问题核心是Spring Security的表单登录拦截优先级高于控制器逻辑:当请求未通过认证时,Spring Security会直接拦截请求,重定向到默认HTML登录页面,根本不会进入你的控制器方法。你添加的produces注解仅在控制器被调用时生效,但此时请求还没到达控制器就被拦截了。

另外,你同时配置了LDAP表单登录和JWT验证,但默认表单登录机制会接管未认证请求,导致控制器里的JWT验证代码完全没机会执行。

具体解决步骤

1. 禁用默认表单登录,添加JWT认证过滤器

既然是REST接口,不需要HTML登录页面,直接禁用表单登录,改用自定义JWT过滤器处理请求头中的Token:

修改WebSecurityConfig的configure(HttpSecurity http)方法:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.cors()
        .configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues())
        .and()
        .csrf()
        .disable()
        .authorizeRequests()
        .antMatchers(HttpMethod.OPTIONS, "/**")
        .permitAll()
        .antMatchers("/actuator/**")
        .permitAll()
        .antMatchers("/v1/createUser/**")
        .permitAll()
        .antMatchers("/v1/login/**")
        .permitAll()
        .anyRequest()
        .authenticated()
        // 禁用默认表单登录
        .and()
        .formLogin().disable()
        // 添加JWT过滤器,放在用户名密码过滤器之前
        .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
        // 配置未认证时返回JSON
        .exceptionHandling()
        .authenticationEntryPoint(authenticationEntryPoint());
}

// 注入自定义JWT过滤器
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
    return new JwtAuthenticationFilter();
}

// 配置未认证响应的入口点
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
    return (request, response, authException) -> {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        ObjectMapper mapper = new ObjectMapper();
        mapper.writeValue(response.getWriter(), Collections.singletonMap("message", "未授权,请提供有效Token"));
    };
}

2. 实现JWT认证过滤器

创建JwtAuthenticationFilter类,在过滤器层面完成Token验证,避免在控制器中重复处理:

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authorizationHeader = request.getHeader(AuthenticationControllerConstants.AUTHORIZATION_NAME);

        String username = null;
        String jwtToken = null;

        // 提取Bearer Token
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwtToken = authorizationHeader.substring(7);
            username = jwtTokenUtil.extractUsername(jwtToken);
        }

        // 验证Token并设置认证上下文
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            } else {
                // Token无效时直接返回401 JSON响应
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                ObjectMapper mapper = new ObjectMapper();
                mapper.writeValue(response.getWriter(), Collections.singletonMap("message", "Token无效"));
                return;
            }
        }

        filterChain.doFilter(request, response);
    }
}

3. 简化控制器逻辑

现在Token验证已经在过滤器中完成,控制器无需再处理验证逻辑,可以简化为:

@GetMapping(value = "/user", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<User> getUserByUsername(@RequestParam String username) {
    User user = userService.findByUsername(username);
    return user != null ? ResponseEntity.ok(user) : ResponseEntity.notFound().build();
}

额外说明

  • 确保你的JwtTokenUtil工具类能正确提取用户名、验证Token有效性
  • 如果需要保留LDAP认证,可以在/v1/login接口中实现:接收用户名密码,通过LDAP验证后生成JWT返回给前端

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 21:58:08