如何使用 Spring Security OAuth2 要求每个唯一的匿名用户同意?

How can I require consent for each unique anonymous user with Spring Security OAuth2?

我的应用程序只有一个端点。它触发 OAuth2 授权授予流程。它只能由匿名用户调用。每个匿名用户代表资源服务器中具有不同权限的不同人。每个匿名用户都需要同意(即不同的授权授予)。

Spring 启动 OAuth2 要求每个匿名用户同意的配置是什么?

我正在使用 Spring Boot oath2-client 2.6.4 和 Spring Security 5.6.2。

目前,我有oauth2client配置。它不满足要求。在此配置中,仅请求一次同意并应用于所有后续匿名调用者。所有呼叫者共享相同的授权和访问令牌。

我感觉 oauth2login 可能是合适的配置,但在尝试 oauth2login 之前,我有必要的自定义设置,我必须克服这些设置。我必须禁用提示用户 select 提供商的生成的登录页面,并且我必须向授权请求添加自定义字段。我在 outh2login 中对这些定制没有任何成功。所以,这个方法感觉是对的,但是貌似是行不通的。

有关此端点调用者的信息,请参阅:HL7 FHIR SMART-APP-LAUNCH

这方面存在许多挑战,涉及:

My app has a singular endpoint. [...] It is meant to be called only by anonymous users.

此要求使 Spring 安全性很难提供太多帮助。这是因为匿名用户通常没有会话,并且 authorization_code 授权是一个需要状态的流程,因此需要一个会话。作为旁注,我不确定我是否完全理解您链接到的规范(据我所知,它是基于 OAuth 2.0 构建的)在允许匿名用户的客户端上下文中如何或为什么有意义。

话虽如此,如果您创建用于管理匿名用户的自定义过滤器,则仅使用 Spring 安全性中的 .oauth2Client() 支持似乎是可能的。注意:以下假设即使浏览器中存在会话,授权服务器也不会忽略launch参数。

以下配置定义和配置此过滤器,以及自定义 oauth2Client() 以将 launch 参数传递给授权服务器。它实质上为启动参数创建了一个临时身份验证,以便在流期间保存为会话中的 principalName

@EnableWebSecurity
public class SecurityConfig {

    private static final String PARAMETER_NAME = "launch";

    private static final String ROLE_NAME = "LAUNCH_USER";

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().hasRole(ROLE_NAME)
            )
            .addFilterAfter(authenticationFilter(), AnonymousAuthenticationFilter.class)
            .oauth2Client((oauth2) -> oauth2
                .authorizationCodeGrant((authorizationCode) -> authorizationCode
                    .authorizationRequestResolver(authorizationRequestResolver(clientRegistrationRepository))
                )
            );
        return http.build();
    }

    private OAuth2AuthorizationRequestResolver authorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {
        DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
                new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);

        // Configure a request customizer for the OAuth2AuthorizationRequestRedirectFilter
        authorizationRequestResolver.setAuthorizationRequestCustomizer((authorizationRequest) -> {
            Authentication currentAuthentication = SecurityContextHolder.getContext().getAuthentication();

            // Customize request with principal name originally obtained from request parameter
            if (currentAuthentication instanceof RequestParameterAuthenticationToken) {
                Map<String, Object> additionalParameters = Map.of(PARAMETER_NAME, currentAuthentication.getName());
                authorizationRequest.additionalParameters(additionalParameters);
            }
        });

        return authorizationRequestResolver;
    }

    private RequestParameterAuthenticationFilter authenticationFilter() {
        return new RequestParameterAuthenticationFilter(PARAMETER_NAME, AuthorityUtils.createAuthorityList("ROLE_" + ROLE_NAME));
    }

    /**
     * Authentication filter that authenticates an anonymous request using a request parameter.
     */
    public static final class RequestParameterAuthenticationFilter extends OncePerRequestFilter {

        private final String parameterName;

        private final List<GrantedAuthority> authorities;

        public RequestParameterAuthenticationFilter(String parameterName, List<GrantedAuthority> authorities) {
            this.parameterName = parameterName;
            this.authorities = authorities;
        }

        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            SecurityContext existingSecurityContext = SecurityContextHolder.getContext();
            if (existingSecurityContext != null && !(existingSecurityContext.getAuthentication() instanceof AnonymousAuthenticationToken)) {
                filterChain.doFilter(request, response);
                return;
            }

            String principalName = request.getParameter(parameterName);
            if (principalName != null) {
                Authentication authenticationResult = new RequestParameterAuthenticationToken(principalName, authorities);
                authenticationResult.setAuthenticated(true);

                SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
                securityContext.setAuthentication(authenticationResult);
                SecurityContextHolder.setContext(securityContext);
            }
            filterChain.doFilter(request, response);
        }

    }

    /**
     * Custom authentication token that can be persisted between requests, but is otherwise very similar to
     * {@link AnonymousAuthenticationToken}.
     */
    public static final class RequestParameterAuthenticationToken extends AbstractAuthenticationToken implements Serializable {

        private static final long serialVersionUID = 1L;

        private final String principalName;

        public RequestParameterAuthenticationToken(String principalName, Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            this.principalName = principalName;
        }

        @Override
        public Object getPrincipal() {
            return this.principalName;
        }

        @Override
        public Object getCredentials() {
            return this.principalName;
        }

    }

}

您可以在控制器端点中使用它,如下例所示:

@RestController
public class LaunchController {

    @GetMapping("/app/launch")
    public void launch(
            @RegisteredOAuth2AuthorizedClient("fhir-client")
                    OAuth2AuthorizedClient authorizedClient) {
        String launchParameter = authorizedClient.getPrincipalName();
        String accessToken = authorizedClient.getAccessToken().getTokenValue();
        // Use authorizedClient.getAccessToken() to make a request (WebClient)...

        // Clear the SecurityContext after the request, to force the next request
        // to start the flow over again
        SecurityContextHolder.clearContext();
    }

}

请参阅相关问题 #11069 了解有关此答案的更多上下文。