Using Security with JDBC

本指南演示 Quarkus 应用如何使用数据库来存储您的用户标识。

This guide demonstrates how your Quarkus application can use a database to store your user identities.

Prerequisites

Unresolved directive in security-jdbc.adoc - include::{includes}/prerequisites.adoc[]

Architecture

在此示例中,我们构建了一个非常简单的微服务,它提供三个端点:

In this example, we build a very simple microservice which offers three endpoints:

  • /api/public

  • /api/users/me

  • /api/admin

/api/public 端点可以匿名访问。/api/admin 端点受 RBAC(基于角色的访问控制)保护,只有被授予 admin 角色的用户才能访问。在此端点,我们使用 @RolesAllowed 注释来以声明方式强制访问约束。/api/users/me 端点也受 RBAC(基于角色的访问控制)保护,只有被授予 user 角色的用户才能访问。作为响应,它返回一个包含用户详细信息的 JSON 文档。

The /api/public endpoint can be accessed anonymously. The /api/admin endpoint is protected with RBAC (Role-Based Access Control) where only users granted with the admin role can access. At this endpoint, we use the @RolesAllowed annotation to declaratively enforce the access constraint. The /api/users/me endpoint is also protected with RBAC (Role-Based Access Control) where only users granted with the user role can access. As a response, it returns a JSON document with details about the user.

Solution

我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

克隆 Git 存储库: git clone {quickstarts-clone-url},或下载 {quickstarts-archive-url}[存档]。

Clone the Git repository: git clone {quickstarts-clone-url}, or download an {quickstarts-archive-url}[archive].

该解决方案位于 security-jdbc-quickstart directory 中。

The solution is located in the security-jdbc-quickstart directory.

Creating the Maven Project

首先,我们需要一个新项目。使用以下命令创建一个新项目:

First, we need a new project. Create a new project with the following command:

Unresolved directive in security-jdbc.adoc - include::{includes}/devtools/create-app.adoc[]

别忘了添加数据库连接器库。我们在此使用 PostgreSQL 作为身份存储。

Don’t forget to add the database connector library of choice. Here we are using PostgreSQL as identity store.

此命令生成一个新项目,引入 elytron-security-jdbc 扩展,该扩展是 Quarkus 应用的 wildfly-elytron-realm-jdbc 适配器。

This command generates a new project, importing the elytron-security-jdbc extension which is an wildfly-elytron-realm-jdbc adapter for Quarkus applications.

如果您已配置好 Quarkus 项目,可以通过在项目基本目录中运行以下命令将 elytron-security-jdbc 扩展添加到您的项目:

If you already have your Quarkus project configured, you can add the elytron-security-jdbc extension to your project by running the following command in your project base directory:

Unresolved directive in security-jdbc.adoc - include::{includes}/devtools/extension-add.adoc[]

这会将以下内容添加到构建文件中:

This will add the following to your build file:

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

Writing the application

首先,实现 /api/public 端点。正如您从下面的源代码中所见,它仅仅是一个常规的 Jakarta REST 资源:

Let’s start by implementing the /api/public endpoint. As you can see from the source code below, it is just a regular Jakarta REST resource:

package org.acme.security.jdbc;

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

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

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

/api/admin 端点的源代码也非常简单。此处的主要不同之处在于,我们使用 @RolesAllowed 注解来确保只有授予了 admin 角色的用户才可以访问此端点:

The source code for the /api/admin endpoint is also very simple. The main difference here is that we are using a @RolesAllowed annotation to make sure that only users granted with the admin role can access the endpoint:

package org.acme.security.jdbc;

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

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

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

最后,我们考虑 /api/users/me 端点。从下面的源代码中可以看到,我们只信任 user 角色的用户。我们使用 SecurityContext 获取当前经过身份验证的主体,然后返回用户的名称。此信息从数据库中加载。

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.jdbc;

import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.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();
    }
}

Configuring the Application

elytron-security-jdbc 扩展至少需要一个数据源来访问您的数据库。

The elytron-security-jdbc extension requires at least one datasource to access to your database.

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

在我们的上下文中,我们将 PostgreSQL 用作身份存储,并使用用户和角色初始化数据库。在此示例中,我们将 password 的加盐和哈希版本用作密码。我们可以使用 BcryptUtil 类以模块加密格式 (MCF) 生成密码。

In our context, we are using PostgreSQL as identity store, and we initialize the database with users and roles. We will use the salted and hashed version of password as a password in this example. We can use the BcryptUtil class to generate passwords in the Modular Crypt Format (MCF).

CREATE TABLE test_user (
  id INT,
  username VARCHAR(255),
  password VARCHAR(255),
  role VARCHAR(255)
);

INSERT INTO test_user (id, username, password, role) VALUES (1, 'admin', '$2a$10$Uc.SZ0hvGJQlYdsAp7be1.lFjmOnc7aAr4L0YY3/VN3oK.F8zJHRG', 'admin');
INSERT INTO test_user (id, username, password, role) VALUES (2, 'user','$2a$10$Uc.SZ0hvGJQlYdsAp7be1.lFjmOnc7aAr4L0YY3/VN3oK.F8zJHRG', 'user');

在注册新用户时,我们可以按如下方法加密他们的密码:

When signing up new users, we can encrypt their password as follows:

package org.acme.security.jdbc;

import io.quarkus.elytron.security.common.BcryptUtil;

public class AccountService {

    public void signupUser(String username, String password) {
        String encryptedPassword = BcryptUtil.bcryptHash(password);

        // store user with the encrypted password in the database
    }
}

我们现在可以配置 Elytron JDBC Realm。

We can now configure the Elytron JDBC Realm.

quarkus.security.jdbc.enabled=true
quarkus.security.jdbc.principal-query.sql=SELECT u.password, u.role FROM test_user u WHERE u.username=? 1
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.enabled=true 2
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.password-index=1
quarkus.security.jdbc.principal-query.attribute-mappings.0.index=2 3
quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups

elytron-security-jdbc 扩展至少需要一个主体查询来验证用户及其身份。

The elytron-security-jdbc extension requires at least one principal query to authenticate the user and its identity.

1 We define a parameterized SQL statement (with exactly 1 parameter) which should return the user’s password plus any additional information you want to load.
2 The password mapper is configured with the position of the password field in the SELECT fields. The hash is stored in the Modular Crypt Format (MCF) because the salt and iteration count indexes are set to -1 by default. You can override them in order to decompose each element into three separate columns.
3 We use attribute-mappings to bind the SELECT projection fields (i.e. u.role here) to the target Principal representation attributes.

principal-query 配置中,所有 index 属性从 1 开始(而非 0)。

In the principal-query configuration all the index properties start at 1 (rather than 0).

Testing the Application

应用程序现在受到保护,并且数据库提供了身份。首先要检查的是确保匿名访问有效。

The application is now protected and the identities are provided by our database. The very first thing to check is to ensure the anonymous access works.

$ 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%

目前为止还不错,现在我们尝试使用允许的用户。

So far so good, now let’s try with an allowed user.

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

admin%

通过提供 admin:password 凭据,该扩展对用户进行身份验证并加载其角色。 admin 用户被授权访问受保护的资源。

By providing the admin:password credentials, the extension authenticated the user and loaded their roles. The admin user is authorized to access to the protected resources.

用户 admin 应禁止访问受 @RolesAllowed("user") 保护的资源,因为它没有这个角色。

The user admin should be forbidden to access a resource protected with @RolesAllowed("user") because it doesn’t have this role.

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

Forbidden%

最后,使用用户 user 有效,并且安全上下文包含主体详细信息(例如用户名)。

Finally, using the user user works and the security context contains the principal details (username for instance).

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

user%

Advanced Configuration

本指南仅涵盖了一个简单的用例,该扩展提供了多个数据源、多个主体查询配置以及一个 bcrypt 密码映射器。

This guide only covered an easy use case, the extension offers multiple datasources, multiple principal queries configuration as well as a bcrypt password mapper.

quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=quarkus
quarkus.datasource.password=quarkus
quarkus.datasource.jdbc.url=jdbc:postgresql:multiple-data-sources-users

quarkus.datasource.permissions.db-kind=postgresql
quarkus.datasource.permissions.username=quarkus
quarkus.datasource.permissions.password=quarkus
quarkus.datasource.permissions.jdbc.url=jdbc:postgresql:multiple-data-sources-permissions

quarkus.security.jdbc.enabled=true
quarkus.security.jdbc.principal-query.sql=SELECT u.password FROM test_user u WHERE u.username=?
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.enabled=true
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.password-index=1

quarkus.security.jdbc.principal-query.roles.sql=SELECT r.role_name FROM test_role r, test_user_role ur WHERE ur.username=? AND ur.role_id = r.id
quarkus.security.jdbc.principal-query.roles.datasource=permissions
quarkus.security.jdbc.principal-query.roles.attribute-mappings.0.index=1
quarkus.security.jdbc.principal-query.roles.attribute-mappings.0.to=groups

Configuration Reference

Unresolved directive in security-jdbc.adoc - include::{generated-dir}/config/quarkus-elytron-security-jdbc.adoc[]