验证 <saml2:Response>

为了验证 SAML 2.0 响应,Spring Security 使用 Saml2AuthenticationTokenConverter 来填充 Authentication 请求,并使用 OpenSaml5AuthenticationProvider 来验证它。

您可以通过多种方式进行配置,包括:

  1. 更改 RelyingPartyRegistration 的查找方式

  2. 为时间戳验证设置时钟偏移

  3. 将响应映射到 GrantedAuthority 实例列表

  4. 自定义断言的验证策略

  5. 自定义解密响应和断言元素的策略

要配置这些,您将在 DSL 中使用 saml2Login#authenticationManager 方法。

更改 SAML 响应处理端点

默认端点是 /login/saml2/sso/{registrationId}。您可以在 DSL 和关联的元数据中更改它,如下所示:

  • Java

  • Kotlin

@Bean
SecurityFilterChain securityFilters(HttpSecurity http) throws Exception {
	http
        // ...
        .saml2Login((saml2) -> saml2.loginProcessingUrl("/saml2/login/sso"))
        // ...

    return http.build();
}
@Bean
fun securityFilters(val http: HttpSecurity): SecurityFilterChain {
	http {
        // ...
        .saml2Login {
            loginProcessingUrl = "/saml2/login/sso"
        }
        // ...
    }

    return http.build()
}

  • Java

  • Kotlin

relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")

更改 RelyingPartyRegistration 查找

默认情况下,此转换器将与任何关联的 <saml2:AuthnRequest> 或在 URL 中找到的任何 registrationId 进行匹配。或者,如果在上述两种情况下都找不到,它会尝试通过 <saml2:Response#Issuer> 元素进行查找。

在许多情况下,您可能需要更复杂的功能,例如支持 ARTIFACT 绑定。在这些情况下,您可以通过自定义 AuthenticationConverter 来定制查找,如下所示:

  • Java

  • Kotlin

@Bean
SecurityFilterChain securityFilters(HttpSecurity http, AuthenticationConverter authenticationConverter) throws Exception {
	http
        // ...
        .saml2Login((saml2) -> saml2.authenticationConverter(authenticationConverter))
        // ...

    return http.build();
}
@Bean
fun securityFilters(val http: HttpSecurity, val converter: AuthenticationConverter): SecurityFilterChain {
	http {
        // ...
        .saml2Login {
            authenticationConverter = converter
        }
        // ...
    }

    return http.build()
}

设置时钟偏移

断言方和依赖方的系统时钟未完全同步是很常见的。因此,您可以配置 OpenSaml5AuthenticationProvider.AssertionValidator,如下所示:

  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        OpenSaml5AuthenticationProvider authenticationProvider = new OpenSaml5AuthenticationProvider();
        AssertionValidator assertionValidator = AssertionValidator.builder()
                .clockSkew(Duration.ofMinutes(10)).build();
		authenticationProvider.setAssertionValidator(assertionValidator);
        http
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
            )
            .saml2Login((saml2) -> saml2
                .authenticationManager(new ProviderManager(authenticationProvider))
            );
        return http.build();
	}
}
@Configuration @EnableWebSecurity
class SecurityConfig {
    @Bean
    @Throws(Exception::class)
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        val authenticationProvider = OpenSaml5AuthenticationProvider()
        val assertionValidator = AssertionValidator.builder().clockSkew(Duration.ofMinutes(10)).build()
        authenticationProvider.setAssertionValidator(assertionValidator)
        http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = ProviderManager(authenticationProvider)
            }
        }
        return http.build()
    }
}

Assertion 转换为 Authentication

OpenSamlXAuthenticationProvider#setResponseAuthenticationConverter 提供了一种方法,让您更改它如何将您的断言转换为 Authentication 实例。

您可以通过以下方式设置自定义转换器:

  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    Converter<ResponseToken, Saml2Authentication> authenticationConverter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        OpenSaml5AuthenticationProvider authenticationProvider = new OpenSaml5AuthenticationProvider();
        authenticationProvider.setResponseAuthenticationConverter(this.authenticationConverter);

        http
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated())
            .saml2Login((saml2) -> saml2
                .authenticationManager(new ProviderManager(authenticationProvider))
            );
        return http.build();
    }

}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
    @Autowired
    var authenticationConverter: Converter<ResponseToken, Saml2Authentication>? = null

    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        val authenticationProvider = OpenSaml5AuthenticationProvider()
        authenticationProvider.setResponseAuthenticationConverter(this.authenticationConverter)
        http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = ProviderManager(authenticationProvider)
            }
        }
        return http.build()
    }
}

接下来的示例都建立在此常见构造之上,以向您展示此转换器在不同情况下如何派上用场。

UserDetailsService 协调

或者,您可能希望包含来自传统 UserDetailsService 的用户详细信息。在这种情况下,响应身份验证转换器会派上用场,如下所示:

  • Java

  • Kotlin

@Component
class MyUserDetailsResponseAuthenticationConverter implements Converter<ResponseToken, Saml2Authentication> {
	private final ResponseAuthenticationConverter delegate = new ResponseAuthenticationConverter();
	private final UserDetailsService userDetailsService;

	MyUserDetailsResponseAuthenticationConverter(UserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}

	@Override
    public Saml2Authentication convert(ResponseToken responseToken) {
	    Saml2Authentication authentication = this.delegate.convert(responseToken); (1)
		UserDetails principal = this.userDetailsService.loadByUsername(username); (2)
		String saml2Response = authentication.getSaml2Response();
		Saml2ResponseAssertionAccessor assertion = new OpenSamlResponseAssertionAccessor(
				saml2Response, CollectionUtils.getFirst(response.getAssertions()));
		Collection<GrantedAuthority> authorities = principal.getAuthorities();
		return new Saml2AssertionAuthentication(userDetails, assertion, authorities); (3)
    }

}
@Component
open class MyUserDetailsResponseAuthenticationConverter(val delegate: ResponseAuthenticationConverter,
        UserDetailsService userDetailsService): Converter<ResponseToken, Saml2Authentication> {

	@Override
    open fun convert(responseToken: ResponseToken): Saml2Authentication {
	    val authentication = this.delegate.convert(responseToken) (1)
		val principal = this.userDetailsService.loadByUsername(username) (2)
		val saml2Response = authentication.getSaml2Response()
		val assertion = OpenSamlResponseAssertionAccessor(
				saml2Response, CollectionUtils.getFirst(response.getAssertions()))
		val authorities = principal.getAuthorities()
		return Saml2AssertionAuthentication(userDetails, assertion, authorities) (3)
    }

}
1 首先,调用默认转换器,它从响应中提取属性和权限
2 其次,使用相关信息调用 UserDetailsService
3 第三,返回包含用户详细信息的身份验证

如果您的 UserDetailsService 返回一个也实现 AuthenticatedPrincipal 的值,那么您不需要自定义身份验证实现。

不需要调用 OpenSaml5AuthenticationProvider 的默认身份验证转换器。它返回一个 Saml2AuthenticatedPrincipal,其中包含从 AttributeStatement 中提取的属性以及单个 ROLE_USER 权限。

配置主体名称

有时,主体名称不在 <saml2:NameID> 元素中。在这种情况下,您可以使用自定义策略配置 ResponseAuthenticationConverter,如下所示:

  • Java

  • Kotlin

@Bean
ResponseAuthenticationConverter authenticationConverter() {
	ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
	authenticationConverter.setPrincipalNameConverter((assertion) -> {
		// ... work with OpenSAML's Assertion object to extract the principal
	});
	return authenticationConverter;
}
@Bean
fun authenticationConverter(): ResponseAuthenticationConverter {
    val authenticationConverter: ResponseAuthenticationConverter = ResponseAuthenticationConverter()
    authenticationConverter.setPrincipalNameConverter { assertion ->
		// ... work with OpenSAML's Assertion object to extract the principal
    }
    return authenticationConverter
}

配置主体的授予权限

使用 OpenSamlXAuhenticationProvider 时,Spring Security 会自动授予 ROLE_USER。使用 OpenSaml5AuthenticationProvider,您可以配置一组不同的授予权限,如下所示:

  • Java

  • Kotlin

@Bean
ResponseAuthenticationConverter authenticationConverter() {
	ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
	authenticationConverter.setPrincipalNameConverter((assertion) -> {
		// ... grant the needed authorities based on attributes in the assertion
	});
	return authenticationConverter;
}
@Bean
fun authenticationConverter(): ResponseAuthenticationConverter {
    val authenticationConverter = ResponseAuthenticationConverter()
    authenticationConverter.setPrincipalNameConverter{ assertion ->
		// ... grant the needed authorities based on attributes in the assertion
    }
    return authenticationConverter
}

执行额外的响应验证

OpenSaml5AuthenticationProvider 在解密 Response 后立即验证 IssuerDestination 值。您可以通过扩展默认验证器并与您自己的响应验证器连接来定制验证,或者您可以完全替换为您的验证器。

例如,您可以抛出一个自定义异常,其中包含 Response 对象中可用的任何额外信息,如下所示:

OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
ResponseValidator responseValidator = ResponseValidator.withDefaults(myCustomValidator);
provider.setResponseValidator(responseValidator);

您还可以自定义 Spring Security 应该执行的验证步骤。例如,如果您想跳过 Response#InResponseTo 验证,您可以调用 ResponseValidator 的构造函数,将 InResponseToValidator 从列表中排除。

OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
ResponseValidator responseValidator = new ResponseValidator(new DestinationValidator(), new IssuerValidator());
provider.setResponseValidator(responseValidator);

OpenSAML 在其 BearerSubjectConfirmationValidator 类中执行 Asssertion#InResponseTo 验证,该类可以使用 setAssertionValidator 进行配置。

执行额外的断言验证

OpenSaml5AuthenticationProvider 对 SAML 2.0 断言执行最小验证。验证签名后,它将:

  1. 验证 <AudienceRestriction><DelegationRestriction> 条件

  2. 验证 <SubjectConfirmation>,除了任何 IP 地址信息

要执行额外的验证,您可以配置自己的断言验证器,该验证器委托给 OpenSaml5AuthenticationProvider 的默认验证器,然后执行自己的验证。

例如,您可以使用 OpenSAML 的 OneTimeUseConditionValidator 来验证 <OneTimeUse> 条件,如下所示:

  • Java

  • Kotlin

OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
OneTimeUseConditionValidator validator = ...;
AssertionValidator assertionValidator = AssertionValidator.builder()
        .conditionValidators((c) -> c.add(validator)).build();
provider.setAssertionValidator(assertionValidator);
val provider = OpenSaml5AuthenticationProvider()
val validator: OneTimeUseConditionValidator = ...;
val assertionValidator = AssertionValidator.builder()
        .conditionValidators { add(validator) }.build()
provider.setAssertionValidator(assertionValidator)

您可以使用此相同的构建器来删除您不想使用的验证器,如下所示:

  • Java

  • Kotlin

OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
AssertionValidator assertionValidator = AssertionValidator.builder()
        .conditionValidators((c) -> c.removeIf(AudienceRestrictionValidator.class::isInstance)).build();
provider.setAssertionValidator(assertionValidator);
val provider = new OpenSaml5AuthenticationProvider()
val assertionValidator = AssertionValidator.builder()
        .conditionValidators {
			c: List<ConditionValidator> -> c.removeIf { it is AudienceRestrictionValidator }
        }.build()
provider.setAssertionValidator(assertionValidator)

自定义解密

Spring Security 通过使用在 RelyingPartyRegistration 中注册的解密 Saml2X509Credential 实例,自动解密 <saml2:EncryptedAssertion><saml2:EncryptedAttribute><saml2:EncryptedID> 元素。

OpenSaml5AuthenticationProvider 暴露了 两种解密策略。响应解密器用于解密 <saml2:Response> 的加密元素,例如 <saml2:EncryptedAssertion>。断言解密器用于解密 <saml2:Assertion> 的加密元素,例如 <saml2:EncryptedAttribute><saml2:EncryptedID>

您可以将 OpenSaml5AuthenticationProvider 的默认解密策略替换为您自己的策略。例如,如果您有一个单独的服务来解密 <saml2:Response> 中的断言,您可以改用它,如下所示:

  • Java

  • Kotlin

MyDecryptionService decryptionService = ...;
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
val decryptionService: MyDecryptionService = ...
val provider = OpenSaml5AuthenticationProvider()
provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) }

如果您还在解密 <saml2:Assertion> 中的单个元素,您也可以自定义断言解密器:

  • Java

  • Kotlin

provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) }
有两个独立的解密器,因为断言可以与响应分开签名。在签名验证之前尝试解密已签名断言的元素可能会使签名失效。如果您的断言方只签署响应,那么只使用响应解密器解密所有元素是安全的。

使用自定义身份验证管理器

当然,authenticationManager DSL 方法也可以用于执行完全自定义的 SAML 2.0 身份验证。此身份验证管理器应期望一个包含 SAML 2.0 响应 XML 数据的 Saml2AuthenticationToken 对象。

  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...);
        http
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
            )
            .saml2Login((saml2) -> saml2
                .authenticationManager(authenticationManager)
            )
        ;
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...)
        http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = customAuthenticationManager
            }
        }
        return http.build()
    }
}

使用 Saml2AuthenticatedPrincipal

当依赖方为给定的断言方正确配置后,它就可以接受断言了。一旦依赖方验证了断言,结果将是带有 Saml2AuthenticatedPrincipalSaml2Authentication

这意味着您可以在控制器中访问主体,如下所示:

  • Java

  • Kotlin

@Controller
public class MainController {
	@GetMapping("/")
	public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
		String email = principal.getFirstAttribute("email");
		model.setAttribute("email", email);
		return "index";
	}
}
@Controller
class MainController {
    @GetMapping("/")
    fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String {
        val email = principal.getFirstAttribute<String>("email")
        model.setAttribute("email", email)
        return "index"
    }
}
由于 SAML 2.0 规范允许每个属性有多个值,您可以调用 getAttribute 获取属性列表,或者调用 getFirstAttribute 获取列表中的第一个值。当您知道只有一个值时,getFirstAttribute 非常方便。
© . This site is unofficial and not affiliated with VMware.