跳到主要内容

使用基于角色的访问控制 (RBAC) 和 JWT 验证保护你的 Micronaut API

本指南将帮助你通过使用 Logto 颁发的 基于角色的访问控制 (RBAC)JSON Web Token (JWT) 来实现授权 (Authorization),从而保护你的 Micronaut API。

开始之前

你的客户端应用需要从 Logto 获取访问令牌 (Access tokens)。如果你还没有完成客户端集成,请查看我们的 快速开始,适用于 React、Vue、Angular 或其他客户端框架,或者参考我们的 机器对机器指南 以实现服务器到服务器的访问。

本指南聚焦于在你的 Micronaut 应用中对这些令牌进行服务端验证

A figure showing the focus of this guide

你将学到什么

  • JWT 验证:学习如何验证访问令牌 (Access tokens) 并提取认证 (Authentication) 信息
  • 中间件实现:创建可复用的中间件以保护 API
  • 权限模型:理解并实现不同的授权 (Authorization) 模式:
    • 应用级端点的全局 API 资源
    • 用于租户特定功能控制的组织权限
    • 多租户数据访问的组织级 API 资源
  • RBAC 集成:在你的 API 端点中强制执行基于角色的权限 (Permissions) 和权限范围 (Scopes)

前置条件

  • 已安装 Java 的最新稳定版本
  • 基本了解 Micronaut 及 Web API 开发
  • 已配置 Logto 应用(如有需要请参见 快速开始

权限 (Permission) 模型概览

在实施保护之前,请选择适合你应用架构的权限 (Permission) 模型。这与 Logto 的三大授权 (Authorization) 场景保持一致:

全局 API 资源 RBAC
  • 使用场景: 保护整个应用共享的 API 资源(非组织 (Organization) 专属)
  • 令牌类型: 具有全局受众 (Audience) 的访问令牌 (Access token)
  • 示例: 公共 API、核心产品服务、管理端点
  • 最适合: 所有客户都使用 API 的 SaaS 产品、无租户隔离的微服务
  • 了解更多: 保护全局 API 资源

💡 在继续之前选择你的模型 —— 本指南后续内容将以你选择的方式为参考。

快速准备步骤

配置 Logto 资源和权限

  1. 创建 API 资源:前往 控制台 → API 资源 并注册你的 API(例如,https://api.yourapp.com
  2. 定义权限:添加如 read:productswrite:orders 等权限(Scopes)——参见 定义带权限的 API 资源
  3. 创建全局角色:前往 控制台 → 角色 并创建包含你的 API 权限的角色——参见 配置全局角色
  4. 分配角色:将角色分配给需要访问 API 的用户或 M2M 应用程序
初次接触基于角色的访问控制 (RBAC)?:

从我们的 基于角色的访问控制 (RBAC) 指南 开始,获取分步设置说明。

更新你的客户端应用

在客户端请求合适的权限(Scopes):

通常需要在客户端配置中更新以下一项或多项:

  • OAuth 流程中的 scope 参数
  • 用于 API 资源访问的 resource 参数
  • 用于组织上下文的 organization_id
编码前须知:

请确保你测试的用户或 M2M 应用已被分配包含所需 API 权限的合适角色或组织角色。

初始化你的 API 项目

要初始化一个新的 Micronaut 项目,你可以使用 Micronaut CLI 或访问 Micronaut Launch:

使用 Micronaut CLI:

mn create-app com.example.your-api-name \
--features=security-jwt,http-server-netty \
--build=maven \
--lang=java
cd your-api-name

或者访问 Micronaut Launch,并选择:

  • 应用类型:Micronaut Application
  • Java 版本:17
  • 构建工具:Maven
  • 功能:security-jwt, http-server-netty

这将创建一个基础的 Micronaut 项目:

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>your-api-name</artifactId>
<version>0.1</version>
<packaging>jar</packaging>

<parent>
<groupId>io.micronaut.platform</groupId>
<artifactId>micronaut-parent</artifactId>
<version>4.2.0</version>
</parent>

<properties>
<packaging>jar</packaging>
<jdk.version>17</jdk.version>
<release.version>17</release.version>
<micronaut.version>4.2.0</micronaut.version>
<micronaut.runtime>netty</micronaut.runtime>
<exec.mainClass>com.example.Application</exec.mainClass>
</properties>
</project>

创建一个基础的控制器:

src/main/java/com/example/HelloController.java
package com.example;

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Produces;

@Controller("/hello")
public class HelloController {

@Get
@Produces(MediaType.TEXT_PLAIN)
public String index() {
return "Hello World";
}
}
备注:

更多关于如何设置控制器、服务和其他功能的详细信息,请参考 Micronaut 文档。

初始化常量和工具方法

在你的代码中定义必要的常量和工具函数,用于处理令牌的提取和校验。一个有效的请求必须包含 Authorization 请求头,格式为 Bearer <访问令牌 (Access token)>

AuthorizationException.java
public class AuthorizationException extends RuntimeException {
private final int statusCode;

public AuthorizationException(String message) {
this(message, 403); // 默认使用 403 Forbidden
}

public AuthorizationException(String message, int statusCode) {
super(message);
this.statusCode = statusCode;
}

public int getStatusCode() {
return statusCode;
}
}

获取你的 Logto 租户信息

你需要以下数值来验证 Logto 签发的令牌:

  • JSON Web Key Set (JWKS) URI:Logto 公钥的 URL,用于验证 JWT 签名。
  • 发行者 (Issuer):期望的发行者 (Issuer) 值(Logto 的 OIDC URL)。

首先,找到你的 Logto 租户的端点。你可以在多个地方找到它:

  • 在 Logto 控制台的 设置域名 下。
  • 在你在 Logto 配置的任何应用程序设置中,设置端点与凭证

从 OpenID Connect 发现端点获取

这些数值可以从 Logto 的 OpenID Connect 发现端点获取:

https://<your-logto-endpoint>/oidc/.well-known/openid-configuration

以下是一个示例响应(为简洁省略了其他字段):

{
"jwks_uri": "https://your-tenant.logto.app/oidc/jwks",
"issuer": "https://your-tenant.logto.app/oidc"
}

由于 Logto 不允许自定义 JWKS URI 或发行者 (Issuer),你可以在代码中硬编码这些数值。但对于生产环境的应用程序,这并不推荐,因为如果将来某些配置发生变化,可能会增加维护成本。

  • JWKS URI: https://<your-logto-endpoint>/oidc/jwks
  • 发行者 (Issuer): https://<your-logto-endpoint>/oidc

校验令牌和权限

在提取令牌并获取 OIDC 配置后,请验证以下内容:

  • 签名: JWT 必须有效且由 Logto(通过 JWKS)签名。
  • 发行者 (Issuer): 必须与你的 Logto 租户的发行者 (Issuer) 匹配。
  • 受众 (Audience): 必须与你在 Logto 中注册的 API 的资源指示器 (resource indicator) 匹配,或在适用时匹配组织 (organization) 上下文。
  • 过期时间: 令牌必须未过期。
  • 权限 (Scopes): 令牌必须包含你的 API / 操作所需的权限 (scopes)。权限 (scopes) 是 scope 声明中的以空格分隔的字符串。
  • 组织 (Organization) 上下文: 如果保护的是组织级 API 资源,请验证 organization_id 声明。

参见 JSON Web Token 以了解更多关于 JWT 结构和声明 (Claims) 的信息。

针对每种权限 (Permission) 模型需要检查什么

不同的权限 (Permission) 模型,其声明 (Claims) 和验证规则也不同:

  • 受众 (Audience) 声明 (aud): API 资源指示器 (resource indicator)
  • 组织 (Organization) 声明 (organization_id): 不存在
  • 需要检查的权限 (Scopes) (scope): API 资源权限 (permissions)

对于非 API 的组织 (Organization) 权限 (Permissions),组织上下文由 aud 声明表示 (例如,urn:logto:organization:abc123)。organization_id 声明仅在组织级 API 资源令牌中存在。

提示:

对于安全的多租户 API,请始终同时验证权限 (scopes) 和上下文(受众 (audience)、组织 (organization))。

添加校验逻辑

我们会根据不同的框架使用不同的 JWT 库。请安装所需的依赖:

在你的 pom.xml 中添加:

<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-server-netty</artifactId>
</dependency>
application.yml
micronaut:
security:
authentication: bearer
token:
jwt:
signatures:
jwks:
logto:
url: ${JWKS_URI:https://your-tenant.logto.app/oidc/jwks}
claims-validators:
issuer: ${JWT_ISSUER:https://your-tenant.logto.app/oidc}
JwtClaimsValidator.java
import io.micronaut.security.token.Claims;
import io.micronaut.security.token.validator.TokenValidator;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;

@Singleton
public class JwtClaimsValidator implements TokenValidator {

@Override
public Publisher<Boolean> validateToken(String token, Claims claims) {
try {
verifyPayload(claims);
return Mono.just(true);
} catch (AuthorizationException e) {
// Micronaut 会自动处理状态码
return Mono.just(false);
}
}

private void verifyPayload(Claims claims) {
// 发行者 (Issuer) 校验已由 Micronaut JWT 配置自动处理
// 在这里根据你的权限模型实现额外的验证逻辑
// 可使用下方的辅助方法提取声明 (Claims)

// 示例:throw new AuthorizationException("Insufficient permissions");
}

// Micronaut JWT 辅助方法
@SuppressWarnings("unchecked")
private List<String> extractAudiences(Claims claims) {
Object aud = claims.get("aud");
if (aud instanceof List) {
return (List<String>) aud;
} else if (aud instanceof String) {
return Arrays.asList((String) aud);
}
return List.of();
}

private String extractScopes(Claims claims) {
return (String) claims.get("scope");
}

private String extractOrganizationId(Claims claims) {
return (String) claims.get("organization_id");
}
}

根据你的权限 (Permission) 模型,实现相应的验证逻辑:

// 检查 audience (受众) 声明是否与你的 API 资源指示器匹配
List<String> audiences = extractAudiences(token); // 框架相关的提取方式
if (!audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience");
}

// 检查全局 API 资源所需的权限 (Scopes)
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // 替换为你实际需要的权限 (Scopes)
String scopes = extractScopes(token); // 框架相关的提取方式
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();

if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient scope");
}

用于提取声明 (Claims) 的辅助方法是框架相关的。具体实现细节请参见上方各框架的验证文件。

将中间件应用到你的 API

现在,将中间件应用到你的受保护 API 路由。

ProtectedController.java
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.rules.SecurityRule;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

@Controller("/api")
@Secured(SecurityRule.IS_AUTHENTICATED)
public class ProtectedController {

@Get("/protected")
public Map<String, Object> protectedEndpoint(Authentication authentication) {
// 直接从 Authentication 获取访问令牌 (Access token) 信息
String scopes = (String) authentication.getAttributes().get("scope");
List<String> scopeList = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();

return Map.of(
"sub", authentication.getName(),
"client_id", authentication.getAttributes().get("client_id"),
"organization_id", authentication.getAttributes().get("organization_id"),
"scopes", scopeList,
"audience", authentication.getAttributes().get("aud")
);
}
}

测试你的受保护 API

获取访问令牌 (Access tokens)

从你的客户端应用程序获取: 如果你已经完成了客户端集成,你的应用可以自动获取令牌。提取访问令牌 (Access token),并在 API 请求中使用它。

使用 curl / Postman 进行测试:

  1. 用户令牌: 使用你的客户端应用的开发者工具,从 localStorage 或网络面板复制访问令牌 (Access token)

  2. 机器对机器令牌: 使用客户端凭证流。以下是一个使用 curl 的非规范示例:

    curl -X POST https://your-tenant.logto.app/oidc/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=client_credentials" \
    -d "client_id=your-m2m-client-id" \
    -d "client_secret=your-m2m-client-secret" \
    -d "resource=https://your-api-resource-indicator" \
    -d "scope=api:read api:write"

    你可能需要根据你的 API 资源和权限调整 resourcescope 参数;如果你的 API 是组织范围的,还可能需要 organization_id 参数。

提示:

需要查看令牌内容?使用我们的 JWT 解码器 来解码和验证你的 JWT。

测试受保护的端点

有效令牌请求
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
http://localhost:3000/api/protected

预期响应:

{
"auth": {
"sub": "user123",
"clientId": "app456",
"organizationId": "org789",
"scopes": ["api:read", "api:write"],
"audience": ["https://your-api-resource-indicator"]
}
}
缺少令牌
curl http://localhost:3000/api/protected

预期响应 (401):

{
"error": "Authorization header is missing"
}
无效令牌
curl -H "Authorization: Bearer invalid-token" \
http://localhost:3000/api/protected

预期响应 (401):

{
"error": "Invalid token"
}

权限模型相关测试

针对受全局权限保护的 API 的测试场景:

  • 有效权限 (Scopes): 使用包含所需 API 权限(如 api:readapi:write)的令牌进行测试
  • 缺少权限 (Scopes): 当令牌缺少所需权限时,预期返回 403 Forbidden
  • 受众 (Audience) 错误: 当受众与 API 资源不匹配时,预期返回 403 Forbidden
# 缺少权限的令牌 - 预期 403
curl -H "Authorization: Bearer token-without-required-scopes" \
http://localhost:3000/api/protected

延伸阅读

RBAC 实践:为你的应用实现安全授权 (Authorization)

构建多租户 SaaS 应用:从设计到实现的完整指南