Spring Securityロールに基づいたJackson JSON出力のフィルタリング

1. 概要

このクイックチュートリアルでは、Spring Securityで定義されたユーザーロールに応じてJSONシリアル化出力をフィルター処理する方法を示します。

2. なぜフィルタリングする必要があるのですか?

さまざまな役割を持つユーザーにサービスを提供するウェブアプリケーションがある、シンプルでありながら一般的な使用例を考えてみましょう。 たとえば、これらのロールを_User_および_Admin_とします。
まず、* _ Admins_がパブリックREST APIを介して公開されるオブジェクトの内部状態*に完全にアクセスできるという要件を定義しましょう。 それどころか、* _Users_には事前定義されたオブジェクトのプロパティセットのみが表示されるはずです。*
link:/security-spring[Spring Security framework]を使用して、Webアプリケーションリソースへの不正アクセスを防ぎます。
APIでREST応答ペイロードとして返すオブジェクトを定義しましょう:
class Item {
    private int id;
    private String name;
    private String ownerName;

    // getters
}
もちろん、アプリケーションに存在するロールごとに個別のデータ転送オブジェクトクラスを定義することもできます。 ただし、このアプローチでは、コードベースに無駄な複製や洗練されたクラス階層が導入されます。
一方、* https://www.baeldung.com/jackson-json-view-annotation [JacksonライブラリのJSONビュー]機能*を使用することもできます。 次のセクションで説明するように、フィールドに注釈を追加するのと同じくらい簡単にJSON表現をカスタマイズできます。

3. _ @ JsonView_アノテーション

Jacksonライブラリは、JSON表現に含めるフィールドをwith_ @ JsonView_アノテーションでマークすることにより、*複数のシリアル化/逆シリアル化コンテキスト*の定義をサポートしています。 この注釈には、コンテキストを区別するために使用される_Class_型の*必須パラメーター*があります。
クラス内のフィールドを_ @ JsonView_でマークする場合、デフォルトでは、シリアル化コンテキストには、ビューの一部として明示的にマークされていないすべてのプロパティが含まれることに留意する必要があります。 この動作をオーバーライドするには、_DEFAULT_VIEW_INCLUSION_マッパー機能を無効にすることができます。
まず、* _ @ JsonView_アノテーションの引数として使用するいくつかの内部クラスで_View_クラスを定義しましょう*:
class View {
    public static class User {}
    public static class Admin extends User {}
}
次に、クラスに_ @ JsonView_アノテーションを追加し、_ownerName_をadminロールのみがアクセスできるようにします。
@JsonView(View.User.class)
private int id;
@JsonView(View.User.class)
private String name;
@JsonView(View.Admin.class)
private String ownerName;

4.  _ @ JsonView_アノテーションとSpring Securityを統合する方法

次に、すべてのロールとその名前を含む列挙を追加しましょう。 その後、JSONビューとセキュリティロール間のマッピングを紹介しましょう。
enum Role {
    ROLE_USER,
    ROLE_ADMIN
}

class View {

    public static final Map<Role, Class> MAPPING = new HashMap<>();

    static {
        MAPPING.put(Role.ADMIN, Admin.class);
        MAPPING.put(Role.USER, User.class);
    }

    //...
}
最後に、統合の中心点に到達しました。 JSONビューとSpring Securityロールを結び付けるには、アプリケーションのすべてのコントローラーメソッドに適用される*コントローラーアドバイスを定義する必要があります。
これまでのところ、行う必要があるのは、_AbstractMappingJacksonResponseBodyAdvice_ *クラスの_beforeBodyWriteInternal_メソッドをオーバーライドすることだけです。
@RestControllerAdvice
class SecurityJsonViewControllerAdvice extends AbstractMappingJacksonResponseBodyAdvice {

    @Override
    protected void beforeBodyWriteInternal(
      MappingJacksonValue bodyContainer,
      MediaType contentType,
      MethodParameter returnType,
      ServerHttpRequest request,
      ServerHttpResponse response) {
        if (SecurityContextHolder.getContext().getAuthentication() != null
          && SecurityContextHolder.getContext().getAuthentication().getAuthorities() != null) {
            Collection<? extends GrantedAuthority> authorities
              = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
            List<Class> jsonViews = authorities.stream()
              .map(GrantedAuthority::getAuthority)
              .map(AppConfig.Role::valueOf)
              .map(View.MAPPING::get)
              .collect(Collectors.toList());
            if (jsonViews.size() == 1) {
                bodyContainer.setSerializationView(jsonViews.get(0));
                return;
            }
            throw new IllegalArgumentException("Ambiguous @JsonView declaration for roles "
              + authorities.stream()
              .map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")));
        }
    }
}
このように、*アプリケーションからのすべての応答はこのアドバイスを通過し*、定義したロールマッピングに従って適切なビュー表現を見つけます。 このアプローチでは、*複数のロールを持つユーザーを扱う際には注意する必要があることに注意してください*。

5. 結論

この簡単なチュートリアルでは、Spring Securityロールに基づいてWebアプリケーションでJSON出力をフィルタリングする方法を学びました。
関連するすべてのコードは、https://github.com/eugenp/tutorials/tree/master/spring-security-mvc-jsonview [Github上]にあります。