简体   繁体   中英

Spring Security returns 404 instead of 403 when using @PreAuthorize

After struggling with this for a few days (searching SO for similar questions, doing trial & error), I am tempted to give up...

So the problem is I have a REST service based on Spring Boot using Spring Security and JWT for authentication. Now I want to secure some of the methods to be only called by authorized people using the @PreAuthorize -annotation. This seems to work partly because instead of calling the method Spring returns 404. I would have expected 403.

I have read this SO-question and tried the answers given there, but it did not help. I have moved the @EnableGlobalMethodSecurity(prePostEnabled = true) -Annotation from my SecurityConfiguration to the Application class as suggested elsewhere, still it does not work.

My security configuration looks like this:

@Configuration
@Profile("production")
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Value("${adDomain}")
private String adDomain;

@Value("${adUrl}")
private String adUrl;

@Value("${rootDn}")
private String rootDn;

@Value("${searchFilter}")
private String searchFilter;

private final AuthenticationManagerBuilder auth;

private final SessionRepository sessionRepository;

@Autowired
public SecurityConfiguration(AuthenticationManagerBuilder auth, SessionRepository sessionRepository) {
    this.auth = auth;
    this.sessionRepository = sessionRepository;
}

@Override
public void configure(WebSecurity webSecurity) throws Exception
{
    webSecurity
            .ignoring()
            // All of Spring Security will ignore the requests
            .antMatchers("/static/**", "/api/web/logout")
            .antMatchers(HttpMethod.POST, "/api/web/login");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable() // Using JWT there is no need for CSRF-protection!
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .addFilter(new JwtAuthorizationFilter(authenticationManagerBean(), sessionRepository));
}

@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    ActiveDirectoryLdapAuthenticationProvider adProvider =
            new ActiveDirectoryLdapAuthenticationProvider(adDomain, adUrl, rootDn);
    adProvider.setConvertSubErrorCodesToExceptions(true);
    adProvider.setUseAuthenticationRequestCredentials(true);
    adProvider.setSearchFilter(searchFilter);
    adProvider.setUserDetailsContextMapper(new InetOrgPersonContextMapper());
    auth.authenticationProvider(adProvider);
    return super.authenticationManagerBean();
}

}

The controller method looks like this

@RequestMapping(path = "/licenses", method = RequestMethod.GET)
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> getAllLicenses(@RequestParam("after") int pagenumber, @RequestParam("size") int pagesize
        , @RequestParam("searchText") String searchText) {       
    List<LicenseDTO> result = ...
    return new ResponseEntity<Object>(result, HttpStatus.OK);
}

I am quite sure I am missing something very simple, but I just cannot figure out what.

By the way: if the user requesting the licenses has the ADMIN role everything works as expected, so the problem is not a real 404.

You need to define the exceptionHandling at security configuration as follows,

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable() // Using JWT there is no need for CSRF-protection!
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .exceptionHandling().accessDeniedHandler(new AccessDeniedExceptionHandler())
            .and()
            .addFilter(new JwtAuthorizationFilter(authenticationManagerBean(), sessionRepository));
}

You can define AccessDeniedExceptionHandler class as follows,

public class AccessDeniedExceptionHandler implements AccessDeniedHandler
{
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException ex) throws IOException, ServletException {
        response.setStatus(HttpStatus.FORBIDDEN);
    }
}

I finally found a solution fitting my purposes. I do not know if this is the best way to deal with this, but just adding an ExceptionHandler did the trick. Somewhere deep inside the filterchain the 403 mutates to 404 when there is no such handler in place. Perhaps I am to dump to read and understand the documentation, but I did not find anything that suggest you have to do this. So maybe I am wrong solving the problem like this, but here is the code that did the trick (it is a really basic implementation that should be improved over time):

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(Throwable.class)
    @ResponseBody
    public ResponseEntity<String> handleControllerException(Throwable ex) {
        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
        if(ex instanceof AccessDeniedException) {
            status = HttpStatus.FORBIDDEN;
        }

        return new ResponseEntity<>(ex.getMessage(), status);
    }
}

Global method security can be enabled with the help of annotation @EnableGlobalMethodSecurity(prePostEnabled=true) . The combination of this and @Preauthorize will create a new proxy for your controller and it will loose the Request mapping which will result in 404 Exception.

To handle this you can use the annotation @EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true) which is there in your SecurityConfiguration class.

Provided the details in another answer as well.

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