1. 序章

Springメソッドのセキュリティに関するチュートリアルでは、@PreAuthorizeおよび@PostAuthorizeアノテーションを使用する方法を説明しました。

このチュートリアルでは、承認アノテーションのないメソッドへのアクセスを拒否する方法について説明します。

2. デフォルトのセキュリティ

結局のところ、私たちは人間にすぎないため、エンドポイントの1つを保護することを忘れる可能性があります。 残念ながら、注釈のないエンドポイントへのアクセスを拒否する簡単な方法はありません。

幸い、Spring Securityでは、デフォルトですべてのエンドポイントの認証が必要です。 ただし、特定の役割は必要ありません。 また、セキュリティ注釈を追加しなかった場合、はアクセスを拒否しません。

3. 設定

まず、この例のアプリケーションを見てみましょう。 単純なSpringBootアプリケーションがあります。

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

次に、セキュリティ構成があります。 2人のユーザーを設定し、前後の注釈を有効にします。

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

最後に、2つの方法を持つRESTコントローラーがあります。 ただし、 /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!";
    }
}

この例を実行すると、 user / passwordでサインインできます。 次に、 /helloエンドポイントにアクセスします。 ゲスト/ゲストでサインインすることもできます。 その場合、 /helloエンドポイントにアクセスできません。

ただし、認証されたユーザーは/byeエンドポイントにアクセスできます。 次のセクションでは、それを証明するためのテストを作成します。

4. ソリューションのテスト

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

/ bye エンドポイントにアクセスできるため、2番目のテストは失敗します。 次のセクションでは、構成を更新して、注釈のないエンドポイントへのアクセスを拒否します。

5. 解決策:デフォルトで拒否

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

次に、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; }
}

DENY_ALL_ATTRIBUTE@Controllerクラスのすべてのメソッドに追加します。

ただし、 @PreAuthorize / @PostAuthorize アノテーションが見つかった場合は、それらを追加しません。 これを行うには、 null メタデータが適用されないことを示すを返します。

更新されたコードにより、 / bye エンドポイントが保護され、テストが成功します。

6. 結論

この短いチュートリアルでは、 @PreAuthorize/@PostAuthorizeアノテーションがないエンドポイントを保護する方法を示しました。

また、注釈のないメソッドが実際に保護されていることも示します。

いつものように、記事の完全なソースコードは、GitHubから入手できます。