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

认证<saml2:Response>s

为了验证 SAML 2.0 响应,Spring Security 使用Saml2AuthenticationTokenConverter要填充Authenticationrequest 和OpenSaml4AuthenticationProvider对其进行身份验证。spring-doc.cadn.net.cn

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

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

  2. 将 clock skew 设置为 timestamp 验证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()
}

设置 clock skew

断言方和依赖方的系统时钟不完全同步的情况并不少见。 因此,您可以配置OpenSaml4AuthenticationProvider的默认断言验证器,具有一定的容差:spring-doc.cadn.net.cn

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
        authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider
                .createDefaultAssertionValidator(assertionToken -> {
                    Map<String, Object> params = new HashMap<>();
                    params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
                    // ... other validation parameters
                    return new ValidationContext(params);
                })
        );

        http
            .authorizeHttpRequests(authz -> authz
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(new ProviderManager(authenticationProvider))
            );
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        val authenticationProvider = OpenSaml4AuthenticationProvider()
        authenticationProvider.setAssertionValidator(
            OpenSaml4AuthenticationProvider
                .createDefaultAssertionValidator(Converter<OpenSaml4AuthenticationProvider.AssertionToken, ValidationContext> {
                    val params: MutableMap<String, Any> = HashMap()
                    params[CLOCK_SKEW] =
                        Duration.ofMinutes(10).toMillis()
                    ValidationContext(params)
                })
        )
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = ProviderManager(authenticationProvider)
            }
        }
        return http.build()
    }
}

UserDetailsService

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

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    UserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
        authenticationProvider.setResponseAuthenticationConverter(responseToken -> {
            Saml2Authentication authentication = OpenSaml4AuthenticationProvider
                    .createDefaultResponseAuthenticationConverter() (1)
                    .convert(responseToken);
            Assertion assertion = responseToken.getResponse().getAssertions().get(0);
            String username = assertion.getSubject().getNameID().getValue();
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); (2)
            return MySaml2Authentication(userDetails, authentication); (3)
        });

        http
            .authorizeHttpRequests(authz -> authz
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(new ProviderManager(authenticationProvider))
            );
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
    @Autowired
    var userDetailsService: UserDetailsService? = null

    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        val authenticationProvider = OpenSaml4AuthenticationProvider()
        authenticationProvider.setResponseAuthenticationConverter { responseToken: OpenSaml4AuthenticationProvider.ResponseToken ->
            val authentication = OpenSaml4AuthenticationProvider
                .createDefaultResponseAuthenticationConverter() (1)
                .convert(responseToken)
            val assertion: Assertion = responseToken.response.assertions[0]
            val username: String = assertion.subject.nameID.value
            val userDetails = userDetailsService!!.loadUserByUsername(username) (2)
            MySaml2Authentication(userDetails, authentication) (3)
        }
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = ProviderManager(authenticationProvider)
            }
        }
        return http.build()
    }
}
1 首先,调用默认转换器,该转换器从响应中提取属性和权限
2 其次,调用UserDetailsService使用相关信息
3 第三,返回包含用户详细信息的自定义身份验证
不需要调用OpenSaml4AuthenticationProvider的默认身份验证转换器。 它返回一个Saml2AuthenticatedPrincipal包含它从中提取的属性AttributeStatements 以及单个ROLE_USER柄。

执行其他响应验证

OpenSaml4AuthenticationProvider验证IssuerDestinationResponse. 您可以通过扩展默认验证器与您自己的响应验证器连接来自定义验证,也可以将其完全替换为您的验证器。spring-doc.cadn.net.cn

例如,您可以引发自定义异常,其中包含Responseobject,如下所示:spring-doc.cadn.net.cn

OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseValidator((responseToken) -> {
	Saml2ResponseValidatorResult result = OpenSamlAuthenticationProvider
		.createDefaultResponseValidator()
		.convert(responseToken)
		.concat(myCustomValidator.convert(responseToken));
	if (!result.getErrors().isEmpty()) {
		String inResponseTo = responseToken.getInResponseTo();
		throw new CustomSaml2AuthenticationException(result, inResponseTo);
	}
	return result;
});

执行其他断言验证

OpenSaml4AuthenticationProvider对 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

要执行其他验证,您可以配置自己的断言验证器,该验证器将OpenSaml4AuthenticationProvider的 default,然后执行自己的spring-doc.cadn.net.cn

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

OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
OneTimeUseConditionValidator validator = ...;
provider.setAssertionValidator(assertionToken -> {
    Saml2ResponseValidatorResult result = OpenSaml4AuthenticationProvider
            .createDefaultAssertionValidator()
            .convert(assertionToken);
    Assertion assertion = assertionToken.getAssertion();
    OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse();
    ValidationContext context = new ValidationContext();
    try {
        if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
            return result;
        }
    } catch (Exception e) {
        return result.concat(new Saml2Error(INVALID_ASSERTION, e.getMessage()));
    }
    return result.concat(new Saml2Error(INVALID_ASSERTION, context.getValidationFailureMessage()));
});
var provider = OpenSaml4AuthenticationProvider()
var validator: OneTimeUseConditionValidator = ...
provider.setAssertionValidator { assertionToken ->
    val result = OpenSaml4AuthenticationProvider
        .createDefaultAssertionValidator()
        .convert(assertionToken)
    val assertion: Assertion = assertionToken.assertion
    val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse
    val context = ValidationContext()
    try {
        if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
            return@setAssertionValidator result
        }
    } catch (e: Exception) {
        return@setAssertionValidator result.concat(Saml2Error(INVALID_ASSERTION, e.message))
    }
    result.concat(Saml2Error(INVALID_ASSERTION, context.validationFailureMessage))
}
虽然建议这样做,但无需调用OpenSaml4AuthenticationProvider的默认断言验证器。 您可以跳过它的一种情况是,如果您不需要它来检查<AudienceRestriction><SubjectConfirmation>因为你自己在做这些。

自定义解密

Spring Security 解密<saml2:EncryptedAssertion>,<saml2:EncryptedAttribute><saml2:EncryptedID>元素Saml2X509Credential实例RelyingPartyRegistration.spring-doc.cadn.net.cn

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

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

MyDecryptionService decryptionService = ...;
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
val decryptionService: MyDecryptionService = ...
val provider = OpenSaml4AuthenticationProvider()
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 {
            authorizeRequests {
                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当您知道只有一个值时,它非常方便。