请求未到达控制器但仍获得 200 响应
Request not reaching to the controller but still get 200 response
我正在研究 spring 安全问题并试图保护 restful 应用程序,但后来 运行 陷入了这个相当荒谬的问题。我的控制器上的所有操作都很好并且请求被接受但请求实际上从未到达控制器并且总是返回 200 没有内容。
我的安全配置如下所示:
package com.bpawan.interview.api.config;
import com.bpawan.interview.api.model.Error;
import com.bpawan.interview.api.security.JWTAuthenticationFilter;
import com.bpawan.interview.api.security.JWTAuthorizationFilter;
import com.bpawan.interview.service.UserDetailService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import static com.bpawan.interview.api.security.SecurityConstants.LOGIN_URL;
import static com.bpawan.interview.api.security.SecurityConstants.SIGN_UP_URL;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class ApiSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailService userDetailService;
@Override
protected void configure(AuthenticationManagerBuilder managerBuilder) throws Exception {
managerBuilder
.userDetailsService(userDetailService)
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.authenticationEntryPoint(authenticationEntryPoint())
.and()
.addFilterBefore(corsFilter(), SessionManagementFilter.class)
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, SIGN_UP_URL).permitAll()
.antMatchers(HttpMethod.POST, LOGIN_URL).permitAll()
.antMatchers(
"/v2/api-docs",
"/configuration/ui",
"/swagger-resources/**",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**",
"/actuator/**"
).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("http://localhost:8080");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
private AuthenticationEntryPoint authenticationEntryPoint() {
return (httpServletRequest, httpServletResponse, e) -> {
var error = Error
.builder()
.message("Not authenticated")
.status(401)
.build();
var responseBody = new ObjectMapper().writeValueAsString(error);
httpServletResponse.setContentType(MediaType.APPLICATION_JSON.toString());
httpServletResponse.getWriter().append(responseBody);
httpServletResponse.setStatus(401);
};
}
private AccessDeniedHandler accessDeniedHandler() {
return (httpServletRequest, httpServletResponse, e) -> {
var error = Error
.builder()
.message("Access denied")
.status(403)
.build();
var responseBody = new ObjectMapper().writeValueAsString(error);
httpServletResponse.getWriter().append(responseBody);
httpServletResponse.setStatus(403);
httpServletResponse.setContentType(MediaType.APPLICATION_JSON.toString());
};
}
}
控制器看起来像这样:
package com.bpawan.interview.api.controller;
import com.bpawan.interview.dal.entity.Candidate;
import com.bpawan.interview.dal.repository.CandidateRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.util.List;
@RestController
@RequestMapping("api/candidate")
@RequiredArgsConstructor
@Slf4j
public class CandidateController {
private final CandidateRepository candidateRepository;
@GetMapping
public List<Candidate> getAll(Principal principal) {
log.info(principal.toString());
return this.candidateRepository.findAll();
}
@GetMapping("/{candidateId}")
public Candidate getById(@PathVariable Long candidateId) {
return this.candidateRepository
.findById(candidateId)
.orElseThrow(() -> new RuntimeException("Could not find the candidate for the provided id."));
}
@PostMapping
public Candidate addCandidate(@RequestBody Candidate candidate) {
return this.candidateRepository.save(candidate);
}
@DeleteMapping("/{candidateId}")
public void deleteCandidate(@PathVariable Long candidateId) {
this.candidateRepository.deleteById(candidateId);
}
}
授权过滤器如下所示:
package com.bpawan.interview.api.security;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import static com.bpawan.interview.api.security.SecurityConstants.HEADER_STRING;
import static com.bpawan.interview.api.security.SecurityConstants.TOKEN_PREFIX;
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain
) throws IOException, ServletException {
final var header = request.getHeader(HEADER_STRING);
if (null != header) {
final var headerContainsPrefix = header.startsWith(TOKEN_PREFIX);
if (!headerContainsPrefix) {
chain.doFilter(request, response);
return;
}
return;
}
UsernamePasswordAuthenticationToken authentication = this.getAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
final var token = request.getHeader(HEADER_STRING);
if (null != token) {
final var user = JWT
.require(Algorithm.HMAC512(SecurityConstants.SECRET.getBytes()))
.build().verify(token.replace(TOKEN_PREFIX, ""))
.getSubject();
if (null != user) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
}
我可以发出登录请求并获得有效的 Bearer 令牌,并且似乎可以使用针对数据库的身份验证。
所有执行器端点都可以正常工作,swagger 端点也能正常工作。只有我写的控制器没有。即使当我在控制器上设置断点以查看它是否真的进入那里但没有。
我觉得我可能在某个地方犯了一个愚蠢的错误。任何帮助将不胜感激。
这是我使用 logback-access 所做的请求的示例日志:
logging uri: GET /api/candidate HTTP/1.1 | status code: 200 | bytes: 0 | elapsed time: 1 | request-log: | response-log:
但是在执行器的端点上看不到跟踪actuator/httptrace
,就好像请求曾经发生过一样。
经过一段时间的努力,我发现了问题所在。我确实犯了一个愚蠢的错误。它在授权过滤器上。已更改
if (null != header) {
final var headerContainsPrefix = header.startsWith(TOKEN_PREFIX);
if (!headerContainsPrefix) {
chain.doFilter(request, response);
return;
}
return;
}
至:
if (null != header) {
final var headerContainsPrefix = header.startsWith(TOKEN_PREFIX);
if (!headerContainsPrefix) {
chain.doFilter(request, response);
return;
}
}
似乎解决了问题。
您必须将请求传递到链上,以防万一(header != null && headerContainsprefix)
我正在研究 spring 安全问题并试图保护 restful 应用程序,但后来 运行 陷入了这个相当荒谬的问题。我的控制器上的所有操作都很好并且请求被接受但请求实际上从未到达控制器并且总是返回 200 没有内容。
我的安全配置如下所示:
package com.bpawan.interview.api.config;
import com.bpawan.interview.api.model.Error;
import com.bpawan.interview.api.security.JWTAuthenticationFilter;
import com.bpawan.interview.api.security.JWTAuthorizationFilter;
import com.bpawan.interview.service.UserDetailService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import static com.bpawan.interview.api.security.SecurityConstants.LOGIN_URL;
import static com.bpawan.interview.api.security.SecurityConstants.SIGN_UP_URL;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class ApiSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailService userDetailService;
@Override
protected void configure(AuthenticationManagerBuilder managerBuilder) throws Exception {
managerBuilder
.userDetailsService(userDetailService)
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.authenticationEntryPoint(authenticationEntryPoint())
.and()
.addFilterBefore(corsFilter(), SessionManagementFilter.class)
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, SIGN_UP_URL).permitAll()
.antMatchers(HttpMethod.POST, LOGIN_URL).permitAll()
.antMatchers(
"/v2/api-docs",
"/configuration/ui",
"/swagger-resources/**",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**",
"/actuator/**"
).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("http://localhost:8080");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
private AuthenticationEntryPoint authenticationEntryPoint() {
return (httpServletRequest, httpServletResponse, e) -> {
var error = Error
.builder()
.message("Not authenticated")
.status(401)
.build();
var responseBody = new ObjectMapper().writeValueAsString(error);
httpServletResponse.setContentType(MediaType.APPLICATION_JSON.toString());
httpServletResponse.getWriter().append(responseBody);
httpServletResponse.setStatus(401);
};
}
private AccessDeniedHandler accessDeniedHandler() {
return (httpServletRequest, httpServletResponse, e) -> {
var error = Error
.builder()
.message("Access denied")
.status(403)
.build();
var responseBody = new ObjectMapper().writeValueAsString(error);
httpServletResponse.getWriter().append(responseBody);
httpServletResponse.setStatus(403);
httpServletResponse.setContentType(MediaType.APPLICATION_JSON.toString());
};
}
}
控制器看起来像这样:
package com.bpawan.interview.api.controller;
import com.bpawan.interview.dal.entity.Candidate;
import com.bpawan.interview.dal.repository.CandidateRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.util.List;
@RestController
@RequestMapping("api/candidate")
@RequiredArgsConstructor
@Slf4j
public class CandidateController {
private final CandidateRepository candidateRepository;
@GetMapping
public List<Candidate> getAll(Principal principal) {
log.info(principal.toString());
return this.candidateRepository.findAll();
}
@GetMapping("/{candidateId}")
public Candidate getById(@PathVariable Long candidateId) {
return this.candidateRepository
.findById(candidateId)
.orElseThrow(() -> new RuntimeException("Could not find the candidate for the provided id."));
}
@PostMapping
public Candidate addCandidate(@RequestBody Candidate candidate) {
return this.candidateRepository.save(candidate);
}
@DeleteMapping("/{candidateId}")
public void deleteCandidate(@PathVariable Long candidateId) {
this.candidateRepository.deleteById(candidateId);
}
}
授权过滤器如下所示:
package com.bpawan.interview.api.security;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import static com.bpawan.interview.api.security.SecurityConstants.HEADER_STRING;
import static com.bpawan.interview.api.security.SecurityConstants.TOKEN_PREFIX;
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain
) throws IOException, ServletException {
final var header = request.getHeader(HEADER_STRING);
if (null != header) {
final var headerContainsPrefix = header.startsWith(TOKEN_PREFIX);
if (!headerContainsPrefix) {
chain.doFilter(request, response);
return;
}
return;
}
UsernamePasswordAuthenticationToken authentication = this.getAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
final var token = request.getHeader(HEADER_STRING);
if (null != token) {
final var user = JWT
.require(Algorithm.HMAC512(SecurityConstants.SECRET.getBytes()))
.build().verify(token.replace(TOKEN_PREFIX, ""))
.getSubject();
if (null != user) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
}
我可以发出登录请求并获得有效的 Bearer 令牌,并且似乎可以使用针对数据库的身份验证。
所有执行器端点都可以正常工作,swagger 端点也能正常工作。只有我写的控制器没有。即使当我在控制器上设置断点以查看它是否真的进入那里但没有。
我觉得我可能在某个地方犯了一个愚蠢的错误。任何帮助将不胜感激。
这是我使用 logback-access 所做的请求的示例日志:
logging uri: GET /api/candidate HTTP/1.1 | status code: 200 | bytes: 0 | elapsed time: 1 | request-log: | response-log:
但是在执行器的端点上看不到跟踪actuator/httptrace
,就好像请求曾经发生过一样。
经过一段时间的努力,我发现了问题所在。我确实犯了一个愚蠢的错误。它在授权过滤器上。已更改
if (null != header) {
final var headerContainsPrefix = header.startsWith(TOKEN_PREFIX);
if (!headerContainsPrefix) {
chain.doFilter(request, response);
return;
}
return;
}
至:
if (null != header) {
final var headerContainsPrefix = header.startsWith(TOKEN_PREFIX);
if (!headerContainsPrefix) {
chain.doFilter(request, response);
return;
}
}
似乎解决了问题。
您必须将请求传递到链上,以防万一(header != null && headerContainsprefix)