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

