Пружинный предохранительный фильтр не срабатывает

Мои фильтры аутентификации не срабатывают по запросу.

У меня есть 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();
}
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Версия Java на основе версии загрузки
Версия Java на основе версии загрузки
Если вы зайдете на официальный сайт Spring Boot , там представлен start.spring.io , который упрощает создание проектов Spring Boot, как показано ниже.
Документирование API с помощью Swagger на Springboot
Документирование API с помощью Swagger на Springboot
В предыдущей статье мы уже узнали, как создать Rest API с помощью Springboot и MySql .
1
0
1 759
2

Ответы 2

Ваши фильтры расширяют 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;
}

Позвольте нам продолжить обсуждение в чате.

Stephane 05.08.2018 10:33

Наконец-то я смог запустить фильтры, каждый для своих запросов в среде 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));
    }

}

Интересно, где он мог бы поместиться и как он мог бы заменить фильтр учетных данных ... но это уже другая история.

Другие вопросы по теме