认证与授权

通用认证与授权

此 Vert.x 组件提供认证和授权接口,可用于您的 Vert.x 应用程序,并可由不同的提供者支持。

Vert.x auth 也被 vertx-web 用于处理其认证和授权。

要使用此项目,请将以下依赖项添加到您的构建描述符的依赖项部分

  • Maven(在您的 pom.xml 中)

<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-auth-common</artifactId>
  <version>5.0.1</version>
</dependency>
  • Gradle(在您的 build.gradle 文件中)

compile 'io.vertx:vertx-auth-common:5.0.1'

基本概念

认证意味着验证用户的身份。

授权意味着验证用户是否有权执行特定任务

为支持多种模型并保持高度灵活性,所有授权操作均在类型 Authorization 上执行。

在某些情况下,授权可能代表一种权限,例如访问所有打印机或特定打印机的权限。在其他情况下,授权可能是一个角色(例如:'admin'、'manager'等)。为了提供一小部分实现,以下工厂可用:

这组授权代表了任何类型的授权,例如:

  • 基于角色的授权

  • 基于权限的授权

  • 逻辑授权(AND、OR、NOT)

  • 基于时间的授权(例如:允许在每月的最后 5 天,早上 8 点到 10 点访问等)

  • 基于上下文的授权(例如:如果 IP 地址是 'xxx.xxx.xxx.xxx' 则允许访问)

  • 基于自定义的授权(例如:基于脚本或针对特定应用程序硬编码的代码)

  • 等等…​

要了解特定的 AuthorizationProvider 期望什么,请查阅该认证提供者的文档。

认证

要认证用户,请使用 authenticate

第一个参数是一个包含认证信息的 Credentials 对象。它实际包含的内容取决于具体的实现;对于简单的基于用户名/密码的认证,它可能包含类似以下内容:

{
  "username": "tim",
  "password": "mypassword"
}

对于基于 JWT 令牌或 OAuth bearer 令牌的实现,它可能包含令牌信息。

认证是异步进行的,结果通过调用中提供的结果处理器传递给用户。异步结果包含一个 User 实例,该实例代表已认证的用户。

认证用户对象没有关于该对象有权获得哪些授权的上下文或信息。授权和认证分离的原因是,认证和授权是两个不同的操作,不需要在同一提供者上执行。一个简单的例子是,使用普通的 OAuth2.0 进行认证的用户可以使用 JWT 授权提供者来匹配给定权限的令牌,或者其他任何场景,例如使用 LDAP 进行认证并使用 MongoDB 执行授权。

这是一个使用简单用户名/密码实现来认证用户的示例:

Credentials authInfo =
  new UsernamePasswordCredentials("tim", "mypassword");

authProvider.authenticate(authInfo)
  .onSuccess(user -> {
    System.out.println("User " + user.principal() + " is now authenticated");
  })
  .onFailure(Throwable::printStackTrace);

授权

一旦您拥有一个 User 实例,您可以调用 authorizations 来获取其授权。新创建的用户将不包含任何授权。您可以直接在 User 本身或通过 AuthorizationProvider 添加授权。

上述所有结果都在处理器中异步提供。

这是一个通过 AuthorizationProvider 添加授权的示例:

authorizationProvider.getAuthorizations(user)
  .onSuccess(done -> {
    // cache is populated, perform query
    if (PermissionBasedAuthorization.create("printer1234").match(user)) {
      System.out.println("User has the authority");
    } else {
      System.out.println("User does not have the authority");
    }
  });

另一个在基于角色的模型中进行授权的示例,它使用接口 RoleBasedAuthorization

请注意,如上所述,权限字符串的解释完全由底层实现决定,Vert.x 在此不作任何假设。

列出授权

用户对象持有一系列授权,因此后续调用应检查其是否具有相同的授权,这将避免对底层授权提供者执行一次额外的 I/O 操作来加载授权。

为了清除授权列表,您可以使用 clear

用户主体和属性

您可以使用 principal 获取与已认证用户对应的主体。

此返回的内容取决于底层实现。主体映射是用于创建用户实例的源数据。属性是额外的属性,它们在实例创建时提供,但作为用户数据处理的结果而产生。这种区分旨在确保对主体的处理不会篡改或覆盖现有数据。

为了简化使用,可以使用两个辅助方法在两个源上查找和读取值

if (user.containsKey("sub")) {
  // the check will first assert that the attributes contain
  // the given key and if not assert that the principal contains
  // the given key

  // just like the check before the get will follow the same
  // rules to retrieve the data, first "attributes" then "principal"
  String sub = user.get("sub");
}

创建您自己的认证或授权提供者实现

如果您希望创建自己的认证提供者,您应该实现以下一个或两个接口:

用户工厂可以使用给定的 principal JSON 内容创建一个 User 对象。可选地,可以提供第二个参数 attributes 以提供额外的元数据供以后使用。例如以下属性:

  • exp - 过期时间(秒)。

  • iat - 签发时间(秒)。

  • nbf - 生效时间(秒)。

  • leeway - 时钟漂移容限(秒)。

虽然前 3 个控制 expired 方法如何计算用户的过期时间,但最后一个可用于在计算过期时间时允许时钟漂移补偿。

伪随机数生成器

由于 Java 的 Secure Random 在从系统获取熵时可能会阻塞,我们提供了一个简单的包装器,可以在不阻塞事件循环的危险下使用它。

默认情况下,此 PRNG 使用混合模式,播种时阻塞,生成时非阻塞。PRNG 还会每 5 分钟用 64 位新熵重新播种。但是,所有这些都可以通过系统属性进行配置:

  • io.vertx.ext.auth.prng.algorithm 例如:SHA1PRNG

  • io.vertx.ext.auth.prng.seed.interval 例如:1000(每秒)

  • io.vertx.ext.auth.prng.seed.bits 例如:128

除非您注意到应用程序的性能受到 PRNG 算法的影响,否则大多数用户无需配置这些值。

共享伪随机数生成器

由于伪随机数生成器对象资源开销大,它们消耗稀缺的系统熵资源,因此在所有处理器之间共享 PRNG 是明智的。为了实现这一点并使其可用于 Vert.x 支持的所有语言,您应该研究 VertxContextPRNG

该接口放宽了 PRNG 对终端用户的生命周期管理,并确保它可以在您的所有应用程序中重用,例如:

String token = VertxContextPRNG.current(vertx).nextString(32);
// Generate a secure random integer
int randomInt = VertxContextPRNG.current(vertx).nextInt();

使用密钥

在处理安全性时,您将面临加载安全密钥的需求。安全密钥有多种格式和标准,这使得它成为一项相当复杂的任务。为了简化开发人员的工作,本模块包含 2 个抽象:

  1. KeyStoreOptions 抽象了 JVM 密钥库的通用格式。

  2. PubSecKeyOptions 抽象了 PEM 的通用格式。

要加载本地密钥库,模块应请求一个选项对象,例如:

KeyStoreOptions options = new KeyStoreOptions()
  .setPath("/path/to/keystore/file")
  .setType("pkcs8")
  .setPassword("keystore-password")
  .putPasswordProtection("key-alias", "alias-password");

类型非常重要,因为它随所使用的 JVM 版本而异。在版本 9 之前,默认是 jks,它是 JVM 特定的;之后是 pkcs12,这是一个通用标准。

非 JVM 密钥库密钥可以导入到 pkcs12 文件中,甚至无需使用 keytool 命令,例如这可以通过 OpenSSL 完成:

openssl pkcs12 -export -in mykeycertificate.pem -out mykeystore.pkcs12 -name myAlias -noiter -nomaciter

上述命令会将一个现有的 pem 文件转换为 pkcs12 密钥库,并将给定的密钥放在 myAlias 名称下。需要额外的参数 -noiter -nomaciter 以使文件与 JVM 加载器兼容。

要加载 PEM 文件,您应该注意有一些限制。默认的 JVM 类仅支持 PKCS8 格式的密钥,因此如果您有不同的 PEM 文件,您需要使用 OpenSSL 转换它,例如:

openssl pkcs8 -topk8 -inform PEM -in private.pem -out private_key.pem -nocrypt

之后,使用这样的文件就如同以下示例一样简单:

PubSecKeyOptions options = new PubSecKeyOptions()
  .setAlgorithm("RS256")
  .setBuffer(
    vertx.fileSystem()
      .readFileBlocking("/path/to/pem/file")
      .toString());

PEM 文件常见且易于使用,但没有密码保护,因此私钥很容易被嗅探。

JSON Web 密钥

JWK 是 OpenID Connect 和 JWT 提供者使用的标准。它们将密钥表示为 JSON 对象。通常这些 JSON 文档由身份提供者服务器(如 Google、Microsoft 等)提供,但您也可以使用在线应用程序 https://mkjwk.org 生成自己的密钥。对于离线体验,还有一个工具:https://connect2id.com/products/nimbus-jose-jwt/generator

链式认证提供者

在某些情况下,支持链式认证提供者可能会很有趣,例如在 LDAP 或属性文件中查找用户。这可以通过 ChainAuth 实现。

ChainAuth.any()
  .add(ldapAuthProvider)
  .add(propertiesAuthProvider);

也可以执行全部匹配,例如用户必须在 LDAP 和属性中都匹配

ChainAuth.all()
  .add(ldapAuthProvider)
  .add(propertiesAuthProvider);