访问控制

如需访问受保护的资源(REST API 端点),客户端必须提供有效访问 token。

获取通用 REST API token 的操作由认证服务(Authorization Server)扩展组件实现。当使用 Studio 添加 REST API 扩展时,会自动包含认证服务的 starter(io.jmix.authserver:jmix-authserver-starter)。认证服务扩展组件基于 Spring Authorization Server 框架开发。

OAuth 2.1 协议指定了获取 token 的几种方式,称为授权类型(grant type)。下面我们将说明如何使用客户端凭证(Client Credential)和认证码(Authentication Code)进行授权。

客户端凭证授权

Client Credentials grant(客户端凭证授权) 支持在请求中使用注册客户端的凭证从认证服务获取访问 token。这种授权类型应当用于服务与服务的通信。

方案如下:

  • 客户端使用 id 和秘钥在 Jmix 应用程序中注册。

  • 为客户端分配资源和行级角色。

  • 客户端应用程序,使用客户端 id 和秘钥获取访问 token。

  • 客户端应用程序使用 token 访问受保护的 API 端点,数据权限受所分配的角色控制。

Getting Started 部分有一个示例,演示了如何在 application.properties 中注册客户端,并使用客户端凭证获取访问 token 授权。

除了在 application.properties 文件设置客户端属性外,也可以通过提供一个 RegisteredClientRepository bean 进行注册。参考 Spring Authorization Server 文档 了解详情。

如果创建了一个 RegisteredClientRepository,则 application.properties 的配置将不会生效。

认证码授权

OAuth 2.1 规范 中,有关于认证码授权的详细介绍。

这个流程支持通过某个真正的 Jmix 用户许可而获得访问 token。当使用 Google、Facebook 或任何其他网站进行授权时,使用的就是认证码授权。Jmix 应用程序会展示一个特殊的登录表单,Jmix 对凭证进行验证后,如果验证通过,则访问 token 会用客户端应用和 Jmix 应用的一系列 HTTP 请求进行发送。

如需在 Jmix 中启用这种授权类型,需要定义一个支持这种授权类型的客户端。

spring.security.oauth2.authorizationserver.client.myapp.registration.client-id=myapp
spring.security.oauth2.authorizationserver.client.myapp.registration.client-secret={noop}myappsecret
spring.security.oauth2.authorizationserver.client.myapp.registration.client-authentication_methods=client_secret_basic
# enable required grant types
spring.security.oauth2.authorizationserver.client.myapp.registration.authorization-grant-types=authorization_code,refresh_token
# in this example we use the https://oauthdebugger.com website for testing
spring.security.oauth2.authorizationserver.client.myapp.registration.redirect-uris=https://oauthdebugger.com/debug
# use opaque tokens instead of JWT
spring.security.oauth2.authorizationserver.client.myapp.token.access-token-format=reference
# access token time-to-live
spring.security.oauth2.authorizationserver.client.myapp.token.access-token-time-to-live=1h
# refresh token token time-to-live
spring.security.oauth2.authorizationserver.client.myapp.token.refresh-token-time-to-live=24h
# use PKCE when performing the Authorization Code Grant flow
spring.security.oauth2.authorizationserver.client.myapp.require-proof-key=true

需要注意,默认的 Spring 认证服务会配置以下端点的 URL:

在下面的示例中,我们将使用 https://oauthdebugger.com 网站测试 token 的分发。这个网站将模拟一个访问 Jmix 资源的外部应用,需要获取访问 token。在你自己的应用中,需要实现 OAuth 2.1 协议规范中提到的步骤:发送请求至认证端点、处理重定向、解析认证码、发送请求至 token 端点用认证码获取访问 token 等。

打开 https://oauthdebugger.com,填写下列字段:

  • Authorize URI: http://localhost:8080/oauth2/authorize

  • Client ID: myapp

  • Scope: 留空

  • Use PKCE?: 勾选这个复选框。

oauthdebugger website

请记下 Code Verifier 字段的值,后续我们会用到。

点击页面底部的 SEND REQUEST 按钮。

此时会展示 Jmix 应用程序的一个特殊登录表单,这里需要填写已有 Jmix 用户的凭证。

authserver login form

完成所有步骤后获取的访问 token 将关联至该用户,所有发送至 Jmix REST API 的请求都会检查该用户的权限(资源角色和行级角色)。

如果凭证有效,网页会重定向至 URI https://oauthdebugger.com/debug,这个地址在 application.properties 定义。认证码必须以 code URL 参数进行添加,例如:https://oauthdebugger.com/debug?code=BdgQArzTaj_xna_a0-PoUIQwszMR0xPkToxcktd5wPe4SbO18qBYStqJePOPNaoe9cuIJe0nac0cw0yVC9Iv3SeofEYbMZhMKldoJQQwcBUnBTfp2AyQayDlaE8KPaCf&state=sujodv3j7eh

如需用这个认证码获取访问 token,则需要执行另一个发送至 Jmix 应用 token 端点的 HTTP 请求。这里我们用 curl 命令行工具,使用 https://oauthdebugger.com 初始网页的 code_verifier 值。

curl -X POST http://localhost:8080/oauth2/token \
   --basic --user myapp:myappsecret \
   -H "Content-Type: application/x-www-form-urlencoded" \
   -d "grant_type=authorization_code" \
   -d "redirect_uri=https://oauthdebugger.com/debug" \
   -d "code=c9ehHTJyT84mX-v2v2Q8sbAxkAFYg-gjfZDJImu5ExZVGLUyWn_J2-afs_m7kiv7MwjD-XXVRQtwz_6H-JTb4NvuWiUw6-5vrF75LtyNYAovuvSJQ680nQwv3PbhB4Y-" \
   -d "code_verifier=zdhRZIStXgwonFfvNYo2oI6nYuYt022LdcZF8eh3LGE" \
   -d "code_challenge_method=S256"

结果如下:

{
  "access_token":"Q6zvq8qGMUrN1VgouerOp4TJrry2f8oqL6mix8lDW-VKD_JHZXx0xv-ZZ_Zg_qgaHNw_wmeX6Qs0SlvEiFCyHqJ-PjqsnNkfF1XNKCAV43GQO0QeqmuV2sMiLgzY-m5r",
  "refresh_token":"DSINNaxmYykPrs3bDaKqaRgnrQDeZYInEF0yjtj2Vzkf5Nbf7OA0N09uQFN97MUmqaHBIXVxJFPQHtIbn-BM6Di035P68NqiIVfCawR5m6qQ6HbD6pQsCqAo-FBYAMqv",
  "token_type":"Bearer",
  "expires_in":299
}

自定义登录表单

本小节介绍如何使用自定义的登录表单替换认证服务扩展组件中标准的登录界面。

首先,创建一个登录表单模板。认证服务扩展组件包含了 Thymeleaf 模板引擎的依赖,因此可以直接使用。

创建一个新文件 my-as-login.html 并放置于 src/main/resources/templates 目录。这个目录是 Thymeleaf 默认搜索模板的目录。

my-as-login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" th:href="@{/my-as-login/styles/bootstrap.min.css}">
    <link rel="stylesheet" th:href="@{/my-as-login/styles/as-login.css}">
    <title>Please sign in</title>
</head>
<body>
<form class="as-login-form" th:action="@{/as-login}" method="post">
    <img th:src="@{/my-as-login/icons/as-login-icon.png}" class="mb-3 mx-auto d-block">
    <h2>Please sign in</h2>
    <div class="alert alert-danger" th:if="${param.error}">Bad credentials</div>
    <div class="mb-3">
        <label for="username" class="form-label">Username</label>
        <input type="text" class="form-control" id="username" name="username" required autofocus>
    </div>
    <div class="mb-3">
        <label for="password" class="form-label">Password</label>
        <input type="password" class="form-control" id="password" name="password" required>
    </div>
    <button type="submit" class="btn btn-primary w-100">Sign in</button>
</form>
</body>
</html>

你可能会注意到,模板中使用了位于 /my-as-login/styles/**/my-as-login/icons/** 路径的样式和图片。

下载 Bootstrap CSS 文件,并复制 bootstrap.min.csssrc/main/resources/META-INF/resources/my-as-login/styles 目录。

META-INF/resources 是 Spring 搜索静态资源的默认路径之一。

src/main/resources/META-INF/resources/my-as-login/styles 目录创建 as-login.css 文件:

as-login.css
.as-login-form {
    max-width: 300px;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

.as-login-form img {
    width: 72px;
    height: 72px;
}

将任意图标(比如 Jmix logo),重命名为 as-login-icon.png,然后放入 src/main/resources/META-INF/resources/my-as-login/icons 目录。

下一步,需要配置 Spring Security 支持对这些样式和图片资源的访问。创建一个 Spring 配置类,允许访问登录表单的资源 URL。

MySecurityConfiguration.java
@Configuration
public class MySecurityConfiguration {

    @Bean
    @Order(JmixSecurityFilterChainOrder.FLOWUI - 10) (1)
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.securityMatcher("/my-as-login/icons/**", "/my-as-login/styles/**")
                .authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll());
        return http.build();
    }
}
1 这个 bean 在安全 filter chain 中必须有高于 FlowuiSecurityConfiguration 的优先级。

最后,配置应用程序属性,使用新的登录表单:

application.properties
jmix.authserver.login-page-view-name = my-as-login.html

完成了上述步骤后,你的认证服务登录表单类似如下:

custom login form

Refresh Token 授权

关于 Refresh Token 授权可以阅读 OAuth 2.1 规范。需要为客户端注册 refresh-token 授权类型:

# enable required grant types
spring.security.oauth2.authorizationserver.client.myapp.registration.authorization-grant-types=authorization_code,refresh_token

如需用刷新码获取新的访问 token,则需要执行一个发送至 Jmix 应用 token 端点的 HTTP 请求。这里我们用 curl 命令行工具。

curl -X POST http://localhost:8080/oauth2/token \
   --basic --user myapp:myappsecret \
   -H "Content-Type: application/x-www-form-urlencoded" \
   -d "grant_type=refresh_token" \
   -d "refresh_token=zN2i5JooLfi0iqNJzaE-iiEiC2oHStv_X-kOaLuqX6ZNyRCs0EaNLik1xZrz-TPHfNEahLS2c402S_1kAO09K2x6oi3LFgpFoyr9snwE3ZXJ3Lp5AVH7s4YUBOXi0VRc"

匿名访问

默认情况下,所有的端点都需要在应用程序中认证成功才能访问。 但是,我们也可以将某些 REST API 端点开放为匿名接口。这其实是使用了 Jmix 的匿名访问功能。此时,API 请求是通过 anonymous 用户进行,这个用户是 Jmix 应用默认配置的。

对于没有使用 Authentication 请求头调用任何安全端点,都会使用 anonymous 用户进行认证。

如需添加匿名访问端点的白名单,可以使用 jmix.rest.anonymous-url-patterns 应用程序属性配置,URL pattern 以逗号隔开。例如:

jmix.rest.anonymous-url-patterns = \
  /rest/services/productService/getProductInformation,\
  /rest/entities/Product,\
  /rest/entities/Product/*

上面示例中配置的最后一个 pattern 是必须的,因为在更新或删除 Product 实体时,URL 还带有 id 部分。

配置完成后,可以不使用 Authentication 请求头与 ProductService 进行交互:

GetProductInformation Request
GET {{baseRestUrl}}
         /services
         /productService
         /getProductInformation
         ?productId=123
# Authorization: not set

这个请求会成功拿到服务的返回内容:

HTTP/1.1 200
{
  "name": "Apple iPhone",
  "productId": "123",
  "price": 499.99
}

如需为某些 实体 端点提供匿名访问,请确保 anonymous 用户有访问这些实体的权限。可以创建一个 资源角色 然后分配给 anonymous 用户,代码中使用 DatabaseUserRepository.initAnonymousUser() 方法。示例:

@ResourceRole(name = "AnonymousRestRole", code = AnonymousRestRole.CODE, scope = "API")
public interface AnonymousRestRole {

    String CODE = "anonymous-rest-role";

    @EntityAttributePolicy(entityClass = Product.class,
        attributes = "*",
        action = EntityAttributePolicyAction.MODIFY)
    @EntityPolicy(entityClass = Product.class,
        actions = {EntityPolicyAction.READ, EntityPolicyAction.UPDATE})
    void product();
}
@Primary
@Component("UserRepository")
public class DatabaseUserRepository extends AbstractDatabaseUserRepository<User> {
    // ...

    @Override
    protected void initAnonymousUser(User anonymousUser) {
        Collection<GrantedAuthority> authorities = getGrantedAuthoritiesBuilder()
                .addResourceRole(AnonymousRestRole.CODE)
                .build();
        anonymousUser.setAuthorities(authorities);
    }
}
匿名访问功能 不需要 anonymous 用户有 rest-minimal 角色。

预定义角色

REST: minimal accessrest-minimal):支持用户使用 API 与应用程序交互。