最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

spring - Override anyRequest().denyAll() with method level and class level security annotations - Stack Overflow

programmeradmin4浏览0评论

Situation

When developing web applications, it is often useful to have sane security defaults. In the case of my web API, I want to shut down access to all resources by default. No one should be able to access endpoints.

However, I want to explicitly override these rules on the controllers and/or on the endpoint methods themselves, using annotations like @Secured, @RolesAllowed, @PermitAll or @PreAuthorize.

Furthermore, I want the /login and /register paths to be public.

For now, I have created following security configuration (I have stripped it down a bit to the most important parts):

fun securityFilterChain(http: HttpSecurity): SecurityFilterChain =
http
    .csrf { it.disable() }
    .authorizeHttpRequests {
        it
            .requestMatchers(HttpMethod.POST, "/auth/register", "/auth/login")
            .permitAll()
            .anyRequest()
            .denyAll()
    }
// ... further config
.build()

As you can see, I have taken down access to all paths except the ones mentioned above.

Problem

The problem with my configuration is that denyAll() cancels the filter chain prematurely. That is, the framework does not evaluate my controller/method level annotations at all. A 403 forbidden is returned automatically.

What I want

Example 1

Now I want to do the following in my controllers:

@RestController
class TodoController(private val todoService: TodoService) {
    
    // ONLY users with role admin
    @RolesAllowed(Role.ADMIN_VALUE)
    // @<Some HTTP mapping>
    fun method0()

    // ONLY users with roles admin and user
    @RolesAllowed(Role.ADMIN_VALUE, Role.USER_VALUE)
    // @<Some HTTP mapping>
    fun method1()

    // ANY "authn" user, independent of their "auhtz" status
    @PreAuthorize("isAuthenticated()")
    fun method2()

    // Anyone, independent of their "authn" / "authz" status
    @PermitAll
    // @<Some HTTP mapping>
    fun method3()

    // UUUUPPPPPPSSS..... Fot to annotate? No problem, NO ONE can call this!
    // @<Some HTTP mapping>
    fun method4()
}

As you can see above, the annotations enable granular control over the endpoints. If a developer fets to annotate the endpoint methods properly, no damage is done, because the system rejects everything by default.

Example 2

Suppose we have a controller in which all endpoint methods require the same settings. In these cases, it would be helpful to just annotate the class itself and the methods inherit this setting.

// Endpoint methods in this controller can be called by anyone, regardless of "authn" / "authz" status
@PermitAll
@RestController
class TodoController

// Endpoint methods in this controller can ONLY be called by admins
@RolesAllowed(Role.ADMIN_VALUE)
@RestController
class TodoController

// Endpoint methods in this controller can ONLY be called admins and regular users
@RolesAllowed(Role.ADMIN_VALUE, Role.USER_VALUE)
@RestController
class TodoController

// Endpoint methods in this controller can ONLY be called by logged-in users (regardless of their roles)
@PreAuthorize("isAuthenticated()")
@RestController
class TodoController

/* 
   Endpoint methods in this controller cannot be called by anyone, 
   because neither the controller, nor the methods have security annotations. 
   The default configuration of denying everything automatically kicks in. 
*/
@RestController
class TodoController

Example 3

As you might have imagined, in the case that the controller and the methods are both annotated, I want the method annotations to over rule the controller annotations:

@PreAuthorize("isAuthenticated()")
@RestController
class TodoController {
    
    // Overrides "isAuthenticated": Now anyone can call this
    @PermitAll
    fun foo()

    // Has inherited "isAuthenticated" from the controller
    fun bar()

    // Overrides "isAuthenticated": Only users with role admin can access
    @Secured(Role.ADMIN_VALUE)
    fun baz()
}

Question

How does one accomplish this? I know for a fact that this is somehow possible in Spring Boot, because the Vaadin framework follows a similar approach. However, since this is a regular Spring Boot application without dependencies to Vaadin, I am clueless on how to do this. I could not find a satisfying answer either.

Lastly... Yes, I know, I can just set the path configurations in my SecurityConfig for every endpoint method. However, that's not what I want. I want explicit control on the controllers/endpoint methods.

Situation

When developing web applications, it is often useful to have sane security defaults. In the case of my web API, I want to shut down access to all resources by default. No one should be able to access endpoints.

However, I want to explicitly override these rules on the controllers and/or on the endpoint methods themselves, using annotations like @Secured, @RolesAllowed, @PermitAll or @PreAuthorize.

Furthermore, I want the /login and /register paths to be public.

For now, I have created following security configuration (I have stripped it down a bit to the most important parts):

fun securityFilterChain(http: HttpSecurity): SecurityFilterChain =
http
    .csrf { it.disable() }
    .authorizeHttpRequests {
        it
            .requestMatchers(HttpMethod.POST, "/auth/register", "/auth/login")
            .permitAll()
            .anyRequest()
            .denyAll()
    }
// ... further config
.build()

As you can see, I have taken down access to all paths except the ones mentioned above.

Problem

The problem with my configuration is that denyAll() cancels the filter chain prematurely. That is, the framework does not evaluate my controller/method level annotations at all. A 403 forbidden is returned automatically.

What I want

Example 1

Now I want to do the following in my controllers:

@RestController
class TodoController(private val todoService: TodoService) {
    
    // ONLY users with role admin
    @RolesAllowed(Role.ADMIN_VALUE)
    // @<Some HTTP mapping>
    fun method0()

    // ONLY users with roles admin and user
    @RolesAllowed(Role.ADMIN_VALUE, Role.USER_VALUE)
    // @<Some HTTP mapping>
    fun method1()

    // ANY "authn" user, independent of their "auhtz" status
    @PreAuthorize("isAuthenticated()")
    fun method2()

    // Anyone, independent of their "authn" / "authz" status
    @PermitAll
    // @<Some HTTP mapping>
    fun method3()

    // UUUUPPPPPPSSS..... Fot to annotate? No problem, NO ONE can call this!
    // @<Some HTTP mapping>
    fun method4()
}

As you can see above, the annotations enable granular control over the endpoints. If a developer fets to annotate the endpoint methods properly, no damage is done, because the system rejects everything by default.

Example 2

Suppose we have a controller in which all endpoint methods require the same settings. In these cases, it would be helpful to just annotate the class itself and the methods inherit this setting.

// Endpoint methods in this controller can be called by anyone, regardless of "authn" / "authz" status
@PermitAll
@RestController
class TodoController

// Endpoint methods in this controller can ONLY be called by admins
@RolesAllowed(Role.ADMIN_VALUE)
@RestController
class TodoController

// Endpoint methods in this controller can ONLY be called admins and regular users
@RolesAllowed(Role.ADMIN_VALUE, Role.USER_VALUE)
@RestController
class TodoController

// Endpoint methods in this controller can ONLY be called by logged-in users (regardless of their roles)
@PreAuthorize("isAuthenticated()")
@RestController
class TodoController

/* 
   Endpoint methods in this controller cannot be called by anyone, 
   because neither the controller, nor the methods have security annotations. 
   The default configuration of denying everything automatically kicks in. 
*/
@RestController
class TodoController

Example 3

As you might have imagined, in the case that the controller and the methods are both annotated, I want the method annotations to over rule the controller annotations:

@PreAuthorize("isAuthenticated()")
@RestController
class TodoController {
    
    // Overrides "isAuthenticated": Now anyone can call this
    @PermitAll
    fun foo()

    // Has inherited "isAuthenticated" from the controller
    fun bar()

    // Overrides "isAuthenticated": Only users with role admin can access
    @Secured(Role.ADMIN_VALUE)
    fun baz()
}

Question

How does one accomplish this? I know for a fact that this is somehow possible in Spring Boot, because the Vaadin framework follows a similar approach. However, since this is a regular Spring Boot application without dependencies to Vaadin, I am clueless on how to do this. I could not find a satisfying answer either.

Lastly... Yes, I know, I can just set the path configurations in my SecurityConfig for every endpoint method. However, that's not what I want. I want explicit control on the controllers/endpoint methods.

Share Improve this question asked Feb 15 at 22:39 aslaryaslary 4112 silver badges12 bronze badges 1
  • Can't be done in a tidy way. Rather add a an ArchUnit annotation check (archunit./use-cases) to prevent any unannotated controller methods being released. – John Williams Commented Feb 16 at 13:42
Add a comment  | 

2 Answers 2

Reset to default 2

Recently I faced this same scenario that I needed to implement for a Java application. After walking-through the Spring Security docs and code I decided to implement custom AuthorizationManager that replaces the default PreAuthorizeAuthorizationManager with my own authorization decision.

This custom PreAuthorizeAuthorizationManager only delegates the decision to the common Spring Security implementation, and if it doesn't have a decision (the method called does not have an JSR250/PreAuthorize annotation) return a AuthorizationDecision that denies the request.

@Component
public class CustomDelegatingPreAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocation> {

    private static final AuthorizationDecision DENIED_DECISION = new AuthorizationDecision(false);

    private final AuthorizationManager<MethodInvocation> preAuthorizeDelegate = new PreAuthorizeAuthorizationManager();
    private final AuthorizationManager<MethodInvocation> jsr250Delegate = new Jsr250AuthorizationManager();

    @Override
    public AuthorizationDecision check(final Supplier<Authentication> authentication, final MethodInvocation object) {
        return Optional.ofNullable(preAuthorizeDelegate.authorize(authentication, object))
            .or(() -> Optional.ofNullable(jsr250Delegate.authorize(authentication, object)))
            .filter(AuthorizationDecision.class::isInstance)
            .map(AuthorizationDecision.class::cast)
            .orElse(DENIED_DECISION);
    }

}

Then, we need to publish the method interceptor with a pointcut to when this custom AuthorizationManager should run. The example below was inspired in the Spring's PreAuthorize method interceptor implementation, which the pointcut is for methods annotated with the @PreAuthorize, and I replaced to run for all methods annotated with @RequestMapping, which would be all controller endpoints. (You can add any pointcut you want here like @RestController for example)

Note: If you observe I'm delegating the jsr250 AuthorizationManager instead of enabling in the @EnableMethodSecurity(jsr250Enabled = true). The reason is in the CustomAuthorizationManager, always returns an AuthorizationDecision and if the mehtod does not have an annotation will be a denied decision, making the spring not call the next AuthorizationInterception since the JSR250 order is after PRE_AUTHORIZE

@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public Advisor preAuthorize(final CustomDelegatingPreAuthorizeAuthorizationManager manager) {
        final Pointcut pointcut = Pointcuts.union(
            new AnnotationMatchingPointcut(null, RequestMapping.class, true),
            new AnnotationMatchingPointcut(RequestMapping.class, true)
        );

        final AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor(
            pointcut,
            manager
        );
        interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder());

        return interceptor;
    }

}

The last but not least, we need to change in the SecurityFilterChain to permit all requests, which will make the Spring always call the PreAuthorizedMethodInterceptor in the filter chain, and doing the decision in the CustomAuthorizationManager.

@EnableWebSecurity
public class WebSecurityConfiguration {

    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(
        final HttpSecurity http
    ) throws Exception {
        return http
            .authorizeHttpRequests(authorize ->
                authorize
                    .anyRequest().permitAll()
            )
            .build();
    }

}

Unfortunately, I don't think this approach will cover all examples you gave, especially of the example 3, that you have a @PreAuthorize in the class level and a @PermitAll to override in method level. As I mentioned before, the CustomAuthorizationManager will always return a AuthorizationDecision, then when find a @PreAuthorize in the controller level does not executes a JSR250 for the method. For my scenarios that was OK, once I always use the @PreAuthorize, and this should work.

@PreAuthorize("isAuthenticated()")
@RestController
@RequestMapping("todo")
public class TodoController {
    
    // Overrides "isAuthenticated": Now anyone can call this
    @PreAuthorize("permitAll()")
    public void foo() {}
}

I'm not sure if this is the right approach, but was what worked to me and hope that can help you.

EDIT:

As asked about the 403 in the Unauthorized response, Spring removed a time ago the Http401AuthenticationEntryPoint, and as suggested in that same thread GitHub thread I added manually an AuthenticationEntryPoint for my scenario.

http.exceptionHandling(exceptions ->
                exceptions
                    .accessDeniedHandler(default403AccessDeniedHandler)
                    .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
            )

The default403AccessDeniedHandler is just a implementation from AccessDeniedHandler to make sure for every request denied with respond with 403

@Component
public class Default403AccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(
        final HttpServletRequest request,
        final HttpServletResponse response,
        final AccessDeniedException accessDeniedException
    ) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
    }

}

Instead of using denyAll() you should use authenticated() that way spring security will try to authenticate all the paths except /login and /register.

If you want to use jsr-250 based annotations then annotate your security config with @EnableMethodSecurity(jsr250Enabled = true) this will enable the @RolesAllowed and @PermitAll annotations.

I would suggest you to use spring security method level annotations exclusively and not mix jsr-250 annotations, as spring security can handle all cases of jsr-250 annotations like @PreAuthorize("permitAll") and @PreAuthorize("hasRole('USER')") etc .

For problem 1, you can try @PreAuthorize("denyAll") at class level. This will block any method which is not annotated by defualt.

Not sure but this might override your security config level request matcher based configurations.

For problem 2, you can use spring security annotations at class level which will automatically apply them at method level.

For problem 3, this will also work by default with spring security annotations.

发布评论

评论列表(0)

  1. 暂无评论