简体   繁体   中英

Custom spring security filter not called at runtime

I'm trying to enable spring security in a spring boot rest services project and I'm getting some problems.

I configured it with this code

@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();
    }

}

and implemented a custom authentication provider in order to login to LDAP (which has a non standard configuration so I wasn't able to make the default ldap provider works)

@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

    }

}

This code is working fine, in the sense that calling my services with a basic authorization header it is able to correctly login and return the service called. The problems started when I tried to insert a different authorization/authentication. Instead of using the basic authentication I would like to pass the credential from a form in my react front end, so I would like to pass them as a json body in a POST call. (the idea is then to generate a jwt token and use that for the following communication).

So I changed the configure method to this:

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

and defined a custom authentication filter:

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);

    }

}

Problem is, whatever I'm doing, the runtime doesn't seems to enter in this filter at all. What am I missing? I guess is something big and stupid but I can't figure it out...

UPDATE: the problem seems to be that the UsernamePassWordAuthenticationFilter can be called only through a form. I then change my code to extend AbstractAuthenticationProcessingFilter instead.

The modified filter:

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 );
        }
    }
}

and the modified configure method:

@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);
}

You expect the filter to be triggered by accessing the path api/secureLogin . By default UsernamePasswordAuthenticationFilter is triggered only by accessing /login .

If you add following line in the constructor of the JWTAuthenticationFilter which extends UsernamePasswordAuthenticationFilter it should work:

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

Hi @Mikyjpeg

You can use UsernamePassWordAuthenticationFilter . It does not have to be called from a form as you mentioned.

As long as it is called via a POST method with the url /login (instead of your api/secureLogin url), it will be executed.

The constructor uses a request matcher that only allows this url & request method:

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

I see your adding the configuration while overriding the configure method, try adding the filter mapping in your web.xml. Something like this under the 'web-app' node:

<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>

Answer by ADAM has worked for me. Those who are working with annotation based configuration can add following

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

Into your AppInitialiser extends AbstractAnnotationConfigDispatcherServletInitializer

public class BeforeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public BeforeAuthenticationFilter() {
        super(new AntPathRequestMatcher("api/secureLogin", "POST"));
        super.setFilterProcessesUrl("api/secureLogin");
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM