此版本仍在开发中,尚不被认为是稳定的。对于最新的稳定版本,请使用 Spring Security 6.5.3spring-doc.cadn.net.cn

认证<saml2:Response>s

为了验证 SAML 2.0 响应,Spring Security 使用Saml2AuthenticationTokenConverter以填充Authenticationrequest 和OpenSaml5AuthenticationProvider以验证它。spring-doc.cadn.net.cn

您可以通过多种方式进行配置,包括:spring-doc.cadn.net.cn

  1. 改变方式RelyingPartyRegistration查找spring-doc.cadn.net.cn

  2. 设置时钟偏差以进行时间戳验证spring-doc.cadn.net.cn

  3. 将响应映射到GrantedAuthority实例spring-doc.cadn.net.cn

  4. 自定义验证断言的策略spring-doc.cadn.net.cn

  5. 自定义解密响应和断言元素的策略spring-doc.cadn.net.cn

要配置这些,您将使用saml2Login#authenticationManager方法。spring-doc.cadn.net.cn

更改 SAML 响应处理端点

默认终结点为/login/saml2/sso/{registrationId}. 您可以在 DSL 和关联的元数据中更改此设置,如下所示:spring-doc.cadn.net.cn

@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()
}
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")

改变RelyingPartyRegistration查找

默认情况下,此转换器将与任何关联的<saml2:AuthnRequest>或任何registrationId它在 URL 中找到。 或者,如果在这两种情况下都找不到,那么它会尝试通过<saml2:Response#Issuer>元素。spring-doc.cadn.net.cn

在许多情况下,您可能需要更复杂的东西,例如,如果您正在支持ARTIFACT捆绑。 在这些情况下,您可以通过自定义AuthenticationConverter,您可以按如下方式进行自定义:spring-doc.cadn.net.cn

@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如下:spring-doc.cadn.net.cn

@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实例。spring-doc.cadn.net.cn

您可以通过以下方式设置自定义转换器:spring-doc.cadn.net.cn

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

接下来的示例都建立在这个通用结构之上,向您展示该转换器派上用场的不同方式。spring-doc.cadn.net.cn

UserDetailsService

或者,您可能希望包含旧版中的用户详细信息UserDetailsService. 在这种情况下,响应身份验证转换器可以派上用场,如下所示:spring-doc.cadn.net.cn

@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,则不需要自定义身份验证实现。spring-doc.cadn.net.cn

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

配置主体名称

有时,主体名称不在<saml2:NameID>元素。 在这种情况下,您可以配置ResponseAuthenticationConverter使用如下自定义策略:spring-doc.cadn.net.cn

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

配置主体授予的权限

Spring Security 自动授予ROLE_USER使用时OpenSamlXAuhenticationProvider. 跟OpenSaml5AuthenticationProvider,您可以配置一组不同的授予权限,如下所示:spring-doc.cadn.net.cn

@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验证IssuerDestination值在解密Response. 您可以通过扩展默认验证器与您自己的响应验证器连接来自定义验证,也可以将其完全替换为您的响应验证器。spring-doc.cadn.net.cn

例如,可以抛出自定义异常,其中包含Response对象,如下所示:spring-doc.cadn.net.cn

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

您还可以自定义 Spring Security 应执行的验证步骤。 例如,如果您想跳过Response#InResponseTovalidation,可以调用ResponseValidator的构造函数,不包括InResponseToValidator从列表中:spring-doc.cadn.net.cn

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

OpenSAML 执行Asssertion#InResponseTo验证BearerSubjectConfirmationValidator类,可使用 setAssertionValidator 进行配置。spring-doc.cadn.net.cn

执行其他断言验证

OpenSaml5AuthenticationProvider对 SAML 2.0 断言执行最少的验证。 验证签名后,它将:spring-doc.cadn.net.cn

  1. 驗證<AudienceRestriction><DelegationRestriction>条件spring-doc.cadn.net.cn

  2. 驗證<SubjectConfirmation>s,期望任何 IP 地址信息spring-doc.cadn.net.cn

要执行其他验证,您可以配置自己的断言验证器,该验证器委托给OpenSaml5AuthenticationProvider的默认值,然后执行自己的。spring-doc.cadn.net.cn

例如,您可以使用 OpenSAML 的OneTimeUseConditionValidator以同时验证<OneTimeUse>条件,如下所示:spring-doc.cadn.net.cn

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)

您可以使用相同的构建器来删除您不想使用的验证器,如下所示:spring-doc.cadn.net.cn

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 解密<saml2:EncryptedAssertion>,<saml2:EncryptedAttribute><saml2:EncryptedID>元素通过使用解密自动Saml2X509Credential实例RelyingPartyRegistration.spring-doc.cadn.net.cn

OpenSaml5AuthenticationProvider公开了两种解密策略。 响应解密器用于解密<saml2:Response>喜欢<saml2:EncryptedAssertion>. 断言解密器用于解密<saml2:Assertion>喜欢<saml2:EncryptedAttribute><saml2:EncryptedID>.spring-doc.cadn.net.cn

您可以将OpenSaml5AuthenticationProvider的默认解密策略与您自己的解密策略。 例如,如果您有一个单独的服务来解密<saml2:Response>,您可以这样使用它:spring-doc.cadn.net.cn

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>,您也可以自定义断言解密器:spring-doc.cadn.net.cn

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

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

当然,authenticationManagerDSL 方法还可用于执行完全自定义的 SAML 2.0 身份验证。 此身份验证管理器应期望Saml2AuthenticationToken包含 SAML 2.0 响应 XML 数据的对象。spring-doc.cadn.net.cn

@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

为给定断言方正确配置信赖方后,它就可以接受断言了。 信赖方验证断言后,结果是Saml2Authentication使用Saml2AuthenticatedPrincipal.spring-doc.cadn.net.cn

这意味着您可以像这样访问控制器中的主体:spring-doc.cadn.net.cn

@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当您知道只有一个值时,非常方便。