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

发布于 2025-01-20 11:51:15 字数 610 浏览 0 评论 0原文

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

Spring Boot OAuth2 中需要每个匿名用户同意的配置是什么?

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

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

我感觉 oauth2login 可能是合适的配置,但在尝试 oauth2login 之前我必须克服一些必要的自定义。我必须禁用生成的登录页面,该页面会提示用户选择提供商,并且我必须向授权请求添加自定义字段。我在 outh2login 中的这些自定义方面没有取得任何成功。所以,这种方法感觉是正确的,但似乎不可行。

有关此端点的调用方的信息,请参阅:HL7 FHIR SMART -应用程序启动

My app has a singular endpoint. It triggers an OAuth2 authorization grant flow. It is meant to be called only by anonymous users. Each anonymous user represents a different person with different authorizations in the resource server. Consent (i.e., distinct authorization grant) is required from each anonymous user.

What is configuration in Spring Boot OAuth2 to require a consent for each anonymous user?

I'm using Spring Boot oath2-client 2.6.4 and Spring Security 5.6.2.

Currently, I have oauth2client configuration. It does not satisfy requirement. In this configuration, consent is requested only once and applied to all following anonymous callers. All callers share the same grant and access token.

I sense oauth2login may be the appropriate configuration, but I have needful customizations which I have to overcome before I try oauth2login. I have to disable the generated login page which prompts the user to select a provider, and I have to add custom fields to the authorization request. I have not had any success with these customizations in outh2login. So, this approach feels right, but is seemingly unavailable.

For information about this endpoint's caller, see: HL7 FHIR SMART-APP-LAUNCH

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

茶色山野 2025-01-27 11:51:15

这与以下方面存在许多挑战:

我的应用程序具有单一的端点。 [...]仅由匿名用户调用。

这项要求使春季安全很难有很多帮助。这是因为匿名用户通常没有会话,而授权授予赠款是需要状态和会话的流程。附带说明,我不确定我完全了解您如何链接到的规范(据我所知,在OAuth 2.0上构建)在允许匿名用户的客户端的上下文中有意义。

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

以下配置定义并配置了此过滤器,以及自定义oauth2client()以将启动>启动参数传递到授权服务器。从本质上讲,它为启动参数创建了一个临时身份验证,该启动参数被保存为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 有关此答案的其他上下文。

There are a number of challenges to this, related to:

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

This requirement makes it difficult for Spring Security to be of much help. This is because anonymous users typically don't have sessions, and the authorization_code grant is a flow which requires state and therefore a session. As a side note, I am not sure I fully understand how or why the specification you linked to (which is built on OAuth 2.0, as far as I can see) makes sense in the context of a client that allows an anonymous user.

Having said that, this seems possible using only the .oauth2Client() support in Spring Security if you create a custom filter for managing anonymous users. Note: The following assumes that the authorization server does not ignore the launch parameter even if a session exists in the browser.

The following configuration defines and configures this filter, as well as customizing the oauth2Client() to pass the launch parameter to the authorization server. It essentially creates a temporary authentication for the launch parameter to be saved as the principalName in the session for the duration of the flow.

@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;
        }

    }

}

You can use this in a controller endpoint, as in the following example:

@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();
    }

}

See related issue #11069 for additional context on this answer.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文