介绍
SAML全称是安全断言标记语言(Security Assertion Markup Language)是一个基于XML的开源标准数据格式。用于在不同的安全域之间交换认证和数据授权。在SAML标准定义了身份提供者(IDP)和服务提供者(SP),这两者构成了前面所说的不同的安全域。 SAML是OASIS组织安全服务技术委员会(Security Services Technical Committee)的产品。
SAML解决的最重要的需求是Web端应用的单点登录(SSO)。
专业术语
名词 | 描述 |
---|---|
SP(Service Provider) | 向用户提供服务的web 端应用 |
IDP(Identity Provide) | 向SP提供用户身份信息,这里指认证中心 |
metadata | 元数据,一种符合SAML协议的xml文件,包含端点的 URL、有关支持的绑定、标识符和公钥的信息。IDP和SP都有各自的元数据,IDP和SP通过交换对方的元数据获取对方的信息和达成互信。 |
流程
SAML请求示例
<?xml version="1.0" encoding="UTF-8"?>
<saml2p:AuthnRequest xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"
AssertionConsumerServiceURL="http://192.168.59.117:30046/spring-boot-saml-sp-demo/saml/SSO"
Destination="http://192.168.59.117:30040/ac/idp/profile/SAML2/POST/SSO"
ForceAuthn="false"
ID="a2jfe492c73g7b1e98b3dg93i3h1"
IsPassive="false"
IssueInstant="2022-02-09T08:33:50.303Z"
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Version="2.0">
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">com:springboot:demo:saml:sp</saml2:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<ds:Reference URI="#a2jfe492c73g7b1e98b3dg93i3h1">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /></ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<ds:DigestValue>woroqO5A74nsOfcH/8CLhQJgDww=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>f9eQl+G5zp45D9Q9ZvnJTVstrnzKNbYBq3aAAtmrQ8ZOudXH5sy03zb4ewc5VVDvLEb9vEH0KvucCx31lJ7vKtQ+8HolMRTwUA3V+wDAMqx36PVjBH3tuxSTzbJCUhaOzdoPJSURJlyVyLg+CDaBxwdcLWVd01tdW7Hk7krKxsj3J6nw7IWFwRLRm0838IMajrhFV9TjVFc2LHzf84RpYeGG3GWbMQyftuK0llMgaY7jdvpvKTNaCINtMpPYW7KQ7KQxUcsfJV9JKP15NHi8785a8y3X9khuTQP+vmEWgF02jwyDym1f5uzfChpDa/LlqAx/mRU42y3N+9tHkS4Fvg==</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>MIIDUjCCAjqgAwIBAgIEUOLIQTANBgkqhkiG9w0BAQUFADBrMQswCQYDVQQGEwJGSTEQMA4GA1UE CBMHVXVzaW1hYTERMA8GA1UEBxMISGVsc2lua2kxGDAWBgNVBAoTD1JNNSBTb2Z0d2FyZSBPeTEM MAoGA1UECwwDUiZEMQ8wDQYDVQQDEwZhcG9sbG8wHhcNMTMwMTAxMTEyODAxWhcNMjIxMjMwMTEy ODAxWjBrMQswCQYDVQQGEwJGSTEQMA4GA1UECBMHVXVzaW1hYTERMA8GA1UEBxMISGVsc2lua2kx
GDAWBgNVBAoTD1JNNSBTb2Z0d2FyZSBPeTEMMAoGA1UECwwDUiZEMQ8wDQYDVQQDEwZhcG9sbG8w ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCXqP0wqL2Ai1haeTj0alwsLafhrDtUt00E 5xc7kdD7PISRA270ZmpYMB4W24Uk2QkuwaBp6dI/yRdUvPfOT45YZrqIxMe2451PAQWtEKWF5Z13 F0J4/lB71TtrzyH94RnqSHXFfvRN8EY/rzuEzrpZrHdtNs9LRyLqcRTXMMO4z7QghBuxh3K5gu7K
qxpHx6No83WNZj4B3gvWLRWv05nbXh/F9YMeQClTX1iBNAhLQxWhwXMKB4u1iPQ/KSaal3R26pON UUmu1qVtU1quQozSTPD8HvsDqGG19v2+/N3uf5dRYtvEPfwXN3wIY+/R93vBA6lnl5nTctZIRsyg 0Gv5AgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAFQwAAYUjso1VwjDc2kypK/RRcB8bMAUUIG0hLGL 82IvnKouGixGqAcULwQKIvTs6uGmlgbSG6Gn5ROb2mlBztXqQ49zRvi5qWNRttir6eyqwRFGOM6A
8rxj3Jhxi2Vb/MJn7XzeVHHLzA1sV5hwl/2PLnaL2h9WyG9QwBbwtmkMEqUt/dgixKb1Rvby/tBu RogWgPONNSACiW+Z5o8UdAOqNMZQozD/i1gOjBXoF0F5OksjQN7xoQZLj9xXefxCFQ69FPcFDeEW bHwSoBy5hLPNALaEUoa5zPDwlixwRjFQTc5XXaRpgIjy/2gsL8+Y5QRhyXnLqgO67BlLYW/GuHE=
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
</saml2p:AuthnRequest>
saml2p:AuthnRequest
根节点,表明这是一个 request 对象* `AssertionConsumerServiceURL` SP解析断言的地址,即ACS地址<br /> * `Destination` 目标地址,即IDP单点登录地址<br /> * `ID` 唯一标识符<br /> * `IssueInstant` 时间戳
saml2:Issuer
SP身份信息,即SP的entityIdds:Signature
签名信息
SAML响应示例
<?xml version="1.0" encoding="UTF-8"?>
<saml2p:Response Destination="http://192.168.59.117:30046/spring-boot-saml-sp-demo/saml/SSO"
ID="_8212989719767153610"
InResponseTo="a52bi1bd5949c0f9gh714d13h41141"
IssueInstant="2022-02-09T08:12:13.155Z"
Version="2.0"
xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol">
<saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://192.168.59.117:30040/ac/idp</saml2:Issuer>
<saml2p:Status>
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></saml2p:Status>
<saml2:Assertion ID="_1417160124216499794"
IssueInstant="2022-02-09T08:12:13.113Z"
Version="2.0"
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
<saml2:Issuer>http://192.168.59.117:30040/ac/idp</saml2:Issuer>
<saml2:Subject>
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
NameQualifier="com:springboot:demo:saml:sp"
SPNameQualifier="com:springboot:demo:saml:sp">admin</saml2:NameID>
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml2:SubjectConfirmationData InResponseTo="a52bi1bd5949c0f9gh714d13h41141"
NotOnOrAfter="2022-02-09T08:12:18.091Z"
Recipient="http://192.168.59.117:30046/spring-boot-saml-sp-demo/saml/SSO" /></saml2:SubjectConfirmation>
</saml2:Subject>
<saml2:Conditions NotBefore="2022-02-09T08:12:13.115Z"
NotOnOrAfter="2022-02-09T08:12:18.115Z">
<saml2:AudienceRestriction>
<saml2:Audience>com:springboot:demo:saml:sp</saml2:Audience>
</saml2:AudienceRestriction>
</saml2:Conditions>
<saml2:AuthnStatement AuthnInstant="2022-02-09T08:12:13.091Z"
SessionIndex="_7124877723365264043">
<saml2:SubjectLocality Address="192.168.59.117" />
<saml2:AuthnContext>
<saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml2:AuthnContextClassRef>
</saml2:AuthnContext>
</saml2:AuthnStatement>
<saml2:AttributeStatement>
<saml2:Attribute FriendlyName="samlAuthenticationStatementAuthMethod"
Name="samlAuthenticationStatementAuthMethod"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue>urn:oasis:names:tc:SAML:1.0:am:password</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="credentialType"
Name="credentialType"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue>UsernamePasswordCredential</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="isFromNewLogin"
Name="isFromNewLogin"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue>true</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="authenticationDate"
Name="authenticationDate"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue>2022-02-09T16:12:12.779+08:00[Asia/Shanghai]</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="authenticationMethod"
Name="authenticationMethod"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue>QueryDatabaseAuthenticationHandler</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="successfulAuthenticationHandlers"
Name="successfulAuthenticationHandlers"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue>QueryDatabaseAuthenticationHandler</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="longTermAuthenticationRequestTokenUsed"
Name="longTermAuthenticationRequestTokenUsed"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue>false</saml2:AttributeValue>
</saml2:Attribute>
</saml2:AttributeStatement>
</saml2:Assertion>
</saml2p:Response>
saml2p:Response
根节点,表明这是一个 response 对象* `Destination` 目标地址,即 ACS 地址,Response 返回的地址<br /> * `ID` 唯一标识<br /> * `IssueInstant` 时间戳
saml2:Issuer
IDP 身份信息,IDP 的 EntityIdsaml2p:Status
认证结果, samlp:StatusCode 表明认证成功或失败saml2:Assertion
断言,这是 Response 中最为重要的字段,里面包含着用户身份信息Signature
断言的签名,使用非对称私钥对 Assertion 内容(不包含 Signature)进行签名,防止信息被篡改saml2:Subject
身份主体,主要包括身份信息saml2:NameID
身份信息saml2:Conditions
给出了断言被认为有效的验证条件。saml2:AuthnStatement
描述了在身份提供者的认证行为。
接入
这里使用spring-security-saml进行接入,该依赖是spring框架的官方库,配置方便、文档详细。提供了包括单点登录、单点登出、获取sq元数据文件等接口,无需自己实现,参考:spring-security-saml与应用程序的集成
用户中心注册
客户端需要先注册到用户中心中,注册后通过编辑页面可以获取到接入的必要服务信息
生成密钥库jks文件
SAML客户端在发送SAML请求时需要进行加密和签名,这就需要密钥,上面配置文件中也有需要去配置jks。jks即密钥库(Java Key Store),里面包可以含多个公钥和私钥。这里介绍如何使用jdk的keytool工具生成私钥和自签名证书。
生成密钥库,密钥库包含了公钥和私钥
# 这个-alias后的值就是密钥库的密钥别名,也就是配置文件中sp.jks.defaultKey的值
keytool -genkeypair -alias qianxing -keyalg RSA -keystore samlKeystore.jks
:::warning
注意:
生成密钥库时会需要你填写密钥库密码,这个密钥库密码就是配置文件中sp.jks.password
的值,还需要填写一些国家、地区、组织的一些信息,如实填写即可
:::
生成公钥,IDP解密时需要使用
keytool -alias qianxing -exportcert -keystore samlKeystore.jks -file public.cer
根据jks生成私钥
keytool -v -importkeystore -srckeystore samlKeystore.jks -srcstoretype jks -destkeystore qianxing.pfx -deststoretype pkcs12
openssl pkcs12 -in qianxing.pfx -nocerts -nodes -out private.key
普通JAVA接入
Maven添加spring-security-saml依赖
<dependency>
<groupId>org.springframework.security.extensions</groupId>
<artifactId>spring-security-saml2-core</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
配置文件中添加客户端相关配置
# 认证中心服务信息 -> IDP元数据URL
sp.idpMetadataUrl=http://192.168.59.117:30030/gc-starter-ac/idp/metadata
# entityId,服务提供商唯一标识
sp.entityId=cas:saml:sp:springboot
# 是否想要IDP签名断言,该项设置后,必须保持IDP注册该应用时是否签名断言也为true,否则客户端报错
sp.wantAssertionSigned=false
# 是否签名元数据,该项设置为true时,在IDP注册应用时必须上传SP客户端的公钥/证书,否则IDP无法验证经过签名的元数据
sp.signMetadata=false
# 签名算法
sp.signAlg=http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
# 是否启用服务发现。一个sp可以配置多个idp,启动服务发现允许进入idp选择页面选择idp,如果不启用的话默认使用idp列表的第一个
sp.idpDiscoveryEnable=true
# 服务发现选择页面路由
sp.IdpSelectionPath=/saml/discovery
# idp登录成功后的重定向的页面路由,也就是首页路由
sp.successLoginUrl=/landing
# idp登录失败后的重定向的页面路由
sp.failLoginUrl=/error
# 登出成功后跳转的页面路由
sp.successLogoutUrl=/
# jks文件位置
sp.jks.path=classpath:/saml/samlKeystore.jks
# jks密码
sp.jks.password=nalle123
# 默认密钥
sp.jks.defaultKey=apollo
通过xml文件的方式配置bean,classpath下新建securityContext.xml
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-3.1.xsd">
<!-- 引用配置文件 -->
<context:property-placeholder location="classpath:application.properties"/>
<!-- Enable auto-wiring -->
<context:annotation-config/>
<!-- Scan for auto-wiring classes in spring saml packages -->
<context:component-scan base-package="org.springframework.security.saml"/>
<!-- IDP Metadata configuration - paths to metadata of IDPs in circle of trust is here -->
<bean id="metadata" class="org.springframework.security.saml.metadata.CachingMetadataManager">
<constructor-arg>
<list>
<!-- Example of classpath metadata with Extended Metadata -->
<!-- <bean class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">-->
<!-- <constructor-arg>-->
<!-- <bean class="org.opensaml.saml2.metadata.provider.ResourceBackedMetadataProvider">-->
<!-- <constructor-arg>-->
<!-- <bean class="java.util.Timer"/>-->
<!-- </constructor-arg>-->
<!-- <constructor-arg>-->
<!-- <bean class="org.opensaml.util.resource.ClasspathResource">-->
<!-- <constructor-arg value="/metadata/idp.xml"/>-->
<!-- </bean>-->
<!-- </constructor-arg>-->
<!-- <property name="parserPool" ref="parserPool"/>-->
<!-- </bean>-->
<!-- </constructor-arg>-->
<!-- <constructor-arg>-->
<!-- <bean class="org.springframework.security.saml.metadata.ExtendedMetadata">-->
<!-- </bean>-->
<!-- </constructor-arg>-->
<!-- </bean>-->
<!-- Example of HTTP metadata without Extended Metadata -->
<bean class="org.opensaml.saml2.metadata.provider.HTTPMetadataProvider">
<!-- URL containing the metadata -->
<!--idp元数据的url-->
<constructor-arg>
<value type="java.lang.String">${sp.idpMetadataUrl}</value>
</constructor-arg>
<!-- Timeout for metadata loading in ms -->
<constructor-arg>
<value type="int">5000</value>
</constructor-arg>
<property name="parserPool" ref="parserPool"/>
</bean>
<!-- Example of file system metadata without Extended Metadata -->
<!--
<bean class="org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider">
<constructor-arg>
<value type="java.io.File">/usr/local/metadata/idp.xml</value>
</constructor-arg>
<property name="parserPool" ref="parserPool"/>
</bean>
-->
</list>
</constructor-arg>
<!-- OPTIONAL used when one of the metadata files contains information about this service provider -->
<!-- <property name="hostedSPName" value=""/> -->
<!-- OPTIONAL property: can tell the system which IDP should be used for authenticating user by default. -->
<!-- <property name="defaultIDP" value="http://localhost:8080/opensso"/> -->
</bean>
<!-- Filter automatically generates default SP metadata -->
<!--客户端元数据生成的相关配置-->
<bean id="metadataGeneratorFilter" class="org.springframework.security.saml.metadata.MetadataGeneratorFilter">
<constructor-arg>
<bean class="org.springframework.security.saml.metadata.MetadataGenerator">
<!--客户端唯一标识-->
<property name="entityId" value="${sp.entityId}" />
<!--是否签名断言-->
<property name="wantAssertionSigned" value="${sp.wantAssertionSigned}" />
<property name="extendedMetadata">
<bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
<!--是否签名元数据-->
<property name="signMetadata" value="${sp.signMetadata}"/>
<!--签名算法-->
<property name="signingAlgorithm" value="${sp.signAlg}"/>
<!--是否启用服务发现-->
<property name="idpDiscoveryEnabled" value="${sp.idpDiscoveryEnable}"/>
</bean>
</property>
</bean>
</constructor-arg>
</bean>
<!-- IDP Discovery Service -->
<!--当启用idp发现的时候,选择idp的页面路径-->
<bean id="samlIDPDiscovery" class="org.springframework.security.saml.SAMLDiscovery">
<property name="idpSelectionPath" value="${sp.IdpSelectionPath}"/>
</bean>
<!-- Handler deciding where to redirect user after successful login -->
<!-- 登录成功后跳转的页面,即客户端首页地址 -->
<bean id="successRedirectHandler"
class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
<property name="defaultTargetUrl" value="${sp.successLoginUrl}"/>
</bean>
<!-- Handler deciding where to redirect user after failed login -->
<!-- 登录失败后跳转的页面 -->
<bean id="failureRedirectHandler"
class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
<property name="useForward" value="true"/>
<property name="defaultFailureUrl" value="${sp.failLoginUrl}"/>
</bean>
<!-- Handler for successful logout -->
<!-- 登出成功后跳转的页面,一般会跳转到登录页面或是应用非登录状态的首页 -->
<bean id="successLogoutHandler" class="org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler">
<property name="defaultTargetUrl" value="${sp.successLogoutUrl}"/>
</bean>
<!-- Central storage of cryptographic keys -->
<!-- jks密钥库设置,密钥库中可包含多个公钥私钥对-->
<bean id="keyManager" class="org.springframework.security.saml.key.JKSKeyManager">
<!--jks文件位置-->
<constructor-arg value="${sp.jks.path}"/>
<!--密钥库密码-->
<constructor-arg type="java.lang.String" value="${sp.jks.password}"/>
<!--密钥库中可用的密钥集合,key为别名,value为密码-->
<constructor-arg>
<map>
<entry key="${sp.jks.defaultKey}" value="${sp.jks.password}"/>
</map>
</constructor-arg>
<!--客户端默认使用的密钥别名-->
<constructor-arg type="java.lang.String" value="${sp.jks.defaultKey}"/>
</bean>
<!-- Unsecured pages -->
<!-- 设置springmvc 静态资源放行,如果有图片、页面或者公钥文件给予放行 -->
<security:http security="none" pattern="/favicon.ico"/>
<security:http security="none" pattern="/images/**"/>
<security:http security="none" pattern="/css/**"/>
<security:http security="none" pattern="/logout.jsp"/>
<security:http security="none" pattern="/key/**"/>
<!-- Security for the administration UI -->
<security:http pattern="/saml/web/**" use-expressions="false">
<security:access-denied-handler error-page="/saml/web/metadata/login"/>
<security:form-login login-processing-url="/saml/web/login" login-page="/saml/web/metadata/login" default-target-url="/saml/web/metadata"/>
<security:intercept-url pattern="/saml/web/metadata/login" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<security:intercept-url pattern="/saml/web/**" access="ROLE_ADMIN"/>
<security:custom-filter before="FIRST" ref="metadataGeneratorFilter"/>
</security:http>
<!-- Secured pages with SAML as entry point -->
<security:http entry-point-ref="samlEntryPoint" use-expressions="false">
<security:intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY"/>
<security:custom-filter before="FIRST" ref="metadataGeneratorFilter"/>
<security:custom-filter after="BASIC_AUTH_FILTER" ref="samlFilter"/>
</security:http>
<!-- Filters for processing of SAML messages -->
<!-- 单点登录相关端点 -->
<bean id="samlFilter" class="org.springframework.security.web.FilterChainProxy">
<security:filter-chain-map request-matcher="ant">
<security:filter-chain pattern="/saml/login/**" filters="samlEntryPoint"/>
<security:filter-chain pattern="/saml/logout/**" filters="samlLogoutFilter"/>
<security:filter-chain pattern="/saml/metadata/**" filters="metadataDisplayFilter"/>
<security:filter-chain pattern="/saml/SSO/**" filters="samlWebSSOProcessingFilter"/>
<security:filter-chain pattern="/saml/SSOHoK/**" filters="samlWebSSOHoKProcessingFilter"/>
<security:filter-chain pattern="/saml/SingleLogout/**" filters="samlLogoutProcessingFilter"/>
<security:filter-chain pattern="/saml/discovery/**" filters="samlIDPDiscovery"/>
</security:filter-chain-map>
</bean>
<!--
Use the following for interpreting RelayState coming from unsolicited response as redirect URL:
<bean id="successRedirectHandler" class="org.springframework.security.saml.SAMLRelayStateSuccessHandler">
<property name="defaultTargetUrl" value="/" />
</bean>
-->
<security:authentication-manager alias="authenticationManager">
<!-- Register authentication manager for SAML provider -->
<security:authentication-provider ref="samlAuthenticationProvider"/>
<!-- Register authentication manager for administration UI -->
<security:authentication-provider>
<security:user-service id="adminInterfaceService">
<security:user name="admin" password="admin" authorities="ROLE_ADMIN"/>
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
<!-- Logger for SAML messages and events -->
<bean id="samlLogger" class="org.springframework.security.saml.log.SAMLDefaultLogger">
<!-- Enable these to see the actual SAML Messages in logs -->
<!-- <property name="logAllMessages" value="true"/> -->
<!-- <property name="logErrors" value="true"/> -->
<!-- <property name="logMessagesOnException" value="true"/> -->
</bean>
<!-- Entry point to initialize authentication, default values taken from properties file -->
<bean id="samlEntryPoint" class="org.springframework.security.saml.SAMLEntryPoint">
<property name="defaultProfileOptions">
<bean class="org.springframework.security.saml.websso.WebSSOProfileOptions">
<property name="includeScoping" value="false"/>
</bean>
</property>
</bean>
<!-- The filter is waiting for connections on URL suffixed with filterSuffix and presents SP metadata there -->
<bean id="metadataDisplayFilter" class="org.springframework.security.saml.metadata.MetadataDisplayFilter"/>
<!-- Configure HTTP Client to accept certificates from the keystore for HTTPS verification -->
<!--
<bean class="org.springframework.security.saml.trust.httpclient.TLSProtocolConfigurer">
<property name="sslHostnameVerification" value="default"/>
</bean>
-->
<!-- SAML Authentication Provider responsible for validating of received SAML messages -->
<bean id="samlAuthenticationProvider" class="org.springframework.security.saml.SAMLAuthenticationProvider">
<!-- OPTIONAL property: can be used to store/load user data after login -->
<!--
<property name="userDetails" ref="bean" />
-->
</bean>
<!-- Provider of default SAML Context -->
<bean id="contextProvider" class="org.springframework.security.saml.context.SAMLContextProviderImpl"/>
<!-- Processing filter for WebSSO profile messages -->
<bean id="samlWebSSOProcessingFilter" class="org.springframework.security.saml.SAMLProcessingFilter">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="authenticationSuccessHandler" ref="successRedirectHandler"/>
<property name="authenticationFailureHandler" ref="failureRedirectHandler"/>
</bean>
<!-- Processing filter for WebSSO Holder-of-Key profile -->
<bean id="samlWebSSOHoKProcessingFilter" class="org.springframework.security.saml.SAMLWebSSOHoKProcessingFilter">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="authenticationSuccessHandler" ref="successRedirectHandler"/>
<property name="authenticationFailureHandler" ref="failureRedirectHandler"/>
</bean>
<!-- Logout handler terminating local session -->
<bean id="logoutHandler"
class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler">
<property name="invalidateHttpSession" value="false"/>
</bean>
<!-- Override default logout processing filter with the one processing SAML messages -->
<bean id="samlLogoutFilter" class="org.springframework.security.saml.SAMLLogoutFilter">
<constructor-arg index="0" ref="successLogoutHandler"/>
<constructor-arg index="1" ref="logoutHandler"/>
<constructor-arg index="2" ref="logoutHandler"/>
</bean>
<!-- Filter processing incoming logout messages -->
<!-- First argument determines URL user will be redirected to after successful global logout -->
<bean id="samlLogoutProcessingFilter" class="org.springframework.security.saml.SAMLLogoutProcessingFilter">
<constructor-arg index="0" ref="successLogoutHandler"/>
<constructor-arg index="1" ref="logoutHandler"/>
</bean>
<!-- Class loading incoming SAML messages from httpRequest stream -->
<bean id="processor" class="org.springframework.security.saml.processor.SAMLProcessorImpl">
<constructor-arg>
<list>
<ref bean="redirectBinding"/>
<ref bean="postBinding"/>
<ref bean="artifactBinding"/>
<ref bean="soapBinding"/>
<ref bean="paosBinding"/>
</list>
</constructor-arg>
</bean>
<!-- SAML 2.0 WebSSO Assertion Consumer -->
<bean id="webSSOprofileConsumer" class="org.springframework.security.saml.websso.WebSSOProfileConsumerImpl"/>
<!-- SAML 2.0 Holder-of-Key WebSSO Assertion Consumer -->
<bean id="hokWebSSOprofileConsumer" class="org.springframework.security.saml.websso.WebSSOProfileConsumerHoKImpl"/>
<!-- SAML 2.0 Web SSO profile -->
<bean id="webSSOprofile" class="org.springframework.security.saml.websso.WebSSOProfileImpl"/>
<!-- SAML 2.0 Holder-of-Key Web SSO profile -->
<bean id="hokWebSSOProfile" class="org.springframework.security.saml.websso.WebSSOProfileConsumerHoKImpl"/>
<!-- SAML 2.0 ECP profile -->
<bean id="ecpprofile" class="org.springframework.security.saml.websso.WebSSOProfileECPImpl"/>
<!-- SAML 2.0 Logout Profile -->
<bean id="logoutprofile" class="org.springframework.security.saml.websso.SingleLogoutProfileImpl"/>
<!-- Bindings, encoders and decoders used for creating and parsing messages -->
<bean id="postBinding" class="org.springframework.security.saml.processor.HTTPPostBinding">
<constructor-arg ref="parserPool"/>
<constructor-arg ref="velocityEngine"/>
</bean>
<bean id="redirectBinding" class="org.springframework.security.saml.processor.HTTPRedirectDeflateBinding">
<constructor-arg ref="parserPool"/>
</bean>
<bean id="artifactBinding" class="org.springframework.security.saml.processor.HTTPArtifactBinding">
<constructor-arg ref="parserPool"/>
<constructor-arg ref="velocityEngine"/>
<constructor-arg>
<bean class="org.springframework.security.saml.websso.ArtifactResolutionProfileImpl">
<constructor-arg>
<bean class="org.apache.commons.httpclient.HttpClient">
<constructor-arg>
<bean class="org.apache.commons.httpclient.MultiThreadedHttpConnectionManager"/>
</constructor-arg>
</bean>
</constructor-arg>
<property name="processor">
<bean class="org.springframework.security.saml.processor.SAMLProcessorImpl">
<constructor-arg ref="soapBinding"/>
</bean>
</property>
</bean>
</constructor-arg>
</bean>
<bean id="soapBinding" class="org.springframework.security.saml.processor.HTTPSOAP11Binding">
<constructor-arg ref="parserPool"/>
</bean>
<bean id="paosBinding" class="org.springframework.security.saml.processor.HTTPPAOS11Binding">
<constructor-arg ref="parserPool"/>
</bean>
<!-- Initialization of OpenSAML library-->
<bean class="org.springframework.security.saml.SAMLBootstrap"/>
<!-- Initialization of the velocity engine -->
<bean id="velocityEngine" class="org.springframework.security.saml.util.VelocityFactory" factory-method="getEngine"/>
<!--
XML parser pool needed for OpenSAML parsing
WARNING: If customizing a ParserPool implementation See https://shibboleth.net/community/advisories/secadv_20131213.txt
Specifically the following should be explicitly set to avoid exploits:
1) set pool property 'expandEntityReferences' to 'false'
2) set feature 'javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING' to true
3) set feature 'https://apache.org/xml/features/disallow-doctype-decl' to true. This is a Xerces-specific feature,
including derivatives such as the internal JAXP implementations supplied with the Oracle and OpenJDK JREs. For
other JAXP implementations, consult the documentation for the implementation for guidance on how to achieve a
similar configuration.
-->
<bean id="parserPool" class="org.opensaml.xml.parse.StaticBasicParserPool" init-method="initialize"/>
<bean id="parserPoolHolder" class="org.springframework.security.saml.parser.ParserPoolHolder"/>
</beans>
- web.xml文件中添加该bean
<context-param> <param-name>contextConfigLocation</param-name> <param-value> /WEB-INF/securityContext.xml </param-value> </context-param>
SpringBoot接入
Maven添加spring-security-saml依赖
...
<dependency>
<groupId>org.springframework.security.extensions</groupId>
<artifactId>spring-security-saml2-core</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
...
# 还需要springboot的安全管理依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
配置文件添加客户端相关配置,其实与SpringMVC工程一致,这里展示的是yml文件方式
server:
port: 9092
servlet:
context-path: /saml-demo
tomcat:
max-http-form-post-size: 2MB
max-http-header-size: 2MB
spring:
main:
# 请添加允许循环依赖,否则会启动报错
allow-circular-references: TRUE
sp:
# 认证中心服务信息 -> IDP元数据URL
idpMetadataUrl: http://localhost:8080/gc-starter-ac/idp/metadata
# entityId,服务提供商唯一标识
entityId: cas:saml:sp:springboot
# 是否想要IDP签名断言,该项设置后,必须保持IDP注册该应用时是否签名断言也为true,否则客户端报错
wantAssertionSigned: false
# 是否签名元数据,该项设置为true时,在IDP注册应用时必须上传SP客户端的公钥/证书,否则IDP无法验证经过签名的元数据
signMetadata: false
# 签名算法
signAlg: http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
# 是否启用服务发现。一个sp可以配置多个idp,启动服务发现允许进入idp选择页面选择idp,如果不启用的话默认使用idp列表的第一个
idpDiscoveryEnable: true
# 服务发现选择页面路由,页面可以参考下面的客户端示例
IdpSelectionPath: /saml/discovery
# idp登录成功后的重定向的页面路由,也就是首页路由
successLoginUrl: /landing
# idp登录失败后的重定向的页面路由
failLoginUrl: /error
# 登出成功后跳转的页面路由
successLogoutUrl: /
# 密钥库设置
jks:
# jks文件位置
path: classpath:/saml/samlKeystore.jks
# jks密码
password: nalle123
# 默认密钥
defaultKey: apollo
配置类
- MVC配置
```java
/*
- Copyright 2021 Vincenzo De Notaris *
- Licensed under the Apache License, Version 2.0 (the “License”);
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at *
- http://www.apache.org/licenses/LICENSE-2.0 *
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an “AS IS” BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License. */
package com.example.samldemo.config;
import com.example.samldemo.core.CurrentUserHandlerMethodArgumentResolver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration public class MvcConfig implements WebMvcConfigurer {
# 自定义的参数处理器
@Autowired
CurrentUserHandlerMethodArgumentResolver currentUserHandlerMethodArgumentResolver;
# 首页路由
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("pages/index");
}
# 静态资源映射
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!registry.hasMappingForPattern("/static/**")) {
registry.addResourceHandler("/static/**")
.addResourceLocations("/static/");
}
}
# 注册自定义参数处理器
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers){
argumentResolvers.add(currentUserHandlerMethodArgumentResolver);
}
}
- SP配置
```java
package com.example.samldemo.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "sp")
@Data
public class SpConfig {
/**
* sp唯一表示符
*/
private String entityId;
/**
* 是否签名断言
*/
private Boolean wantAssertionSigned;
/**
* 是否签名响应
*/
private Boolean signMetadata;
/**
* 是否允许idp发现,如果为是,将会允许用户在页面上选择idp,如果为否,将会默认使用第一个idp
*/
private Boolean idpDiscoveryEnable;
/**
* idp元数据文件
*/
private String idpMetadataUrl;
/**
* 签名算法
*/
private String signAlg;
/**
* idp选择页面路由
*/
private String idpSelectionPath;
/**
* idp成功登录之后重定向的页面路由,也就是首页路由
*/
private String successLoginUrl;
/**
* idp登录失败后重定向的页面路由
*/
private String failLoginUrl;
/**
* 登出成功后跳转的页面路由
*/
private String successLogoutUrl;
/**
* 密钥库配置
*/
@NestedConfigurationProperty
private JksConfig jks;
}
package com.example.samldemo.config;
import lombok.Data;
@Data
public class JksConfig {
/**
* jks文件位置
*/
private String path;
/**
* 密钥库密码
*/
private String password;
/**
* 默认密钥别名
*/
private String defaultKey;
}
自定义身份认证对象方法
这是spring-security-saml提供的接口,可以从SAML断言中解析属性,这里是用来获取认证的用户信息。
/*
* Copyright 2021 Vincenzo De Notaris
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.samldemo.core;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.saml.SAMLCredential;
import org.springframework.security.saml.userdetails.SAMLUserDetailsService;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class SAMLUserDetailsServiceImpl implements SAMLUserDetailsService {
// Logger
private static final Logger LOG = LoggerFactory.getLogger(SAMLUserDetailsServiceImpl.class);
@Override
public Object loadUserBySAML(SAMLCredential credential)
throws UsernameNotFoundException {
// The method is supposed to identify local account of user referenced by
// data in the SAML assertion and return UserDetails object describing the user.
String userID = credential.getNameID().getValue();
LOG.info(userID + " is logged in");
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER");
authorities.add(authority);
// In a real scenario, this implementation has to locate user in a arbitrary
// dataStore based on information present in the SAMLCredential and
// returns such a date in a form of application specific UserDetails object.
return new User(userID, "<abc123>", true, true, true, true, authorities);
}
}
创建spring-security-saml的配置Bean,WebSecurityConfig.class
/*
* Copyright 2021 Vincenzo De Notaris
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.vdenotaris.spring.boot.security.saml.web.config;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.velocity.app.VelocityEngine;
import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.opensaml.xml.parse.ParserPool;
import org.opensaml.xml.parse.StaticBasicParserPool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.saml.SAMLAuthenticationProvider;
import org.springframework.security.saml.SAMLBootstrap;
import org.springframework.security.saml.SAMLDiscovery;
import org.springframework.security.saml.SAMLEntryPoint;
import org.springframework.security.saml.SAMLLogoutFilter;
import org.springframework.security.saml.SAMLLogoutProcessingFilter;
import org.springframework.security.saml.SAMLProcessingFilter;
import org.springframework.security.saml.SAMLWebSSOHoKProcessingFilter;
import org.springframework.security.saml.context.SAMLContextProviderImpl;
import org.springframework.security.saml.key.JKSKeyManager;
import org.springframework.security.saml.key.KeyManager;
import org.springframework.security.saml.log.SAMLDefaultLogger;
import org.springframework.security.saml.metadata.CachingMetadataManager;
import org.springframework.security.saml.metadata.ExtendedMetadata;
import org.springframework.security.saml.metadata.ExtendedMetadataDelegate;
import org.springframework.security.saml.metadata.MetadataDisplayFilter;
import org.springframework.security.saml.metadata.MetadataGenerator;
import org.springframework.security.saml.metadata.MetadataGeneratorFilter;
import org.springframework.security.saml.parser.ParserPoolHolder;
import org.springframework.security.saml.processor.HTTPArtifactBinding;
import org.springframework.security.saml.processor.HTTPPAOS11Binding;
import org.springframework.security.saml.processor.HTTPPostBinding;
import org.springframework.security.saml.processor.HTTPRedirectDeflateBinding;
import org.springframework.security.saml.processor.HTTPSOAP11Binding;
import org.springframework.security.saml.processor.SAMLBinding;
import org.springframework.security.saml.processor.SAMLProcessorImpl;
import org.springframework.security.saml.util.VelocityFactory;
import org.springframework.security.saml.websso.ArtifactResolutionProfile;
import org.springframework.security.saml.websso.ArtifactResolutionProfileImpl;
import org.springframework.security.saml.websso.SingleLogoutProfile;
import org.springframework.security.saml.websso.SingleLogoutProfileImpl;
import org.springframework.security.saml.websso.WebSSOProfile;
import org.springframework.security.saml.websso.WebSSOProfileConsumer;
import org.springframework.security.saml.websso.WebSSOProfileConsumerHoKImpl;
import org.springframework.security.saml.websso.WebSSOProfileConsumerImpl;
import org.springframework.security.saml.websso.WebSSOProfileECPImpl;
import org.springframework.security.saml.websso.WebSSOProfileImpl;
import org.springframework.security.saml.websso.WebSSOProfileOptions;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import com.vdenotaris.spring.boot.security.saml.web.core.SAMLUserDetailsServiceImpl;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements InitializingBean, DisposableBean {
# 注入配置类
@Autowired
private SpConfig spConfig;
# 注入自定义的身份验证对象的方法
@Autowired
private SAMLUserDetailsServiceImpl samlUserDetailsServiceImpl;
private Timer backgroundTaskTimer;
private MultiThreadedHttpConnectionManager multiThreadedHttpConnectionManager;
public void init() {
this.backgroundTaskTimer = new Timer(true);
this.multiThreadedHttpConnectionManager = new MultiThreadedHttpConnectionManager();
}
public void shutdown() {
this.backgroundTaskTimer.purge();
this.backgroundTaskTimer.cancel();
this.multiThreadedHttpConnectionManager.shutdown();
}
// Initialization of the velocity engine
@Bean
public VelocityEngine velocityEngine() {
return VelocityFactory.getEngine();
}
// XML parser pool needed for OpenSAML parsing
@Bean(initMethod = "initialize")
public StaticBasicParserPool parserPool() {
return new StaticBasicParserPool();
}
@Bean(name = "parserPoolHolder")
public ParserPoolHolder parserPoolHolder() {
return new ParserPoolHolder();
}
// Bindings, encoders and decoders used for creating and parsing messages
@Bean
public HttpClient httpClient() {
return new HttpClient(this.multiThreadedHttpConnectionManager);
}
// SAML Authentication Provider responsible for validating of received SAML
// messages
@Bean
public SAMLAuthenticationProvider samlAuthenticationProvider() {
SAMLAuthenticationProvider samlAuthenticationProvider = new SAMLAuthenticationProvider();
samlAuthenticationProvider.setUserDetails(samlUserDetailsServiceImpl);
samlAuthenticationProvider.setForcePrincipalAsString(false);
return samlAuthenticationProvider;
}
// Provider of default SAML Context
@Bean
public SAMLContextProviderImpl contextProvider() {
return new SAMLContextProviderImpl();
}
// Initialization of OpenSAML library
@Bean
public static SAMLBootstrap sAMLBootstrap() {
return new SAMLBootstrap();
}
// Logger for SAML messages and events
@Bean
public SAMLDefaultLogger samlLogger() {
return new SAMLDefaultLogger();
}
// SAML 2.0 WebSSO Assertion Consumer
@Bean
public WebSSOProfileConsumer webSSOprofileConsumer() {
return new WebSSOProfileConsumerImpl();
}
// SAML 2.0 Holder-of-Key WebSSO Assertion Consumer
@Bean
public WebSSOProfileConsumerHoKImpl hokWebSSOprofileConsumer() {
return new WebSSOProfileConsumerHoKImpl();
}
// SAML 2.0 Web SSO profile
@Bean
public WebSSOProfile webSSOprofile() {
return new WebSSOProfileImpl();
}
// SAML 2.0 Holder-of-Key Web SSO profile
@Bean
public WebSSOProfileConsumerHoKImpl hokWebSSOProfile() {
return new WebSSOProfileConsumerHoKImpl();
}
// SAML 2.0 ECP profile
@Bean
public WebSSOProfileECPImpl ecpprofile() {
return new WebSSOProfileECPImpl();
}
@Bean
public SingleLogoutProfile logoutprofile() {
return new SingleLogoutProfileImpl();
}
// sp密钥库
// Central storage of cryptographic keys
@Bean
public KeyManager keyManager() {
DefaultResourceLoader loader = new DefaultResourceLoader();
Resource storeFile = loader
.getResource(spConfig.getJks().getPath());
String storePass = spConfig.getJks().getPassword();
Map<String, String> passwords = new HashMap<String, String>();
passwords.put(spConfig.getJks().getDefaultKey(), spConfig.getJks().getPassword());
String defaultKey = spConfig.getJks().getDefaultKey();
return new JKSKeyManager(storeFile, storePass, passwords, defaultKey);
}
@Bean
public WebSSOProfileOptions defaultWebSSOProfileOptions() {
WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions();
webSSOProfileOptions.setIncludeScoping(false);
return webSSOProfileOptions;
}
// Entry point to initialize authentication, default values taken from
// properties file
@Bean
public SAMLEntryPoint samlEntryPoint() {
SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint();
samlEntryPoint.setDefaultProfileOptions(defaultWebSSOProfileOptions());
return samlEntryPoint;
}
// 扩展元数据
// Setup advanced info about metadata
@Bean
public ExtendedMetadata extendedMetadata() {
ExtendedMetadata extendedMetadata = new ExtendedMetadata();
extendedMetadata.setIdpDiscoveryEnabled(spConfig.getIdpDiscoveryEnable());
extendedMetadata.setSigningAlgorithm(spConfig.getSignAlg());
extendedMetadata.setSignMetadata(spConfig.getSignMetadata());
extendedMetadata.setEcpEnabled(true);
return extendedMetadata;
}
// 服务发现页面地址
// IDP Discovery Service
@Bean
public SAMLDiscovery samlIDPDiscovery() {
SAMLDiscovery idpDiscovery = new SAMLDiscovery();
idpDiscovery.setIdpSelectionPath(spConfig.getIdpSelectionPath());
return idpDiscovery;
}
@Bean
@Qualifier("idp-ssocircle")
public ExtendedMetadataDelegate ssoCircleExtendedMetadataProvider()
throws MetadataProviderException {
String idpSSOCircleMetadataURL = spConfig.getIdpMetadataUrl();
HTTPMetadataProvider httpMetadataProvider = new HTTPMetadataProvider(
this.backgroundTaskTimer, httpClient(), idpSSOCircleMetadataURL);
httpMetadataProvider.setParserPool(parserPool());
ExtendedMetadataDelegate extendedMetadataDelegate =
new ExtendedMetadataDelegate(httpMetadataProvider, extendedMetadata());
extendedMetadataDelegate.setMetadataTrustCheck(false);
extendedMetadataDelegate.setMetadataRequireSignature(false);
backgroundTaskTimer.purge();
return extendedMetadataDelegate;
}
// IDP Metadata configuration - paths to metadata of IDPs in circle of trust
// is here
// Do no forget to call iniitalize method on providers
@Bean
@Qualifier("metadata")
public CachingMetadataManager metadata() throws MetadataProviderException {
List<MetadataProvider> providers = new ArrayList<MetadataProvider>();
providers.add(ssoCircleExtendedMetadataProvider());
return new CachingMetadataManager(providers);
}
// 元数据生成bean
// Filter automatically generates default SP metadata
@Bean
public MetadataGenerator metadataGenerator() {
MetadataGenerator metadataGenerator = new MetadataGenerator();
metadataGenerator.setEntityId(spConfig.getEntityId());
metadataGenerator.setExtendedMetadata(extendedMetadata());
metadataGenerator.setIncludeDiscoveryExtension(false);
metadataGenerator.setKeyManager(keyManager());
metadataGenerator.setWantAssertionSigned(spConfig.getWantAssertionSigned());
return metadataGenerator;
}
// The filter is waiting for connections on URL suffixed with filterSuffix
// and presents SP metadata there
@Bean
public MetadataDisplayFilter metadataDisplayFilter() {
return new MetadataDisplayFilter();
}
// 设置登陆成功后的重定向地址,或者说是首页地址
// Handler deciding where to redirect user after successful login
@Bean
public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() {
SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler =
new SavedRequestAwareAuthenticationSuccessHandler();
successRedirectHandler.setDefaultTargetUrl(spConfig.getSuccessLoginUrl());
return successRedirectHandler;
}
// Handler deciding where to redirect user after failed login
@Bean
public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() {
SimpleUrlAuthenticationFailureHandler failureHandler =
new SimpleUrlAuthenticationFailureHandler();
failureHandler.setUseForward(true);
failureHandler.setDefaultFailureUrl(spConfig.getFailLoginUrl());
return failureHandler;
}
@Bean
public SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter() throws Exception {
SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter = new SAMLWebSSOHoKProcessingFilter();
samlWebSSOHoKProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler());
samlWebSSOHoKProcessingFilter.setAuthenticationManager(authenticationManager());
samlWebSSOHoKProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
return samlWebSSOHoKProcessingFilter;
}
// Processing filter for WebSSO profile messages
@Bean
public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception {
SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager());
samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler());
samlWebSSOProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
return samlWebSSOProcessingFilter;
}
@Bean
public MetadataGeneratorFilter metadataGeneratorFilter() {
return new MetadataGeneratorFilter(metadataGenerator());
}
// Handler for successful logout
@Bean
public SimpleUrlLogoutSuccessHandler successLogoutHandler() {
SimpleUrlLogoutSuccessHandler successLogoutHandler = new SimpleUrlLogoutSuccessHandler();
successLogoutHandler.setDefaultTargetUrl(spConfig.getSuccessLogoutUrl());
return successLogoutHandler;
}
// Logout handler terminating local session
@Bean
public SecurityContextLogoutHandler logoutHandler() {
SecurityContextLogoutHandler logoutHandler =
new SecurityContextLogoutHandler();
logoutHandler.setInvalidateHttpSession(true);
logoutHandler.setClearAuthentication(true);
return logoutHandler;
}
// Filter processing incoming logout messages
// First argument determines URL user will be redirected to after successful
// global logout
@Bean
public SAMLLogoutProcessingFilter samlLogoutProcessingFilter() {
return new SAMLLogoutProcessingFilter(successLogoutHandler(),
logoutHandler());
}
// Overrides default logout processing filter with the one processing SAML
// messages
@Bean
public SAMLLogoutFilter samlLogoutFilter() {
return new SAMLLogoutFilter(successLogoutHandler(),
new LogoutHandler[] { logoutHandler() },
new LogoutHandler[] { logoutHandler() });
}
// Bindings
private ArtifactResolutionProfile artifactResolutionProfile() {
final ArtifactResolutionProfileImpl artifactResolutionProfile =
new ArtifactResolutionProfileImpl(httpClient());
artifactResolutionProfile.setProcessor(new SAMLProcessorImpl(soapBinding()));
return artifactResolutionProfile;
}
@Bean
public HTTPArtifactBinding artifactBinding(ParserPool parserPool, VelocityEngine velocityEngine) {
return new HTTPArtifactBinding(parserPool, velocityEngine, artifactResolutionProfile());
}
@Bean
public HTTPSOAP11Binding soapBinding() {
return new HTTPSOAP11Binding(parserPool());
}
@Bean
public HTTPPostBinding httpPostBinding() {
return new HTTPPostBinding(parserPool(), velocityEngine());
}
@Bean
public HTTPRedirectDeflateBinding httpRedirectDeflateBinding() {
return new HTTPRedirectDeflateBinding(parserPool());
}
@Bean
public HTTPSOAP11Binding httpSOAP11Binding() {
return new HTTPSOAP11Binding(parserPool());
}
@Bean
public HTTPPAOS11Binding httpPAOS11Binding() {
return new HTTPPAOS11Binding(parserPool());
}
// Processor
@Bean
public SAMLProcessorImpl processor() {
Collection<SAMLBinding> bindings = new ArrayList<SAMLBinding>();
bindings.add(httpRedirectDeflateBinding());
bindings.add(httpPostBinding());
bindings.add(artifactBinding(parserPool(), velocityEngine()));
bindings.add(httpSOAP11Binding());
bindings.add(httpPAOS11Binding());
return new SAMLProcessorImpl(bindings);
}
/**
* Define the security filter chain in order to support SSO Auth by using SAML 2.0
*
* @return Filter chain proxy
* @throws Exception
*/
@Bean
public FilterChainProxy samlFilter() throws Exception {
List<SecurityFilterChain> chains = new ArrayList<SecurityFilterChain>();
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"),
samlEntryPoint()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"),
samlLogoutFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/metadata/**"),
metadataDisplayFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"),
samlWebSSOProcessingFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSOHoK/**"),
samlWebSSOHoKProcessingFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"),
samlLogoutProcessingFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/discovery/**"),
samlIDPDiscovery()));
return new FilterChainProxy(chains);
}
/**
* Returns the authentication manager currently used by Spring.
* It represents a bean definition with the aim allow wiring from
* other classes performing the Inversion of Control (IoC).
*
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* Defines the web based security configuration.
*
* @param http It allows configuring web based security for specific http requests.
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.authenticationEntryPoint(samlEntryPoint());
http
.addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
.addFilterAfter(samlFilter(), BasicAuthenticationFilter.class)
.addFilterBefore(samlFilter(), CsrfFilter.class);
http
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/saml/**").permitAll()
.antMatchers("/css/**").permitAll()
.antMatchers("/img/**").permitAll()
.antMatchers("/js/**").permitAll()
.anyRequest().authenticated();
http
.logout()
.disable(); // The logout procedure is already handled by SAML filters.
}
/**
* Sets a custom authentication provider.
*
* @param auth SecurityBuilder used to create an AuthenticationManager.
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(samlAuthenticationProvider());
}
@Override
public void afterPropertiesSet() throws Exception {
init();
}
@Override
public void destroy() throws Exception {
shutdown();
}
}
设置自定义参数处理器
- 添加自定义注解
```java
/*
- Copyright 2021 Vincenzo De Notaris *
- Licensed under the Apache License, Version 2.0 (the “License”);
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at *
- http://www.apache.org/licenses/LICENSE-2.0 *
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an “AS IS” BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License. */
package com.example.samldemo.stereotypes;
import java.lang.annotation.*;
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CurrentUser {}
- 自定义参数处理器类
```java
/*
* Copyright 2021 Vincenzo De Notaris
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.samldemo.core;
import com.example.samldemo.stereotypes.CurrentUser;
import org.springframework.core.MethodParameter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebArgumentResolver;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import java.security.Principal;
@Component
public class CurrentUserHandlerMethodArgumentResolver implements
HandlerMethodArgumentResolver {
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterAnnotation(CurrentUser.class) != null
&& methodParameter.getParameterType().equals(User.class);
}
public Object resolveArgument(MethodParameter methodParameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
if (this.supportsParameter(methodParameter)) {
Principal principal = (Principal) webRequest.getUserPrincipal();
return (User) ((Authentication) principal).getPrincipal();
} else {
return WebArgumentResolver.UNRESOLVED;
}
}
}
控制层
- 获取IDP列表并跳转到IDP服务发现页面的controller
```java
/*
- Copyright 2021 Vincenzo De Notaris *
- Licensed under the Apache License, Version 2.0 (the “License”);
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at *
- http://www.apache.org/licenses/LICENSE-2.0 *
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an “AS IS” BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License. */
package com.example.samldemo.controller;
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.saml.metadata.MetadataManager; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.http.HttpServletRequest; import java.util.Set;
@Controller @RequestMapping(“/saml”) public class SSOController {
// Logger
private static final Logger LOG = LoggerFactory
.getLogger(SSOController.class);
@Autowired
private MetadataManager metadata;
@RequestMapping(value = "/discovery", method = RequestMethod.GET)
public String idpSelection(HttpServletRequest request, Model model) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null)
LOG.debug("Current authentication instance from security context is null");
else
LOG.debug("Current authentication instance from security context: "
+ this.getClass().getSimpleName());
if (auth == null || (auth instanceof AnonymousAuthenticationToken)) {
Set<String> idps = metadata.getIDPEntityNames();
for (String idp : idps)
LOG.info("Configured Identity Provider for SSO: " + idp);
model.addAttribute("idps", idps);
return "pages/discovery";
} else {
LOG.warn("The current user is already logged.");
return "redirect:/landing";
}
}
}
<a name="e4KlK"></a>
#### <br />
- 验证认证信息并跳转到首页的控制层
```java
/*
* Copyright 2021 Vincenzo De Notaris
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.samldemo.controller;
import com.example.samldemo.stereotypes.CurrentUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class LandingController {
// Logger
private static final Logger LOG = LoggerFactory
.getLogger(LandingController.class);
@RequestMapping("/landing")
public String landing(@CurrentUser User user, Model model) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null) {
LOG.debug("Current authentication instance from security context is null");
} else {
LOG.debug("Current authentication instance from security context: "
+ this.getClass().getSimpleName());
}
model.addAttribute("username", user.getUsername());
return "pages/landing";
}
}
SpringBoot客户端Demo下载
完整代码请参考:SAML客户端示例
SAML客户端签名
- 生成密钥库jks文件,并放入配置文件中
sp.jks.path
中配置的路径下 - 按照上述的命令生成公钥文件
修改配置文件
... # 是否想要IDP签名断言,该项设置后,必须保持IDP注册该应用时是否签名断言也为true,否则客户端报错 wantAssertionSigned: true # 是否签名元数据,该项设置为true时,在IDP注册应用时必须上传SP客户端的公钥/证书,否则IDP无法验证经过签名的元数据 signMetadata: true ...
用户中心新增/修改应用的配置,将签名断言、加密断言、签名响应改为是,并上传SP客户端的元数据证书/公钥文件
获取SP元数据的API
http://ip:prot/context-path/saml/metadata
常见问题
时间同步
SAML 消息和断言的处理通常限于特定的时间窗口,例如防止重放攻击的可能性。当 IDP 和 SP 机器的内部时钟不同步时,消息验证可能会失败。确保在用户中心注册的所有应用使用时间同步服务。
SSO 期间出现错误“InResponseToField doesn’t correspond to sent message”
确保应用程序在发送请求和接收响应期间使用相同的 HttpSession。通常,当身份验证请求从 localhost 地址或 http 协议发出,而响应是在公开的域名或 https 协议中接收的就会出现此问题。例如,从 https://host:port/app/saml/login 初始化身份验证时,必须在 https://host:port/app/saml/SSO 接收响应,而不是 http://host:port/ app/saml/SSO 或 https://localhost:port/app/saml/SSO。
可以通过重新配置上下文来禁用 InResponseToField 的检查,如下所示:
<bean id="contextProvider" class="org.springframework.security.saml.context.SAMLContextProviderImpl">
<property name="storageFactory">
<bean class="org.springframework.security.saml.storage.EmptyStorageFactory"/>
</property>
</bean>
或者
// Provider of default SAML Context
@Bean
public SAMLContextProviderImpl contextProvider() {
SAMLContextProviderImpl samlContextProvider = new SAMLContextProviderImpl();
samlContextProvider.setStorageFactory(new EmptyStorageFactory());
return samlContextProvider;
}
系统在解密或加密字段期间失败,例如“Failed to decrypt EncryptedData”
确保在您的 JDK 中正确安装了Unlimited Strength Jurisdiction Policy Files 。
我的系统在验证证书时失败,出现类似于“PKIX path building failed”的错误
这通常是由证书配置错误引起的。您的元数据或 keyStore 不包含正确的叶证书或 CA 证书,或者您的证书无效。您可以通过使用标志-Djavax.net.debug=all启动应用程序来获取更多信息。