The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

JPAでセキュリティーを使用

このガイドでは、QuarkusアプリケーションでHibernate ORMまたは Hibernate ORM with Panacheと一緒にデータベースを使用してユーザーIDを保存する方法を説明します。

前提条件

このガイドを完成させるには、以下が必要です:

  • 約15分

  • IDE

  • JDK 11+ がインストールされ、 JAVA_HOME が適切に設定されていること

  • Apache Maven 3.8.1+

  • 使用したい場合、 Quarkus CLI

  • ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること

アーキテクチャ

この例では、3つのエンドポイントを提供する非常にシンプルなマイクロサービスを構築します:

  • /api/public

  • /api/users/me

  • /api/admin

/api/public エンドポイントは匿名でアクセスできます。 /api/admin エンドポイントは RBAC (Role-Based Access Control) で保護されており、 admin の役割を与えられたユーザーのみがアクセスできます。このエンドポイントでは、 @RolesAllowed アノテーションを使用して、アクセス制約を宣言的に強制します。 /api/users/me エンドポイントも RBAC (Role-Based Access Control) で保護されており、 user ロールで付与されたユーザーのみがアクセスできます。レスポンスとして、ユーザーに関する詳細を含むJSONドキュメントを返します。

ソリューション

次のセクションで紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。

Gitレポジトリをクローンするか git clone https://github.com/quarkusio/quarkus-quickstarts.gitアーカイブ をダウンロードします。

ソリューションは security-jpa-quickstart ディレクトリ にあります。

Mavenプロジェクトの作成

まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します:

CLI
quarkus create app org.acme:security-jpa-quickstart \
    --extension=security-jpa,jdbc-postgresql,resteasy-reactive,hibernate-orm-panache \
    --no-code
cd security-jpa-quickstart

Gradleプロジェクトを作成するには、 --gradle または --gradle-kotlin-dsl オプションを追加します。

Quarkus CLIのインストール方法については、Quarkus CLIガイドをご参照ください。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:2.11.1.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=security-jpa-quickstart \
    -Dextensions="security-jpa,jdbc-postgresql,resteasy-reactive,hibernate-orm-panache" \
    -DnoCode
cd security-jpa-quickstart

Gradleプロジェクトを作成するには、 -DbuildTool=gradle または -DbuildTool=gradle-kotlin-dsl オプションを追加します。

選択したデータベースコネクタライブラリを追加することを忘れないでください。ここでは、PostgreSQLをIDストアとして使用しています。

このコマンドは、セキュリティー ソースを JPA エンティティーにマップできる security-jpa エクステンションをインポートして、Maven プロジェクトを生成します。

すでにQuarkusプロジェクトが設定されている場合は、プロジェクトのベースディレクトリーで以下のコマンドを実行することで、プロジェクトに security-jpa エクステンションを追加することができます:

CLI
quarkus extension add 'security-jpa'
Maven
./mvnw quarkus:add-extension -Dextensions="security-jpa"
Gradle
./gradlew addExtension --extensions="security-jpa"

これにより、ビルドファイルに以下が追加されます:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-security-jpa</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-security-jpa")

アプリケーションの記述

/api/public エンドポイントの実装から始めましょう。以下のソースコードから分かるように、通常のJAX-RSリソースです:

package org.acme.security.jpa;

import javax.annotation.security.PermitAll;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/api/public")
public class PublicResource {

    @GET
    @PermitAll
    @Produces(MediaType.TEXT_PLAIN)
    public String publicResource() {
        return "public";
   }
}

/api/admin エンドポイントのソースコードも非常にシンプルです。ここでの主な違いは、 admin ロールで付与されたユーザーだけがエンドポイントにアクセスできるように @RolesAllowed アノテーションを使用していることです:

package org.acme.security.jpa;

import javax.annotation.security.RolesAllowed;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/api/admin")
public class AdminResource {

    @GET
    @RolesAllowed("admin")
    @Produces(MediaType.TEXT_PLAIN)
    public String adminResource() {
         return "admin";
    }
}

Finally, let’s consider the /api/users/me endpoint. As you can see from the source code below, we are trusting only users with the user role. We are using SecurityContext to get access to the current authenticated Principal, and we return the user’s name. This information is loaded from the database.

package org.acme.security.jpa;

import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.SecurityContext;

@Path("/api/users")
public class UserResource {

    @GET
    @RolesAllowed("user")
    @Path("/me")
    public String me(@Context SecurityContext securityContext) {
        return securityContext.getUserPrincipal().getName();
    }
}

ユーザーエンティティーの定義

これで、 User エンティティーにいくつかのアノテーションを追加することで、セキュリティー情報がモデルにどのように保存されているかを説明することができます:

package org.acme.security.jpa;

import javax.persistence.Entity;
import javax.persistence.Table;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.security.jpa.Password;
import io.quarkus.security.jpa.Roles;
import io.quarkus.security.jpa.UserDefinition;
import io.quarkus.security.jpa.Username;

@Entity
@Table(name = "test_user")
@UserDefinition (1)
public class User extends PanacheEntity {
    @Username (2)
    public String username;
    @Password (3)
    public String password;
    @Roles (4)
    public String role;

    /**
     * Adds a new user in the database
     * @param username the username
     * @param password the unencrypted password (it will be encrypted with bcrypt)
     * @param role the comma-separated roles
     */
    public static void add(String username, String password, String role) { (5)
        User user = new User();
        user.username = username;
        user.password = BcryptUtil.bcryptHash(password);
        user.role = role;
        user.persist();
    }
}

security-jpa エクステンションは、 @UserDefinition でアノテーションされた単一のエンティティーがある場合にのみ初期化されます。

1 このアノテーションは、単一のエンティティーに存在しなければなりません。この例のように、通常のHibernate ORMエンティティーまたはHibernate ORM with Panacheエンティティーにすることができます。
2 This indicates the field used for the username.
3 これは、パスワードに使用するフィールドを示します。これはデフォルトでは bcrypt ハッシュ化されたパスワードを使用するように設定されていますが、クリアテキストパスワードやカスタムパスワード用に設定することもできます。
4 これは、対象のプリンシパル表現属性に追加されたロールのコンマ区切りリストを示します。
5 この方法では、パスワードを適切なbcryptハッシュでハッシュしながらユーザーを追加することができます。

アプリケーションの設定

security-jpa エクステンションは、データベースにアクセスするために少なくとも一つのデータソースが必要です。

quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=quarkus
quarkus.datasource.password=quarkus
quarkus.datasource.jdbc.url=jdbc:postgresql:security_jpa

quarkus.hibernate-orm.database.generation=drop-and-create

In our context, we are using PostgreSQL as identity store. The database schema is created by Hibernate ORM automatically on startup (change this in production), and we initialize the database with users and roles in the Startup class:

package org.acme.security.jpa;

import javax.enterprise.event.Observes;
import javax.inject.Singleton;
import javax.transaction.Transactional;

import io.quarkus.runtime.StartupEvent;


@Singleton
public class Startup {
    @Transactional
    public void loadUsers(@Observes StartupEvent evt) {
        // reset and load all test users
        User.deleteAll();
        User.add("admin", "admin", "admin");
        User.add("user", "user", "user");
    }
}

It is probably useless, but we kindly remind you that you must not store clear-text passwords in production environments ;-). As a result, the security-jpa defaults to using bcrypt-hashed passwords.

アプリケーションのテスト

開発モードでアプリケーションを起動するには、次のようにします:

CLI
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

以下のテストでは、基本的な認証メカニズムを使用していますが、 application.properties ファイルに quarkus.http.auth.basic=true を設定することで有効にすることができます。

アプリケーションが保護され、アイデンティティがデータベースから提供されるようになりました。非常に最初に確認しなければならないことは、匿名アクセスが機能することを確認することです。

$ curl -i -X GET http://localhost:8080/api/public
HTTP/1.1 200 OK
Content-Length: 6
Content-Type: text/plain;charset=UTF-8

public%

Now, let’s try to hit a protected resource anonymously.

$ curl -i -X GET http://localhost:8080/api/admin
HTTP/1.1 401 Unauthorized
Content-Length: 14
Content-Type: text/html;charset=UTF-8

Not authorized%

ここまでは順調ですが、今度は許可されたユーザーで試してみましょう。

$ curl -i -X GET -u admin:admin http://localhost:8080/api/admin
HTTP/1.1 200 OK
Content-Length: 5
Content-Type: text/plain;charset=UTF-8

admin%

admin:admin 資格情報を提供することで、エクステンションはユーザーを認証し、そのロールをロードしました。 admin ユーザーは、保護されたリソースへのアクセスを許可されています。

ユーザー admin は、この役割を持っていないので、 @RolesAllowed("user") で保護されたリソースへのアクセスを禁止する必要があります。

$ curl -i -X GET -u admin:admin http://localhost:8080/api/users/me
HTTP/1.1 403 Forbidden
Content-Length: 34
Content-Type: text/html;charset=UTF-8

Forbidden%

最後に、ユーザー user を使用すると動作し、セキュリティーコンテキストには主要な詳細(例えばユーザー名)が含まれています。

$ curl -i -X GET -u user:user http://localhost:8080/api/users/me
HTTP/1.1 200 OK
Content-Length: 4
Content-Type: text/plain;charset=UTF-8

user%

サポートされているモデルの種類

  • @UserDefinition クラスは JPA エンティティーである必要があります(Panache を使用しているかどうかは問いません)。

  • @Username@Password フィールドの型は String でなければなりません。

  • @Roles フィールドは StringCollection<String> のいずれかのタイプであるか、または Collection<X> である必要があります。 X はエンティティークラスで、 @RolesValue アノテーションが付与された String フィールドが 1 つあります。

  • String role 要素の型は、カンマで区切られたロールのリストとして解析されます。

別のエンティティーにロールを格納する

また、別のエンティティーにロールを格納することもできます。

@UserDefinition
@Table(name = "test_user")
@Entity
public class User extends PanacheEntity {
    @Username
    public String name;

    @Password
    public String pass;

    @ManyToMany
    @Roles
    public List<Role> roles = new ArrayList<>();
}

@Entity
public class Role extends PanacheEntity {

    @ManyToMany(mappedBy = "roles")
    public List<ExternalRolesUserEntity> users;

    @RolesValue
    public String role;
}

パスワードの保存とハッシュ化

デフォルトでは、パスワードは MCF ( Modular Crypt Format) の下で bcryptでハッシュ化されて保存されると考えています。

このようなハッシュ化されたパスワードを作成する必要がある場合は、便利な String BcryptUtil.bcryptHash(String password) 関数を用意しています。デフォルトでは、ランダムなソルトを作成して 10 回の繰り返しでハッシュ化します (繰り返しとソルトも指定できます)。

MCF を使うと、ハッシュアルゴリズムや反復回数、 ソルトを格納するための専用のカラムは必要ありません。

また、異なるハッシュアルゴリズム( @Password(value = PasswordType.CUSTOM, provider = CustomPasswordProvider.class) )を使用してパスワードを保存することもできます。

@UserDefinition
@Table(name = "test_user")
@Entity
public class CustomPasswordUserEntity {
    @Id
    @GeneratedValue
    public Long id;

    @Column(name = "username")
    @Username
    public String name;

    @Column(name = "password")
    @Password(value = PasswordType.CUSTOM, provider = CustomPasswordProvider.class)
    public String pass;

    @Roles
    public String role;
}

public class CustomPasswordProvider implements PasswordProvider {
    @Override
    public Password getPassword(String pass) {
        byte[] digest = DatatypeConverter.parseHexBinary(pass);
        return SimpleDigestPassword.createRaw(SimpleDigestPassword.ALGORITHM_SIMPLE_DIGEST_SHA_256, digest);
    }
}

警告: @Password(PasswordType.CLEAR) を使ってパスワードをクリアテキストで保存することもできますが、本番では絶対にしないことを強くお勧めします。