В моем приложении весенней загрузки, которое является базовым API для отдыха, мне нужно управлять двумя видами аутентификации:
- Один на основе токена, который мне нужно проверить и извлечь userId, когда токен присутствует в заголовке http X-API-TOKEN
- Другой основан на BasicAuth, когда я получаю комбинацию заголовка "Authorization = Basic xxx" и заголовка X-API-USER-ID
Я добиваюсь реализации каждого варианта использования отдельно, но я не могу с этими двумя фильтрами настроить так.
Я использую аннотацию Pre / PostAuthorize на моих контроллерах, потому что некоторые методы ограничены, а другие разрешены всем без токена или basicAuth + uid
Вот моя конфигурация безопасности:
@Configuration
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userConnector;
@Autowired
private TokenEncoder tokenEncoder;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests().antMatchers("/").permitAll().and()
.authorizeRequests().antMatchers("/console/**").permitAll();
httpSecurity.csrf().disable();
httpSecurity.headers().frameOptions().disable();
httpSecurity.addFilterAfter(basicAuthFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(tokenFilter(), BasicAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(myExceptionHandler())
.accessDeniedHandler(myExceptionHandler());
}
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.authenticationProvider(authenticationProvider());
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider
= new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
return authProvider;
}
@Bean
public BasicAuthorizationFilter basicAuthFilter() {
return new BasicAuthorizationFilter();
}
@Bean
public MyTokenAuthorizationFilter tokenFilter() {
return new MyTokenAuthorizationFilter(tokenEncoder);
}
@Bean
public MyExceptionHandler myExceptionHandler() {
return new MyExceptionHandler();
}
@Override
@Bean
public UserDetailsService userDetailsService() {
return userConnector;
}
}
TokenFilter:
public class MyTokenAuthorizationFilter extends OncePerRequestFilter {
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
private TokenEncoder tokenEncoder;
private RememberMeServices rememberMeServices = new NullRememberMeServices();
public MyTokenAuthorizationFilter(TokenEncoder tokenEncoder) {
super();
this.tokenEncoder = tokenEncoder;
}
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if(request.getHeader("X-API-TOKEN") == null) {
filterChain.doFilter(request, response);
} else {
String uid = tokenEncoder.isValid(request.getHeader("X-API-TOKEN"));
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uid, "");
token.setDetails(authenticationDetailsSource.buildDetails(request));
Authentication authResult = authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(token);
rememberMeServices.loginSuccess(request, response, authResult);
}
filterChain.doFilter(request, response);
}
@Override
public void afterPropertiesSet() {
Assert.notNull(authenticationManager,
"An AuthenticationManager is required");
Assert.notNull(this.authenticationEntryPoint,
"An AuthenticationEntryPoint is required");
}
}
И другой фильтр:
public class BasicAuthorizationFilter extends OncePerRequestFilter {
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
private RememberMeServices rememberMeServices = new NullRememberMeServices();
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if(request.getHeader("X-API-USER-ID") == null ||
request.getHeader("Authorization") == null) {
filterChain.doFilter(request, response);
}
final String uid = request.getHeader("X-API-USER-ID");
final String basicAuthorization = request.getHeader("Authorization");
if (basicAuthorization.toLowerCase().startsWith("basic")) {
// TODO : something with username/password
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uid, "");
token.setDetails(authenticationDetailsSource.buildDetails(request));
Authentication authResult = authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(token);
rememberMeServices.loginSuccess(request, response, authResult);
}
filterChain.doFilter(request, response);
}
@Override
public void afterPropertiesSet() {
Assert.notNull(authenticationManager,
"An AuthenticationManager is required");
Assert.notNull(this.authenticationEntryPoint,
"An AuthenticationEntryPoint is required");
}
}
Я очень доволен, что эти два фильтра работают, но проблема в том, что они не связаны. Кажется, что запрос распространяется по двум фильтрам дважды, а мой ответ json удваивается, что является нормальным, согласно документации по безопасности Spring.
Как я могу выполнить каждый фильтр на основе того, что я нашел в заголовках, и если один из них успешен, пропустить секунду?