Мои фильтры аутентификации не срабатывают по запросу.
У меня есть 2 конфигурации безопасности: одна только для конечной точки входа в систему с аутентификацией с помощью фильтра AuthenticationFromCredentialsFilter, а другая для других конечных точек с аутентификацией с помощью фильтра AuthenticationFromTokenFilter.
Я ожидаю, что будет вызван метод фильтров attemptAuthentication, но это не так.
Есть ли смысл в предпочтении аутентифицировать учетные данные и создавать токен в фильтре, а не в контроллере входа в систему?
Контроллер входа в систему пока присутствует, но он не должен существовать, поскольку его работа должна выполняться фильтром.
Я настроил их каждый в конфигурации безопасности:
@EnvProd
@EnableWebSecurity
@ComponentScan(nameGenerator = PackageBeanNameGenerator.class, basePackages = { "com.thalasoft.user.rest.security", "com.thalasoft.user.rest.filter" })
public class WebSecurityConfiguration {
@Order(1)
@Configuration
public class CredentialsConfiguration extends WebSecurityConfigurerAdapter {
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
public AuthenticationFromCredentialsFilter authenticationFromCredentialsFilter() throws Exception {
AuthenticationFromCredentialsFilter authenticationFromCredentialsFilter = new AuthenticationFromCredentialsFilter();
authenticationFromCredentialsFilter.setAuthenticationManager(authenticationManagerBean());
return authenticationFromCredentialsFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/api/users/login")
.addFilterBefore(authenticationFromCredentialsFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/api/users/login").permitAll()
.anyRequest().authenticated();
}
}
@Order(2)
@Configuration
public class TokenConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationFromTokenFilter authenticationFromTokenFilter;
@Autowired
private RESTAuthenticationEntryPoint restAuthenticationEntryPoint;
@Autowired
private SimpleCORSFilter simpleCORSFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable();
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.headers()
.cacheControl().disable()
.frameOptions().disable();
http
.httpBasic()
.authenticationEntryPoint(restAuthenticationEntryPoint);
http
.addFilterBefore(simpleCORSFilter, UsernamePasswordAuthenticationFilter.class);
http
.addFilterBefore(authenticationFromTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.antMatcher("/api/**")
.addFilterBefore(authenticationFromTokenFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/error").permitAll()
.antMatchers("/admin/**").hasRole(UserDomainConstants.ROLE_ADMIN)
.anyRequest().authenticated();
}
}
}
Вот два фильтра:
public class AuthenticationFromCredentialsFilter extends UsernamePasswordAuthenticationFilter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenAuthenticationService tokenAuthenticationService;
@Autowired
CredentialsService credentialsService;
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
try {
CredentialsResource credentialsResource = new ObjectMapper().readValue(req.getInputStream(),
CredentialsResource.class);
return authenticationManager.authenticate(credentialsService.authenticate(credentialsResource));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authentication) throws IOException, ServletException {
tokenAuthenticationService.addTokenToResponseHeader(response, authentication);
}
}
public class AuthenticationFromTokenFilter extends UsernamePasswordAuthenticationFilter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenAuthenticationService tokenAuthenticationService;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
tokenAuthenticationService.authenticate(request);
return authenticationManager.authenticate(tokenAuthenticationService.authenticate(request));
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authentication) throws IOException, ServletException {
}
}
Вот пример запроса на вход в систему, который должен быть захвачен фильтром AuthenticationFromCredentialsFilter в конфигурации безопасности, но это не так, и поэтому ему разрешено перейти к контроллеру и дать ответ со статусом 201:
$ curl -i -H "Accept:application/json" -H "Content-Type: application/json" "http://localhost:8080/api/users/login" -X POST -d "{ \"email\" : \"[email protected]\", \"password\" : \"xxxxx\" }"
HTTP/1.1 201
Cache-Control: no-store
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzQwNTE5MjYsInN1YiI6Im1pdHRpcHJvdmVuY2VAeWFob28uc2UifQ.LOJvr5jWouWsLN_Pinlr_F5dntON45hwpUFVmXD2Xqo
Location: http://localhost:8080/api/users/1
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 05 Aug 2018 05:32:07 GMT
{"firstname":"Stephane","lastname":"Eybert","email":"[email protected]","confirmedEmail":false,"password":"bWl0dGlwcm92ZW5jZUB5YWhvby5zZTptaWduZXQxYjE4ZDQ5MS00ZGRhLTQxZWYtYWM5ZS04N2Y5ODk = ","workPhone":null,"userRoles":[{"role":"ROLE_ADMIN","id":1}],"_links":{"self":{"href":"http://localhost:8080/api/users/1"},"roles":{"href":"http://localhost:8080/api/users/1/roles"}},"id":1}[stephane@stephane-ThinkPad-X201 user-rest (master)]
Правильно ли я ожидаю, что запрос на вход будет запускать фильтр AuthenticationFromCredentialsFilter? Что фильтр выполняет аутентификацию и отвечает токеном? И контроллер входа в систему вообще не вызывается?
Вот еще один пример запроса на смену пароля, который должен быть захвачен фильтром AuthenticationFromTokenFilter в конфигурации безопасности, но это не так, и поэтому ему разрешено перейти к контроллеру и дать ответ со статусом 200:
$ curl -i -H "Accept:application/json" -H "Content-Type: application/json" "http://localhost:8080/api/users/1/password" -X PUT -d "\"xxxxx\""
HTTP/1.1 200
Cache-Control: no-store
Location: http://localhost:8080/api/users/1
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 04 Aug 2018 20:23:17 GMT
{"firstname":"Stephane","lastname":"Eybert","email":"[email protected]","confirmedEmail":false,"password":"bWl0dGlwcm92ZW5jZUB5YWhvby5zZTptaWduZXRhYTA4OTNiZS0yMzZlLTQ3ZjktOTE2Ny0zOTU0NTY = ","workPhone":null,"userRoles":[{"role":"ROLE_ADMIN","id":1}],"_links":{"self":{"href":"http://localhost:8080/api/users/1"},"roles":{"href":"http://localhost:8080/api/users/1/roles"}},"id":1}
А как насчет использования CustomAuthenticationProvider implements AuthenticationProvider вместо фильтра AuthenticationFromCredentialsFilter для запроса входа в систему? Это все еще возможно в Spring Boot 2.0.3?
Я думаю о чем-то вроде:
@Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider);
}
Если провайдер аутентификации:
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
CredentialsService credentialsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return credentialsService.authenticate(authentication);
}
@Override
public boolean supports(Class<?> authentication) {
boolean value = (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
return value;
}
}
ОБНОВЛЕНИЕ: я также пробовал эту конфигурацию, но это ничего не изменило в проблеме:
public AuthenticationFromCredentialsFilter authenticationFromCredentialsFilter() throws Exception {
AuthenticationFromCredentialsFilter authenticationFromCredentialsFilter = new AuthenticationFromCredentialsFilter();
authenticationFromCredentialsFilter.setAuthenticationManager(authenticationManagerBean());
authenticationFromCredentialsFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/api/users/login"));
return authenticationFromCredentialsFilter;
}
public AuthenticationFromTokenFilter authenticationFromTokenFilter() throws Exception {
AuthenticationFromTokenFilter authenticationFromTokenFilter = new AuthenticationFromTokenFilter();
authenticationFromTokenFilter.setAuthenticationManager(authenticationManagerBean());
authenticationFromTokenFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/**"));
return authenticationFromTokenFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.headers().cacheControl().disable().frameOptions().disable();
http.httpBasic().authenticationEntryPoint(restAuthenticationEntryPoint);
http.addFilterBefore(simpleCORSFilter, UsernamePasswordAuthenticationFilter.class);
http.antMatcher("/api/**")
.addFilterBefore(authenticationFromCredentialsFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(authenticationFromTokenFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/error").permitAll()
.antMatchers("/api/users/login").permitAll()
.antMatchers("/admin/**").hasRole(UserDomainConstants.ROLE_ADMIN)
.anyRequest().authenticated();
}




Ваши фильтры расширяют UsernamePasswordAuthenticationFilter, и этот фильтр по умолчанию применяется только для URL-адреса /login, см. UsernamePasswordAuthenticationFilter:
This filter by default responds to the URL
/login.
Если вы хотите изменить URL-адрес по умолчанию на другой, см. AbstractAuthenticationProcessingFilter#setFilterProcessesUrl:
Sets the URL that determines if authentication is required
Ваш измененный код:
public AuthenticationFromCredentialsFilter authenticationFromCredentialsFilter() throws Exception {
AuthenticationFromCredentialsFilter authenticationFromCredentialsFilter = new AuthenticationFromCredentialsFilter();
authenticationFromCredentialsFilter.setAuthenticationManager(authenticationManagerBean());
authenticationFromCredentialsFilter.setFilterProcessesUrl("/api/users/login");
return authenticationFromCredentialsFilter;
}
Если вы хотите использовать шаблон, см. АннотацияAuthenticationProcessingFilter:
This filter will intercept a request and attempt to perform authentication from that request if the request matches the
setRequiresAuthenticationRequestMatcher(RequestMatcher).
Ваш измененный код:
public AuthenticationFromTokenFilter authenticationFromTokenFilter() throws Exception {
AuthenticationFromTokenFilter authenticationFromTokenFilter= new AuthenticationFromTokenFilter();
authenticationFromTokenFilter.setAuthenticationManager(authenticationManagerBean());
authenticationFromTokenFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/api/**");
return authenticationFromTokenFilter;
}
Наконец-то я смог запустить фильтры, каждый для своих запросов в среде Spring Boot 2.
Фильтр учетных данных запускается при запросе входа в конечную точку /users/login.
Фильтр токенов срабатывает при любом запросе, кроме запроса на вход.
Чтобы назначить шаблон URL-адреса фильтру, фильтр должен был расширить класс AbstractAuthenticationProcessingFilter. И два фильтра расширяют этот класс. Шаблон указывается в их конструкторе.
Фильтр учетных данных:
public class AuthenticationFromCredentialsFilter extends AbstractAuthenticationProcessingFilter {
@Autowired
private TokenAuthenticationService tokenAuthenticationService;
@Autowired
CredentialsService credentialsService;
public AuthenticationFromCredentialsFilter(final RequestMatcher requestMatcher) {
super(requestMatcher);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
try {
CredentialsResource credentialsResource = new ObjectMapper().readValue(req.getInputStream(),
CredentialsResource.class);
return credentialsService.authenticate(credentialsResource);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authentication) throws IOException, ServletException {
tokenAuthenticationService.addTokenToResponseHeader(response, authentication);
}
}
Обратите внимание, что при успешной аутентификации цепочка фильтров не выполняется, то есть нет такого вызова filterChain.doFilter(httpRequest, httpResponse);, потому что нет необходимости попадать в конечную точку контроллера, поскольку ответ уже отправлен обратно с токеном.
Он явно создается с аннотацией @Bean, чтобы указать шаблон сопоставления:
@Bean
public AuthenticationFromCredentialsFilter authenticationFromCredentialsFilter() throws Exception {
AuthenticationFromCredentialsFilter authenticationFromCredentialsFilter = new AuthenticationFromCredentialsFilter(new AntPathRequestMatcher("/users/login", RequestMethod.POST.name()));
authenticationFromCredentialsFilter.setAuthenticationManager(authenticationManagerBean());
return authenticationFromCredentialsFilter;
}
Фильтр токенов:
public class AuthenticationFromTokenFilter extends AbstractAuthenticationProcessingFilter {
@Autowired
private TokenAuthenticationService tokenAuthenticationService;
public AuthenticationFromTokenFilter(final RequestMatcher requestMatcher) {
super(requestMatcher);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
return tokenAuthenticationService.authenticate(request);
}
@Override
protected void successfulAuthentication(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
FilterChain filterChain, Authentication authResult) throws IOException, ServletException {
filterChain.doFilter(httpRequest, httpResponse);
}
}
Он явно создается с аннотацией @Bean, чтобы указать шаблон сопоставления:
@Bean
public AuthenticationFromTokenFilter authenticationFromTokenFilter() throws Exception {
AuthenticationFromTokenFilter authenticationFromTokenFilter = new AuthenticationFromTokenFilter(new NegatedRequestMatcher(new AntPathRequestMatcher("/users/login", RequestMethod.POST.name())));
authenticationFromTokenFilter.setAuthenticationManager(authenticationManagerBean());
return authenticationFromTokenFilter;
}
В отличие от предыдущего фильтра учетных данных, этот фильтр необходимо явно указать в конфигурации:
http
.addFilterBefore(authenticationFromTokenFilter, UsernamePasswordAuthenticationFilter.class)
Чтобы этот фильтр не запускался дважды в цепочке фильтров, bean-компонент указывает Spring Boot не вводить его автоматически:
@Bean
FilterRegistrationBean<AuthenticationFromTokenFilter> disableAutoRegistration(final AuthenticationFromTokenFilter filter) {
final FilterRegistrationBean<AuthenticationFromTokenFilter> registration = new FilterRegistrationBean<AuthenticationFromTokenFilter>(filter);
registration.setEnabled(false);
return registration;
}
Обратите внимание, что при успешной аутентификации выполняется отслеживание цепочки фильтров, то есть есть вызов filterChain.doFilter(httpRequest, httpResponse);, потому что после успешной аутентификации необходимо поразить конечную точку контроллера.
Полная конфигурация:
@ComponentScan(nameGenerator = PackageBeanNameGenerator.class, basePackages = { "com.thalasoft.user.rest.security",
"com.thalasoft.user.rest.service", "com.thalasoft.user.rest.filter" })
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private RESTAuthenticationEntryPoint restAuthenticationEntryPoint;
@Autowired
private SimpleCORSFilter simpleCORSFilter;
@Autowired
AuthenticationFromTokenFilter authenticationFromTokenFilter;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public AuthenticationFromCredentialsFilter authenticationFromCredentialsFilter() throws Exception {
AuthenticationFromCredentialsFilter authenticationFromCredentialsFilter = new AuthenticationFromCredentialsFilter(new AntPathRequestMatcher("/users/login", RequestMethod.POST.name()));
authenticationFromCredentialsFilter.setAuthenticationManager(authenticationManagerBean());
return authenticationFromCredentialsFilter;
}
@Bean
public AuthenticationFromTokenFilter authenticationFromTokenFilter() throws Exception {
AuthenticationFromTokenFilter authenticationFromTokenFilter = new AuthenticationFromTokenFilter(new NegatedRequestMatcher(new AntPathRequestMatcher("/users/login", RequestMethod.POST.name())));
authenticationFromTokenFilter.setAuthenticationManager(authenticationManagerBean());
return authenticationFromTokenFilter;
}
@Bean
FilterRegistrationBean<AuthenticationFromTokenFilter> disableAutoRegistration(final AuthenticationFromTokenFilter filter) {
final FilterRegistrationBean<AuthenticationFromTokenFilter> registration = new FilterRegistrationBean<AuthenticationFromTokenFilter>(filter);
registration.setEnabled(false);
return registration;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.logout().disable();
http.exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint);
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(simpleCORSFilter, ChannelProcessingFilter.class);
http
.addFilterBefore(authenticationFromTokenFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/", "/error").permitAll()
.antMatchers("/users/login").permitAll()
.antMatchers("/admin/**").hasRole(UserDomainConstants.ROLE_ADMIN)
.anyRequest().authenticated();
}
}
Также обратите внимание, что в этой конфигурации Spring Boot не используется вообще:
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
CredentialsService credentialsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return credentialsService.authenticate(authentication);
}
@Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
}
Интересно, где он мог бы поместиться и как он мог бы заменить фильтр учетных данных ... но это уже другая история.
Позвольте нам продолжить обсуждение в чате.