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プロジェクトの作成
まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します:
選択したデータベースコネクタライブラリを追加することを忘れないでください。ここでは、PostgreSQLをIDストアとして使用しています。 |
このコマンドは、セキュリティー ソースを JPA エンティティーにマップできる security-jpa
エクステンションをインポートして、Maven プロジェクトを生成します。
すでにQuarkusプロジェクトが設定されている場合は、プロジェクトのベースディレクトリーで以下のコマンドを実行することで、プロジェクトに security-jpa
エクステンションを追加することができます:
quarkus extension add 'security-jpa'
./mvnw quarkus:add-extension -Dextensions="security-jpa"
./gradlew addExtension --extensions="security-jpa"
これにより、ビルドファイルに以下が追加されます:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security-jpa</artifactId>
</dependency>
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 |
アプリケーションのテスト
開発モードでアプリケーションを起動するには、次のようにします:
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
以下のテストでは、基本的な認証メカニズムを使用していますが、 |
アプリケーションが保護され、アイデンティティがデータベースから提供されるようになりました。非常に最初に確認しなければならないことは、匿名アクセスが機能することを確認することです。
$ 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
フィールドはString
かCollection<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)
を使ってパスワードをクリアテキストで保存することもできますが、本番では絶対にしないことを強くお勧めします。