Deny Access on Missing @PreAuthorize to Spring Controller Methods – 拒绝对Spring控制器方法的@PreAuthorize缺失的访问

最后修改: 2019年 12月 20日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

In our tutorial on Spring method security, we saw how we can use the @PreAuthorize and @PostAuthorize annotations.

在我们关于Spring方法安全的教程中,我们看到了如何使用@PreAuthorize@PostAuthorize注释。

In this tutorial, we’ll see how to deny access to methods that lack authorization annotations.

在本教程中,我们将看到如何拒绝对缺乏授权注释的方法的访问

2. Security by Default

2.默认的安全

After all, we are only human, so we might forget to protect one of our endpoints. Unfortunately, there’s no easy way to deny access to non-annotated endpoints.

毕竟,我们只是人类,所以我们可能会忘记保护我们的一个端点。不幸的是,没有简单的方法来拒绝对非注解的端点的访问。

Luckily, Spring Security requires authentication for all endpoints by default. However, it will not require a specific role. Also, it will not deny access when we did not add security annotations.

幸运的是,Spring Security默认要求对所有端点进行认证。然而,它不会要求特定的角色。另外,当我们没有添加安全注释时,它不会拒绝访问

3. Setup

3.设置

First, let’s take a look at the application for this example. We have a simple Spring Boot application:

首先,让我们来看看这个例子的应用。我们有一个简单的Spring Boot应用程序。

@SpringBootApplication
public class DenyApplication {
    public static void main(String[] args) {
        SpringApplication.run(DenyApplication.class, args);
    }
}

Secondly, we have a security configuration. We set up two users and enable the pre/post annotations:

第二,我们有一个安全配置。我们设置了两个用户并启用了前/后注释。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class DenyMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    @Bean
    public UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(
            User.withUsername("user").password("{noop}password").roles("USER").build(),
            User.withUsername("guest").password("{noop}password").roles().build()
        );
    }
}

Finally, we have a rest controller with two methods. However, we “forgot” to protect the /bye endpoint:

最后,我们有一个有两个方法的休息控制器。然而,我们 “忘记 “保护/bye端点。

@RestController
public class DenyOnMissingController {
    @GetMapping(path = "hello")
    @PreAuthorize("hasRole('USER')")
    public String hello() {
        return "Hello world!";
    }

    @GetMapping(path = "bye")
    // whoops!
    public String bye() {
        return "Bye bye world!";
    }
}

When running the example, we can sign in with user/password. Then, we access the /hello endpoint. We can also sign in with guest/guest. In that case, we cannot access the /hello endpoint.

运行这个例子时,我们可以用user/password登录。然后,我们访问/hello端点。我们也可以用guest/guest登录。在这种情况下,我们不能访问/hello端点。

However, any authenticated user can access the /bye endpoint. In the next section, we write a test to prove that.

然而,任何经过认证的用户都可以访问/bye端点。在下一节,我们写一个测试来证明这一点。

4. Testing the Solution

4.测试解决方案

Using MockMvc we can set up a test. We check that our non-annotated method is still accessible:

使用MockMvc我们可以设置一个测试。我们检查我们的非注解方法是否仍然可以访问。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DenyApplication.class)
public class DenyOnMissingControllerIntegrationTest {
    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    @Autowired
    private WebApplicationContext context;
    private MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
    }

    @Test
    @WithMockUser(username = "user")
    public void givenANormalUser_whenCallingHello_thenAccessDenied() throws Exception {
        mockMvc.perform(get("/hello"))
          .andExpect(status().isOk())
          .andExpect(content().string("Hello world!"));
    }

    @Test
    @WithMockUser(username = "user")
    // This will fail without the changes from the next section
    public void givenANormalUser_whenCallingBye_thenAccessDenied() throws Exception {
        expectedException.expectCause(isA(AccessDeniedException.class));

        mockMvc.perform(get("/bye"));
    }
}

The second test fails because the /bye endpoint is accessible. In the next section, we update our configuration to deny access to unannotated endpoints.

第二个测试失败了,因为/bye端点是可以访问的。在下一节中,我们更新了我们的配置,以拒绝对未注释的端点的访问

5. Solution: Deny by Default

5.解决方案 默认拒绝

Let’s extend our MethodSecurityConfig class and set up a MethodSecurityMetadataSource:

让我们扩展我们的MethodSecurityConfig类,并设置一个MethodSecurityMetadataSource:

@Configuration 
@EnableWebSecurity 
@EnableGlobalMethodSecurity(prePostEnabled = true) 
public class DenyMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    @Override
    protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() {
        return new CustomPermissionAllowedMethodSecurityMetadataSource();
    }
    // setting up in memory users not repeated
    ...
}

Now let’s implement the MethodSecurityMetadataSource interface:

现在我们来实现MethodSecurityMetadataSource接口。

public class CustomPermissionAllowedMethodSecurityMetadataSource 
  extends AbstractFallbackMethodSecurityMetadataSource {
    @Override
    protected Collection findAttributes(Class<?> clazz) { return null; }

    @Override
    protected Collection findAttributes(Method method, Class<?> targetClass) {
        Annotation[] annotations = AnnotationUtils.getAnnotations(method);
        List attributes = new ArrayList<>();

        // if the class is annotated as @Controller we should by default deny access to all methods
        if (AnnotationUtils.findAnnotation(targetClass, Controller.class) != null) {
            attributes.add(DENY_ALL_ATTRIBUTE);
        }

        if (annotations != null) {
            for (Annotation a : annotations) {
                // but not if the method has at least a PreAuthorize or PostAuthorize annotation
                if (a instanceof PreAuthorize || a instanceof PostAuthorize) {
                    return null;
                }
            }
        }
        return attributes;
    }

    @Override
    public Collection getAllConfigAttributes() { return null; }
}

We’ll add the DENY_ALL_ATTRIBUTE to all methods of @Controller classes.

我们将在@Controller类的所有方法中添加DENY_ALL_ATTRIBUTE

But, we don’t add them if a @PreAuthorize/@PostAuthorize annotation is found. We do this by returning null, indicating that no metadata applies.

但是,如果发现了@PreAuthorize/@PostAuthorize注释,我们就不添加它们。我们通过返回null来实现这一点,表示没有元数据适用

With the updated code, our /bye endpoint is protected and the tests succeed.

通过更新的代码,我们的/bye端点得到了保护,测试成功。

6. Conclusion

6.结语

In this short tutorial, we’ve shown how to protect endpoints lacking @PreAuthorize / @PostAuthorize annotations.

在这个简短的教程中,我们展示了如何保护缺乏@PreAuthorize / @PostAuthorize注释的端点

Also, we show that non-annotated methods are now indeed protected.

此外,我们还表明,非注释的方法现在确实受到了保护。

As always, the full source code of the article is available over on GitHub.

一如既往,该文章的完整源代码可在GitHub上获得