CLOVER🍀

That was when it all began.

RESTEasyでCORSの設定をFilterで行う

これは、なにをしたくて書いたもの?

そういえば、Jakarta EE(Java EE)でCORSの設定をしたことがないなと思いまして。

実装方法はいろいろあると思うのですが、すでに用意されているものとかないのかなと思って少し見ていたら、RESTEasyにあったので
軽く試してみることにしました。

RESTEasyのCorsFilter

CORS(オリジン間リソース共有)および使用するヘッダーについては、MDNを参照ということで。

オリジン間リソース共有 (CORS) - HTTP | MDN

RESTEasyが提供するCorsFilterについてのドキュメントはこちらです。

Chapter 32. CORS

ほぼ内容がありませんが。設定内容についてはJavadocを見てね、という感じです。

CorsFilter (RESTEasy 6.2.8.Final API)

設定をしたら、Applicationにシングルトンとして登録します。

You must allocate this and register it as a singleton provider from your Application class.

これ以上の説明はないので、実際に試してみましょう。

Java SE環境でSeBootstrapを使い、簡単なJakarta RESTful Web Services(JAX-RS)のリソースクラスを作成して確認することにします。

環境

今回の環境はこちら。

$ java --version
openjdk 21.0.2 2024-01-16
OpenJDK Runtime Environment (build 21.0.2+13-Ubuntu-122.04.1)
OpenJDK 64-Bit Server VM (build 21.0.2+13-Ubuntu-122.04.1, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 21.0.2, vendor: Private Build, runtime: /usr/lib/jvm/java-21-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-105-generic", arch: "amd64", family: "unix"

サンプルコードを用意する

それでは、まずはサンプルとなるソースコードを用意します。

Maven依存関係など。

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-core</artifactId>
            <version>6.2.8.Final</version>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-undertow-cdi</artifactId>
            <version>6.2.8.Final</version>
        </dependency>
    </dependencies>

今回使用するCorsFilterresteasy-coreに含まれています。Java SE環境でRESTEasyを起動するためのresteasy-undertow-cdiにも
含まれているのですが、明示的に指定することにしました。

なんとなくbeans.xmlも用意しておきます。中身は空で。

src/main/resources/META-INF/beans.xml




Applicationのサブクラス。

src/main/java/org/littlewings/resteasy/cors/RestApplication.java

package org.littlewings.resteasy.cors;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
import org.jboss.resteasy.plugins.interceptors.CorsFilter;

import java.util.Set;

@ApplicationPath("")
public class RestApplication extends Application {
    @Override
    public Set<Class<?>> getClasses() {
        return Set.of(HelloResource.class);
    }

    @Override
    public Set<Object> getSingletons() {
        CorsFilter corsFilter = new CorsFilter();
        // 後で

        return Set.of(corsFilter);
    }
}

JAX-RSリソースクラス。

src/main/java/org/littlewings/resteasy/cors/HelloResource.java

package org.littlewings.resteasy.cors;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.container.PreMatching;
import jakarta.ws.rs.core.MediaType;

@Path("hello")
public class HelloResource {
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String message() {
        return "Hello World!!";
    }
}

mainクラス。

src/main/java/org/littlewings/resteasy/cors/App.java

package org.littlewings.resteasy.cors;

import jakarta.ws.rs.SeBootstrap;
import org.jboss.logging.Logger;

import java.util.concurrent.ExecutionException;

public class App {
    public static void main(String... args) throws ExecutionException, InterruptedException {
        Logger logger = Logger.getLogger(App.class);

        SeBootstrap.Configuration configuration =
                SeBootstrap
                        .Configuration
                        .builder()
                        .host("0.0.0.0")
                        .port(8080)
                        .build();

        SeBootstrap.Instance instance =
                SeBootstrap
                        .start(new RestApplication(), configuration)
                        .toCompletableFuture()
                        .get();

        logger.info("server startup.");
        System.console().readLine("> Enter stop.");

        instance
                .stop()
                .toCompletableFuture()
                .get();
    }
}

これで準備は完了です。

CorsFilterを設定する

では、Applicationのサブクラスに書いていたCorsFilterの設定をします。

今回はこんな感じにしました。

    @Override
    public Set<Object> getSingletons() {
        CorsFilter corsFilter = new CorsFilter();
        corsFilter.getAllowedOrigins().add("http://localhost:3000");
        corsFilter.setAllowedMethods("POST, GET, PUT, DELETE, OPTIONS");
        corsFilter.setAllowedHeaders("Content-Type, Origin, Authorization");
        corsFilter.setCorsMaxAge(86400);
        corsFilter.setAllowCredentials(true);

        return Set.of(corsFilter);
    }

Access-Control-Allow-Originに対する設定方法がないのでは…?と思ったのですが、Setを取得してこちらに直接追加するみたいです…。

そういえば、ドキュメントにもそう書いていました…。

CorsFilter filter = new CorsFilter();
filter.getAllowedOrigins().add("http://localhost");

Chapter 32. CORS

その他はsetterがあるんですけどね。

CorsFilter (RESTEasy 6.2.8.Final API)

確認する

準備はできたので、確認してみます。

起動。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.resteasy.cors.App

起動しました。

INFO: server startup.
> Enter stop.

設定したヘッダーがレスポンスに含まれるかどうか、OPTIONSで確認。

$ curl -i -XOPTIONS -H 'Origin: http://localhost:3000' localhost:8080/hello
HTTP/1.1 200 OK
Connection: keep-alive
Access-Control-Allow-Origin: http://localhost:3000
Vary: Origin
Access-Control-Allow-Credentials: true
Content-Length: 0
Access-Control-Max-Age: 86400
Date: Sat, 04 May 2024 12:02:17 GMT

指定のオリジンからアクセス。

$ curl -i -H 'Origin: http://localhost:3000' localhost:8080/hello
HTTP/1.1 200 OK
Connection: keep-alive
Access-Control-Allow-Origin: http://localhost:3000
Vary: Origin
Access-Control-Allow-Credentials: true
Content-Type: text/plain;charset=UTF-8
Content-Length: 13
Date: Sat, 04 May 2024 12:03:05 GMT

Hello World!!

許可していないオリジンからのアクセス。

$ curl -i -H 'Origin: http://example.com' localhost:8080/hello
HTTP/1.1 403 Forbidden
Connection: keep-alive
Content-Length: 0
Date: Sat, 04 May 2024 12:03:41 GMT

拒否されました。

OKですね。

少しポイントを

CorsFilterJAX-RSContainerRequestFilterContainerResponseFilterとして実装されています。

CorsFilter (RESTEasy 6.2.8.Final API)

このフィルターには@PreMatchingというアノテーションが付与されているのですが、これはリソースメソッドにマッチする前に
適用することを指示するもののようです。

As stated above, a ContainerRequestFilter that is annotated with @PreMatching is executed upon receiving a client request but before a resource method is matched. Thus, this type of filter has the ability to modify the input to the matching algorithm (see Request Matching) and, consequently, alter its outcome.

Jakarta RESTful Web Services / Filters and Interceptors / Filters

ところで、今回のようなCorsFilterApplication#getSingletonsを使って登録するしかなさそうなのですが、現在このメソッドは
非推奨になっているようです。

Deprecated. Automatic discovery of resources and providers or the getClasses method is preferred over getSingletons.

Application#getSingletons)

今後はどうしたらいいんでしょうね…。

WildFlyでの設定は?

今回はRESTEasyが実装しているクラスを使いましたが、WildFlyでやろうとするとundertowサブシステムのfilterを使って登録することに
なるようです。

WildFly Full Model Reference / subsystem=undertow / configuration=filter / response-header

おわりに

RESTEasyの提供するCorsFilterを使ってCORSの設定をしてみました。

本当、実装方法としてはいろいろと考えられると思うのですが、どうするのが一般的(?)なんでしょうね。