Пользовательский весенний фильтр безопасности не вызывается во время выполнения

Я пытаюсь включить безопасность Spring в проекте служб Spring Boot Rest, и у меня возникают некоторые проблемы.

Я настроил это с помощью этого кода

@Configuration
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {

    @Autowired
    private LdapAuthenticationProvider ldapAuthenticationProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .httpBasic().and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth.authenticationProvider(ldapAuthenticationProvider);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}

и реализовал настраиваемого поставщика аутентификации для входа в LDAP (который имеет нестандартную конфигурацию, поэтому я не смог заставить работать поставщика ldap по умолчанию)

@Component
public class LdapAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {

        String email = authentication.getName();
        String password = authentication.getCredentials().toString();

        LdapConnection ldap = new LdapConnection();
        String uid = ldap.getUserUID(email);
        if (uid == null || uid == ""){
            throw new BadCredentialsException("User " + email + " not found");
        }

        if (ldap.login(uid, password)){

            return new UsernamePasswordAuthenticationToken(uid, null, new ArrayList<>());
        }else{
            throw new BadCredentialsException("Bad credentials");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return true;  
        //To indicate that this authenticationprovider can handle the auth request. since there's currently only one way of logging in, always return true

    }

}

Этот код работает нормально в том смысле, что вызывая мои службы с базовым заголовком авторизации, он может правильно войти в систему и вернуть вызванную службу. Проблемы начались, когда я попытался вставить другую авторизацию / аутентификацию. Вместо использования базовой аутентификации я хотел бы передать учетные данные из формы в моем интерфейсе реагирования, поэтому я хотел бы передать их как тело json в вызове POST. (идея состоит в том, чтобы сгенерировать токен jwt и использовать его для следующего сообщения).

Поэтому я изменил метод настройки на этот:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .addFilter(new JWTAuthenticationFilter(authenticationManager()))
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

и определил настраиваемый фильтр аутентификации:

public class JWTAuthenticationFilter extends
        UsernamePasswordAuthenticationFilter {

    @Autowired
    private AuthenticationManager authenticationManager;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException{
        String requestBody;
        try{
            requestBody = IOUtils.toString(req.getReader());
            JsonParser jsonParser = JsonParserFactory.getJsonParser();
            Map<String, Object> requestMap = jsonParser.parseMap(requestBody);
            return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(requestMap.get("email"), requestMap.get("password"), new ArrayList<>()));
        }catch(IOException e){
            throw new InternalAuthenticationServiceException("Something goes wrong parsing the request body",e );
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException{

        JwtTokenProvider tokenProvider = new JwtTokenProvider();
        String token = tokenProvider.generateToken(auth.getPrincipal().toString());
        Cookie cookie = new Cookie("jwt",token);
        cookie.setHttpOnly(true);
        cookie.setSecure(true);
        res.addCookie(cookie);

    }

}

Проблема в том, что что бы я ни делал, среда выполнения, похоже, вообще не попадает в этот фильтр. Что мне не хватает? Я предполагаю, что это что-то большое и глупое, но я не могу этого понять ...

Обновлено: проблема, похоже, в том, что UsernamePassWordAuthenticationFilter можно вызвать только через форму. Затем я изменяю свой код, чтобы вместо этого расширить AbstractAuthenticationProcessingFilter.

Модифицированный фильтр:

public class JWTAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {


    private AuthenticationManager authenticationManager;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        super("/api/secureLogin");
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException{
        String requestBody;
        try{
            requestBody = IOUtils.toString(req.getReader());
            JsonParser jsonParser = JsonParserFactory.getJsonParser();
            Map<String, Object> requestMap = jsonParser.parseMap(requestBody);
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(requestMap.get("email"), requestMap.get("password"), new ArrayList<>()); 
            return authenticationManager.authenticate(token);
        }catch(IOException e){
            throw new InternalAuthenticationServiceException("Something goes wrong parsing the request body",e );
        }
    }
}

и модифицированный метод настройки:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable().authorizeRequests()
        .antMatchers(HttpMethod.POST, "/api/secureLogin").permitAll()
        .antMatchers(HttpMethod.GET, "/api").permitAll()
        .antMatchers("/api/**").authenticated()
        .and()
        .addFilterBefore(new JWTAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}

Пожалуйста, подтвердите, что вы попали в конечную точку «/ login», когда ожидаете, что этот фильтр сработает.

jzheaux 31.05.2018 01:09

У меня есть собственный контроллер (который в основном записывает строку) в / api / login и ведет себя так же. Я тоже попробовал / войти (на котором я ничего не определил), но это не меняется: фильтр не попадает.

Mikyjpeg 31.05.2018 11:31

Итак, я обнаружил, что на самом деле это вызывается только в том случае, если я использую форму входа по умолчанию (добавление .formLogin() в мой метод настройки). Тогда я, вероятно, мог бы определить настраиваемую форму, но, будучи приложением веб-службы (пользовательский интерфейс управляется в другом месте), я не хочу создавать здесь форму входа, поскольку я хотел бы использовать уже имеющийся у меня вызов отдыха ... если я расширяю другой класс тогда UsernamePasswordAuthenticationFilter? В этом случае есть ли класс, который я могу использовать для своего сценария, или мне следует расширить класс AbstractAuthenticationProcessingFilter?

Mikyjpeg 31.05.2018 14:40

Итак, здесь есть пара вещей. Во-первых, по умолчанию UsernamePasswordAuthenticationFilter # tryAuthentication вызывается только в / login или в том, что настроено как filterProcessesUrl - фильтр может отклонять запрос в методе requiresAuthentication. Во-вторых, да, если это вызов для отдыха, тогда расширение UsernamePasswordAuthenticationFilter немного странно. Если вы посмотрите на successAuthentication, вы увидите, что по умолчанию successHandler хочет перенаправить вызов, когда это будет сделано. Я бы, наверное, просто расширил OncePerRequestFilter.

jzheaux 01.06.2018 01:58

Спасибо, я решил использовать AbstractAuthenticationProcessingFilter (см. Обновленную информацию в сообщении), но я посмотрю на упомянутый вами класс OncePerRequestFilter.

Mikyjpeg 01.06.2018 10:19
3
5
3 571
4

Ответы 4

Я вижу, что вы добавляете конфигурацию, переопределяя метод настройки, попробуйте добавить сопоставление фильтра в свой web.xml. Что-то вроде этого в узле "веб-приложение":

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter>
    <filter-name>JWTAuthenticationFilter</filter-name>
    <filter-class>com.yourProject.JWTAuthenticationFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>JWTAuthenticationFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

У меня нет web.xml, так как я работаю с весенней загрузкой. В любом случае аннотация Configuration не нужна, и я, вероятно, оставил ее там по ошибке из предыдущей попытки, я собираюсь ее удалить.

Mikyjpeg 31.05.2018 11:35

Вы ожидаете, что фильтр будет активирован при доступе к пути api/secureLogin. По умолчанию UsernamePasswordAuthenticationFilter запускается только при доступе к /login.

Если вы добавите следующую строку в конструктор JWTAuthenticationFilter, который расширяет UsernamePasswordAuthenticationFilter, он должен работать:

this.setFilterProcessesUrl("/api/secureLogin");

Привет @Mikyjpeg

Вы можете использовать UsernamePassWordAuthenticationFilter. Его не нужно вызывать из формы, как вы упомянули.

Пока он вызывается с помощью метода POST с URL-адресом /login (вместо вашего URL-адреса api/secureLogin), он будет выполняться.

Конструктор использует сопоставление запросов, которое разрешает только этот метод URL и запроса:

public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); }

Ответ ADAM сработал для меня. Те, кто работает с конфигурацией на основе аннотаций, могут добавить следующие

@Override protected Filter[] getServletFilters() {
   return new Filter[]{
        new JwtAuthenticationFilter()
   }; 
}

В ваш AppInitialiser распространяется AbstractAnnotationConfigDispatcherServletInitializer

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