如何使用 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 了解有关此答案的更多上下文。
我的应用程序只有一个端点。它触发 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 了解有关此答案的更多上下文。