自定义端点

对 Jmix 应用程序的请求受到 Spring Security 框架的保护。本章节介绍如何配置对自定义 API 端点的访问。

要点信息

要深入了解端点安全性的原理,请阅读 Spring Security 文档的相关部分:

Spring Security 使用特殊的 SecurityFilterChain bean 来确定需要保护哪些 URL。每个 SecurityFilterChain bean 都由 HttpSecurity builder 配置。一个应用程序可以声明多个 SecurityFilterChain bean。此时,多个 bean 之间的顺序非常重要。请参阅 Spring Security 文档的 多 HttpSecurity 实例 部分,了解如何配置多个 HttpSecurity 对象。

默认情况下,每个 Jmix 应用程序都包含一个安全配置,该配置扩展了 VaadinWebSecurity 类。该配置设置对 Vaadin 内部端点的访问,并将所有请求的授权委托给 Jmix 和 Vaadin 机制实现(使用视图控制器注解或分析用户的资源角色授予对视图的访问权限)。该配置创建的 SecurityFilterChain 具有最低优先级,并且永远在最后调用。下面的 自定义端点安全性 部分介绍如何定义自己的 SecurityFilterChain 来保护自定义的端点。

OpenID 连接授权服务 这样的扩展组件中,也包含了组件自有的 SecurityFilterChain bean,用于保护授权或资源服务的端点。这些 bean 的顺序要先于 UI 模块中的 bean。参阅下面的 基于 Token 的认证 部分,了解在使用这些扩展组件时如何保护 API 端点。

自定义端点安全性

如需为端点定义自定义的安全规则,需要声明一个新的 SecurityFilterChain bean。重要的是,这个 bean 的顺序必须小于 Jmix 框架中 SecurityFilterChain bean 的顺序。

Jmix 使用的顺序值常量在 JmixSecurityFilterChainOrder 接口中定义。经验规则是使用 JmixSecurityFilterChainOrder.CUSTOMJmixSecurityFilterChainOrder.CUSTOM - 10 之类的值作为自定义 chain 的顺序。

下面是一个简单的 SecurityFilterChain bean 示例:

@Bean
@Order(JmixSecurityFilterChainOrder.CUSTOM)
SecurityFilterChain publicFilterChain(HttpSecurity http) throws Exception {
    http.securityMatcher("/public/**")
            .authorizeHttpRequests(authorize ->
                    authorize.anyRequest().permitAll()
            );
    return http.build();
}

上面的配置授予对 /public/** 匹配端点的访问权限。

公共端点

假设在一个控制器中有两个方法,这两个方法需要开放给任何用户,无需认证。

@RestController
public class GreetingController {

    @PostMapping("/greeting/hello")
    public String hello() {
        return "Hello!";
    }

    @GetMapping("/greeting/public/hi")
    public String hi() {
        return "Hi!";
    }
}

公共端点的访问可以用下列配置:

@Configuration
public class AnonymousControllerSecurityConfiguration {

    @Bean
    @Order(JmixSecurityFilterChainOrder.CUSTOM) (1)
    SecurityFilterChain greetingFilterChain(HttpSecurity http) throws Exception {
        http.securityMatcher("/greeting/**") (2)
                .authorizeHttpRequests(authorize ->
                        authorize.anyRequest().permitAll() (3)
                )
                .csrf(csrf -> csrf.disable()); (4)
        JmixHttpSecurityUtils.configureAnonymous(http); (5)
        return http.build();
    }
}
1 JmixSecurityFilterChainOrder.CUSTOM 的顺序值小于其他 Jmix chain 的顺序,因此会在其他 Jmix chain 之前使用。
2 securityMatcher() 用于确定是否将 HttpSecurity 应用于请求。示例中的请求将匹配符合 /greeting/** 的 URL。对其他 URL 的请求将由 Jmix UI 模块的默认 chain 处理。
3 permitAll() 指令授予访问权限。
4 为 POST 请求禁用 CSRF。
5 调用 JmixHttpSecurityUtils.configureAnonymous(http) 配置匿名认证,将 UserRepository 返回的匿名用户设置到安全上下文中。

HTTP 基本认证

示例演示如何使用 HTTP 基本认证 保护控制器的端点。

控制器类:

@RestController
@RequestMapping("/api")
public class BasicGreetingController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello!";
    }

    @GetMapping("/public/hi")
    public String publicHi() {
        return "Hi!";
    }
}

所有能匹配 /api/** URL 的请求需要使用 HTTP 基本认证进行保护,而能匹配 /api/public/** 的请求则对所有用户开放。这可以通过以下配置实现:

@Configuration
public class BasicControllerSecurityConfiguration {

    @Bean
    @Order(JmixSecurityFilterChainOrder.CUSTOM) (1)
    SecurityFilterChain basicControllerFilterChain(
            HttpSecurity http,
            AuthenticationManager authenticationManager) throws Exception {
        http.securityMatcher("/api/**") (2)
                .authorizeHttpRequests(requests ->
                        requests
                                .requestMatchers("/api/public/**").permitAll() (3)
                                .anyRequest().authenticated() (4)
                )
                .httpBasic(Customizer.withDefaults()) (5)
                .authenticationManager(authenticationManager); (6)
        return http.build();
    }
}
1 JmixSecurityFilterChainOrder.CUSTOM 的顺序值小于其他 Jmix chain 的顺序,因此会在其他 Jmix chain 之前使用。
2 HttpSecurity 仅用于 /api/** 的请求。
3 如果 matcher 匹配成功,则我们可以配置进一步的规则。所有 /api/public/** 的请求都无需认证。
4 所有不能匹配 /api/public/** 的请求都需要认证。
5 启用基本认证。
6 使用 Jmix 配置的 AuthenticationManager 做基本认证。

/api/** 端点的请求必须包含一个请求头 - Authorization: Basic <credentials>,其中 <credentials> 是用户名和密码以冒号连接后的 Base64 编码。示例:

GET /api/hello HTTP/1.1
Host: server.example.com
Authorization: Basic YWRtaW46YWRtaW4=
在这个示例中,/api/public/** 的公开访问也可以通过另一个 SecurityFilterChain bean 配置,带有 securityMatcher("/api/public/**") 且顺序要小于当前的 bean,例如,JmixSecurityFilterChainOrder.CUSTOM - 10

基于 Token 的认证

可以使用由 授权服务 或外部认证提供商颁发的 bearer token 来保护自定义端点,例如,在使用 OpenID Connect 组件时,由 Keycloak 颁发。授权服务和 OpenID Connect 的安全配置为此提供了专门的扩展点。在 OAuth 2.1 规范的定义中,包含受保护端点的应用程序扮演的是 资源服务(resource server) 的角色。

假设有一个 REST 控制器:

@RestController
public class GreetingController {

    @PostMapping("/greeting/hello")
    public String hello() {
        return "Hello!";
    }

    @GetMapping("/greeting/public/hi")
    public String hi() {
        return "Hi!";
    }
}

有几种方式可以定义哪些端点需要使用基于 token 的认证机制、哪些端点可以匿名访问。

配置实现

最简易的方式就是使用下列应用程序属性定义资源服务的哪些端点需要保护,而哪些又可以公开访问:

# All endpoints that match the given pattern will require a bearer token
jmix.resource-server.authenticated-url-patterns = /greeting/**

# However, endpoints that match the following pattern will be accessible without a token
jmix.resource-server.anonymous-url-patterns = /greeting/public/**

URL Patterns Providers

另一个定义需要使用 token 保护端点的方法是,创建一个实现 AuthenticatedUrlPatternsProvider 接口的 bean,并从 getAuthenticatedUrlPatterns() 方法返回 URL 模式列表。这个方式更加灵活,并且可以为端点保护定义更复杂的规则。

@Component
public class GreetingAuthenticatedUrlPatternsProvider implements AuthenticatedUrlPatternsProvider {

    @Override
    public List<String> getAuthenticatedUrlPatterns() {
        return List.of("/greeting/**");
    }

}

资源服务中的匿名端点可以用同样的方式通过实现 AnonymousUrlPatternsProvider 接口定义。

@Component
public class GreetingAnonymousUrlPatternsProvider implements AnonymousUrlPatternsProvider {

    @Override
    public List<String> getAnonymousUrlPatterns() {
        return List.of("/greeting/public/**");
    }

}

RequestMatcher Provider

上面两种方式可以定义端点的保护规则,但这些规则只限制了特定 URL pattern 的字符串集合。如果需要比 URL pattern 更复杂的规则,比如需要控制 HTTP 的访问方法,则可以实现 AuthenticatedRequestMatcherProvider 接口,在 getAuthenticatedRequestMatcher() 方法中返回一个 RequestMatcher 对象。

@Component
public class GreetingAuthenticatedRequestMatcherProvider implements AuthenticatedRequestMatcherProvider {

    @Override
    public RequestMatcher getAuthenticatedRequestMatcher() {
        return new AntPathRequestMatcher("/greeting/**", HttpMethod.POST.name());
    }
}

资源服务中的匿名端点的 RequestMatcher 可以用同样的方式通过实现 AnonymousRequestMatcherProvider 接口定义。

@Component
public class GreetingAnonymousRequestMatcherProvider implements AnonymousRequestMatcherProvider {

    @Override
    public RequestMatcher getAnonymousRequestMatcher() {
        return new AntPathRequestMatcher("/greeting/public/**", HttpMethod.GET.name());
    }
}

访问资源服务的端点

完成上述配置后,所有对 /greeting/** 端点的请求都需要在 Authorization 头中带 access token。例如:

GET /greeting/hello HTTP/1.1
Host: server.example.com
Authorization: Bearer <ACCESS_TOKEN>

问题排查

如果遇到 401 Unauthorized403 Forbidden HTTP 错误或任何其他与端点安全行相关的问题,那么很可能 Spring Security logging 会记录下一些蛛丝马迹。

要启用日志记录,可在 application.properties 配置以下应用程序属性值为 DEBUG 或 TRACE:

logging.level.org.springframework.security = TRACE