最近在工作中和海外一家公司对接单点登录,用到了SAML2.0协议,目前公司的单点登录
还是比较老的CASE3.5版本,不支持SAML2,要支持也要定制优,由于后面肯定是要升级,所
以不在源码上做调整支持,单独建了个SSO应用作为CASE客户端,并包装客户的接口,登录
还是用CASE认证。
由于客户没有完全实现SAML2.0(SP)的功能, IDP由我司CASE提供,我司SSO应用其实是半个SP的功能,提供给页面访问并认证,认证成功后response给客户,客户校验通过后才返回页面,

-
推荐一个很好的SAML工具,想测试一个SAML2的接口很容易,支持多种加密
工具链接[https://www.samltool.com/generic_sso_res.php]Reponse各种示例
签名工具:

2.代码示例
下面是groovy代码,java类似
import org.springframework.boot.context.properties.ConfigurationProperties@ConfigurationProperties(prefix = "sso")
class SSOProperties {String responseAdpString audienceAdpString relayStateAdpboolean testAdpString[] apps
}
@Service("adpService")
@Slf4j
@EnableConfigurationProperties([SSOProperties.class])
class AdpService implements SamlService{@ResourceSSOProperties ssoProperties@ResourceAccountService accountService
def getSamlResponse() {String username = AssertionHolder.getAssertion().getPrincipal().getName()log.info("username: ${username}")String jobNo = accountService.getJobNo(username)def model = [:]//签名消息def xml = signedResponse(jobNo)//返回页面form提交的参数model.put("samlResponse", Base64.encodeBytes(xml.getBytes()))model.put("relayState", ssoProperties.getRelayStateAdp())model.put("redirectUrl", ssoProperties.getResponseAdp())model}
def signedResponse(String userId){String destination = ssoProperties.getResponseAdp()final Response samlResponse = SamlHelper.buildResponse(UUIDFactory.INSTANCE.getUUID(), destination)DateTime notBefore = new DateTime(2018, 10, 19, 1, 0, 0, 0, ISOChronology.getInstanceUTC())DateTime notOnOrAfter = new DateTime(2021, 10, 19, 1, 0, 0, 0, ISOChronology.getInstanceUTC())String audienceURI = ssoProperties.getAudienceAdp()Assertion assertion = SamlHelper.buildAssertion(samlResponse, userId,audienceURI, notBefore, notOnOrAfter)AttributeStatement attributeStatement = SamlHelper.buildAttributeStatement("PersonImmutableID", userId)assertion.getAttributeStatements().add(attributeStatement)SamlHelper.signXMLObject(assertion)samlResponse.getAssertions().add(assertion)def xml = SamlHelper.buildXMLObjectToString(samlResponse)return xml}}
@Slf4j
abstract class SamlHelper {static final XMLObjectBuilderFactory builderFactorystatic {try {DefaultBootstrap.bootstrap()} catch (ConfigurationException e) {log.error(e.getMessage(), e)}Security.addProvider(new BouncyCastleProvider())builderFactory = Configuration.getBuilderFactory()}static String buildXMLObjectToString(XMLObject xmlObject) {Marshaller marshaller = Configuration.getMarshallerFactory().getMarshaller(xmlObject)Element authDOMtry {authDOM = marshaller.marshall(xmlObject)StringWriter rspWrt = new StringWriter()XMLHelper.writeNode(authDOM, rspWrt)String messageXML = rspWrt.toString()return messageXML} catch (MarshallingException e) {throw new RuntimeException(e)}}static XMLObject buildStringToXMLObject(String xmlObjectString) {try {BasicParserPool parser = new BasicParserPool()parser.setNamespaceAware(true)String xmlString = decode64SAMLResponse(xmlObjectString)Document doc = (Document) parser.parse(new ByteArrayInputStream(xmlString.getBytes()))Element samlElement = (Element) doc.getDocumentElement()Unmarshaller unmarshaller = Configuration.getUnmarshallerFactory().getUnmarshaller(samlElement)return unmarshaller.unmarshall(samlElement)} catch (XMLParserException e) {throw new RuntimeException(e)} catch (UnmarshallingException e) {throw new RuntimeException(e)}}static AuthnRequest buildAuthnRequest(String ticket, String setAssertionConsumerServiceURL) {NameID nameid = (NameID) buildXMLObject(NameID.DEFAULT_ELEMENT_NAME)nameid.setFormat(NameID.UNSPECIFIED)nameid.setValue(ticket)Subject subject = (Subject) buildXMLObject(Subject.DEFAULT_ELEMENT_NAME)subject.setNameID(nameid)Audience audience = (Audience) buildXMLObject(Audience.DEFAULT_ELEMENT_NAME)audience.setAudienceURI(Constants.LOCALDOMAIN)AudienceRestriction ar = (AudienceRestriction) buildXMLObject(AudienceRestriction.DEFAULT_ELEMENT_NAME)ar.getAudiences().add(audience)Conditions conditions = (Conditions) buildXMLObject(Conditions.DEFAULT_ELEMENT_NAME)conditions.getAudienceRestrictions().add(ar)AuthnContextClassRef classRef = (AuthnContextClassRef) buildXMLObject(AuthnContextClassRef.DEFAULT_ELEMENT_NAME)classRef.setAuthnContextClassRef("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport")RequestedAuthnContext rac = (RequestedAuthnContext) buildXMLObject(RequestedAuthnContext.DEFAULT_ELEMENT_NAME)rac.getAuthnContextClassRefs().add(classRef)AuthnRequest request = (AuthnRequest) buildXMLObject(AuthnRequest.DEFAULT_ELEMENT_NAME)request.setSubject(subject)request.setConditions(conditions)request.setRequestedAuthnContext(rac)request.setForceAuthn(false)request.setAssertionConsumerServiceURL(setAssertionConsumerServiceURL)request.setAttributeConsumingServiceIndex(0)request.setProviderName("IDP Provider")request.setID("_" + UUIDFactory.INSTANCE.getUUID())request.setVersion(SAMLVersion.VERSION_20)request.setIssueInstant(new DateTime(2005, 1, 31, 12, 0, 0, 0, ISOChronology.getInstanceUTC()))request.setDestination(Constants.LOCALDOMAIN)request.setConsent("urn:oasis:names:tc:SAML:2.0:consent:obtained")Issuer rIssuer = (Issuer) buildXMLObject(Issuer.DEFAULT_ELEMENT_NAME)rIssuer.setFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:entity")rIssuer.setValue(Constants.LOCALDOMAIN)request.setIssuer(rIssuer)return request}static Response buildResponse(String requestId, String destination) {Response response = (Response) buildXMLObject(Response.DEFAULT_ELEMENT_NAME)Namespace namespace = new Namespace("urn:oasis:names:tc:SAML:2.0:assertion", "saml2")response.addNamespace(namespace)response.setID(UUIDFactory.INSTANCE.getUUID())
// response.setInResponseTo(requestId)response.setDestination(destination)Calendar now = DateUtil.getUTCCalendar()response.setIssueInstant(new DateTime(now.get(Calendar.YEAR), (now.get(Calendar.MONTH) + 1), now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE), now.get(Calendar.SECOND), 0, ISOChronology.getInstanceUTC()))Issuer rIssuer = (Issuer) buildXMLObject(Issuer.DEFAULT_ELEMENT_NAME)// rIssuer.setFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:entity")rIssuer.setValue(Constants.ISSUER)rIssuer.removeNamespace(new Namespace("urn:oasis:names:tc:SAML:2.0:assertion", "saml2"))response.setIssuer(rIssuer)Status status = (Status) buildXMLObject(Status.DEFAULT_ELEMENT_NAME)StatusCode statusCode = (StatusCode) buildXMLObject(StatusCode.DEFAULT_ELEMENT_NAME)statusCode.setValue("urn:oasis:names:tc:SAML:2.0:status:Success")response.setStatus(status)status.setStatusCode(statusCode)return response}static Assertion buildAssertion(Response response, String nameIdValue, String audienceURI, DateTime notBefore, DateTime notOnOrAfter) {Calendar now = DateUtil.getUTCCalendar()Assertion assertion = (Assertion) buildXMLObject(Assertion.DEFAULT_ELEMENT_NAME)assertion.setID(UUIDFactory.INSTANCE.getUUID())assertion.setIssueInstant(new DateTime(now.get(Calendar.YEAR), (now.get(Calendar.MONTH) + 1), now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE), now.get(Calendar.SECOND), ISOChronology.getInstanceUTC()))
// assertion.removeNamespace(new Namespace("urn:oasis:names:tc:SAML:2.0:assertion", "saml2"))assertion.addNamespace(new Namespace("http://www.w3.org/2001/XMLSchema-instance", "xsi"))assertion.addNamespace(new Namespace("http://www.w3.org/2001/XMLSchema", "xs"))AuthnStatement authnStatement = (AuthnStatement) buildXMLObject(AuthnStatement.DEFAULT_ELEMENT_NAME)authnStatement.setAuthnInstant(new DateTime(now.get(Calendar.YEAR), (now.get(Calendar.MONTH) + 1), now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE), now.get(Calendar.SECOND), ISOChronology.getInstanceUTC()))authnStatement.setSessionIndex(UUIDFactory.INSTANCE.getUUID())now.add(Calendar.MINUTE, 2)DateTime sessionNotOnOrAfter = new DateTime(now.get(Calendar.YEAR), (now.get(Calendar.MONTH) + 1), now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE), now.get(Calendar.SECOND), ISOChronology.getInstanceUTC())authnStatement.setSessionNotOnOrAfter(sessionNotOnOrAfter)AuthnContext authnContext = (AuthnContext) buildXMLObject(AuthnContext.DEFAULT_ELEMENT_NAME)AuthnContextClassRef classRef = (AuthnContextClassRef) buildXMLObject(AuthnContextClassRef.DEFAULT_ELEMENT_NAME)classRef.setAuthnContextClassRef("urn:oasis:names:tc:SAML:2.0:ac:classes:Password")authnContext.setAuthnContextClassRef(classRef)authnStatement.setAuthnContext(authnContext)assertion.getAuthnStatements().add(authnStatement)Issuer aIssuer = (Issuer) buildXMLObject(Issuer.DEFAULT_ELEMENT_NAME)
// aIssuer.setFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:entity")aIssuer.setValue(Constants.ISSUER)assertion.setIssuer(aIssuer)Subject subject = (Subject) buildXMLObject(Subject.DEFAULT_ELEMENT_NAME)NameID nameID = (NameID) buildXMLObject(NameID.DEFAULT_ELEMENT_NAME)nameID.setFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified")nameID.setSPNameQualifier(audienceURI)nameID.setValue(nameIdValue)SubjectConfirmation subjectConfirmation = (SubjectConfirmation) buildXMLObject(SubjectConfirmation.DEFAULT_ELEMENT_NAME)subjectConfirmation.setMethod("urn:oasis:names:tc:SAML:2.0:cm:bearer")subject.setNameID(nameID)SubjectConfirmationData subjectConfirmationData = (SubjectConfirmationData) buildXMLObject(SubjectConfirmationData.DEFAULT_ELEMENT_NAME)subjectConfirmationData.setNotOnOrAfter(sessionNotOnOrAfter)subjectConfirmationData.setRecipient(response.getDestination())
// subjectConfirmationData.setInResponseTo(response.getInResponseTo())subjectConfirmation.setSubjectConfirmationData(subjectConfirmationData)subject.getSubjectConfirmations().add(subjectConfirmation)assertion.setSubject(subject)Conditions conditions = (Conditions) buildXMLObject(Conditions.DEFAULT_ELEMENT_NAME)conditions.setNotBefore(notBefore)conditions.setNotOnOrAfter(notOnOrAfter)AudienceRestriction audienceRestriction = (AudienceRestriction) buildXMLObject(AudienceRestriction.DEFAULT_ELEMENT_NAME)Audience audience = (Audience) buildXMLObject(Audience.DEFAULT_ELEMENT_NAME)audience.setAudienceURI(audienceURI)audienceRestriction.getAudiences().add(audience)conditions.getAudienceRestrictions().add(audienceRestriction)assertion.setConditions(conditions)assertion}static void signXMLObject(SignableXMLObject signableXMLObject) {SignatureBuilder signatureBuilder = (SignatureBuilder) builderFactory.getBuilder(Signature.DEFAULT_ELEMENT_NAME)BasicCredential basicCredential = new BasicCredential()basicCredential.setPrivateKey(CertificateHelper.getRSAPrivateKey())Signature signature = signatureBuilder.buildObject()signature.setCanonicalizationAlgorithm(Constants.CANON_ALGORITHM)signature.setSignatureAlgorithm(Constants.SIGNATURE_METHOD)signature.setSigningCredential(basicCredential)signableXMLObject.setSignature(signature)MarshallerFactory marshallerFactory = Configuration.getMarshallerFactory()Marshaller marshaller = marshallerFactory.getMarshaller(signableXMLObject)try {marshaller.marshall(signableXMLObject)Signer.signObject(signature)} catch (MarshallingException e) {log.error(e.getMessage(), e)throw new RuntimeException("XML Marshalling failure")} catch (SignatureException e) {log.error(e.getMessage(), e)throw new RuntimeException("Signature failure")}}static Attribute buildStringAttribute(String name, String value) {Attribute attribute = (Attribute) buildXMLObject(Attribute.DEFAULT_ELEMENT_NAME)attribute.setName(name)attribute.setNameFormat("urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified")XMLObjectBuilder<?> stringBuilder = builderFactory.getBuilder(XSString.TYPE_NAME)XSString ldapAttribValue = (XSString) stringBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME)ldapAttribValue.removeNamespace(new Namespace("http://www.w3.org/2001/XMLSchema", "xs"))ldapAttribValue.setValue(value)attribute.getAttributeValues().add(ldapAttribValue)return attribute}static AttributeStatement buildAttributeStatement() {return (AttributeStatement) buildXMLObject(AttributeStatement.DEFAULT_ELEMENT_NAME)}static AttributeStatement buildAttributeStatement(String name, String value) {AttributeStatement attributeStatement = buildAttributeStatement()Attribute attribute = buildStringAttribute(name, value)attributeStatement.getAttributes().add(attribute)attributeStatement}static String buildArtifactResolve(Artifact artifact) {ArtifactResolve artifactResolve = (ArtifactResolve) buildXMLObject(ArtifactResolve.DEFAULT_ELEMENT_NAME)artifactResolve.setArtifact(artifact)return buildXMLObjectToString(artifactResolve)}static SSODescriptor buildSSODescriptor(String xmlFilePath, Class<?> descriptorType) {EntityDescriptor entityDescriptor = (EntityDescriptor) unmarshallElementWithXMLFile(xmlFilePath)if (descriptorType.getClass().getName().equals(IDPSSODescriptor.class.getName())) {return entityDescriptor.getIDPSSODescriptor("urn:oasis:names:tc:SAML:2.0:protocol")}return entityDescriptor.getSPSSODescriptor("urn:oasis:names:tc:SAML:2.0:protocol")}static X509Certificate getX509Certificate(String xmlFilePath) {SSODescriptor _SPSSODescriptor = buildSSODescriptor(xmlFilePath, SPSSODescriptor.class)List<KeyDescriptor> keyDescriptors = _SPSSODescriptor.getKeyDescriptors()KeyDescriptor keyDescriptor = keyDescriptors.get(0)KeyInfo keyInfo = keyDescriptor.getKeyInfo()List<X509Data> x509Datas = keyInfo.getX509Datas()List<X509Certificate> x509Certificates = x509Datas.get(0).getX509Certificates()X509Certificate x509Certificate = x509Certificates.get(0)return x509Certificate}static String decode64SAMLResponse(String samlResponse) {byte[] decodedBytes = Base64.decode(samlResponse)return new String(decodedBytes)}static def buildXMLObject(QName objectQName) {XMLObjectBuilder<?> builder = Configuration.getBuilderFactory().getBuilder(objectQName)return builder.buildObject(objectQName.getNamespaceURI(), objectQName.getLocalPart(), objectQName.getPrefix())}static XMLObject unmarshallElementWithXMLFile(String elementFile) {try {BasicParserPool parser = new BasicParserPool()parser.setNamespaceAware(true)Document doc = parser.parse(SamlHelper.class.getResourceAsStream(elementFile))Element samlElement = doc.getDocumentElement()Unmarshaller unmarshaller = Configuration.getUnmarshallerFactory().getUnmarshaller(samlElement)return unmarshaller.unmarshall(samlElement)} catch (XMLParserException e) {throw new RuntimeException(e)} catch (UnmarshallingException e) {throw new RuntimeException(e)}}/*** 加密断言* @param assertion* @param receiverCredential* @return*/static EncryptedAssertion encrypt(Assertion assertion, X509Credential receiverCredential) {Credential symmetricCredentialEncryptedAssertion encrypted = nulltry {symmetricCredential = SecurityHelper.getSimpleCredential(SecurityHelper.generateSymmetricKey(EncryptionConstants.ALGO_ID_BLOCKCIPHER_AES128))EncryptionParameters encParams = new EncryptionParameters()encParams.setAlgorithm(EncryptionConstants.ALGO_ID_BLOCKCIPHER_AES128)encParams.setEncryptionCredential(symmetricCredential)KeyEncryptionParameters kek = new KeyEncryptionParameters()kek.setAlgorithm(EncryptionConstants.ALGO_ID_KEYTRANSPORT_RSA15)kek.setEncryptionCredential(receiverCredential)Encrypter encrypter = new Encrypter(encParams, kek)encrypter.setKeyPlacement(KeyPlacement.INLINE)encrypted = encrypter.encrypt(assertion)} catch (NoSuchAlgorithmException | KeyException e) {log.error(e.getMessage(), e)} catch (EncryptionException e) {log.error(e.getMessage(), e)}return encrypted}/*** 解密断言* @param enc* @param credential* @param federationMetadata* @return*/static Assertion decrypt(EncryptedAssertion enc, Credential credential, String federationMetadata) {KeyInfoCredentialResolver keyResolver = new StaticKeyInfoCredentialResolver(credential)EncryptedKey key = enc.getEncryptedData().getKeyInfo().getEncryptedKeys().get(0)Decrypter decrypter = new Decrypter(null, keyResolver, new InlineEncryptedKeyResolver())decrypter.setRootInNewDocument(true)SecretKey dkeyAssertion assertion = nulltry {dkey = (SecretKey) decrypter.decryptKey(key, enc.getEncryptedData().getEncryptionMethod().getAlgorithm())Credential shared = SecurityHelper.getSimpleCredential(dkey)decrypter = new Decrypter(new StaticKeyInfoCredentialResolver(shared), null, null)decrypter.setRootInNewDocument(true)assertion = decrypter.decrypt(enc)} catch (DecryptionException e) {log.error(e.getMessage(), e)}return assertion}/*** 签名断言* @param enc* @param credential* @param federationMetadata* @return*/static Signature signature() {SignatureBuilder signatureBuilder = (SignatureBuilder) builderFactory.getBuilder(Signature.DEFAULT_ELEMENT_NAME)BasicCredential basicCredential = new BasicCredential()Signature signature = signatureBuilder.buildObject()basicCredential.setPrivateKey(CertificateHelper.getRSAPrivateKey())signature.setCanonicalizationAlgorithm(Constants.CANON_ALGORITHM)signature.setSignatureAlgorithm(Constants.SIGNATURE_METHOD)return signature}/*** 验签断言* @param enc* @param credential* @param federationMetadata* @return*/static boolean validate(String base64Response) {SignableXMLObject signableXMLObject = (SignableXMLObject) buildStringToXMLObject(base64Response)return validate(signableXMLObject)}static boolean validate(SignableXMLObject signableXMLObject) {BasicCredential basicCredential = new BasicCredential()basicCredential.setPublicKey(CertificateHelper.getRSAPublicKey())SignatureValidator signatureValidator = new SignatureValidator(basicCredential)Signature signature = signableXMLObject.getSignature()try {signatureValidator.validate(signature)return true} catch (ValidationException e) {log.warn("验证签名错误" + e.getMessage())return false}}static Artifact buildArtifact() {String artifactId = UUIDFactory.INSTANCE.getUUID()Artifact artifact = (Artifact) buildXMLObject(Artifact.DEFAULT_ELEMENT_NAME)artifact.setArtifact(artifactId)return artifact}static ArtifactResolve buildArtifactResolve() {String artifactResolveId = UUIDFactory.INSTANCE.getUUID()ArtifactResolve artifactResolve = (ArtifactResolve) buildXMLObject(ArtifactResolve.DEFAULT_ELEMENT_NAME)artifactResolve.setID(artifactResolveId)Issuer aIssuer = (Issuer) buildXMLObject(Issuer.DEFAULT_ELEMENT_NAME)aIssuer.setFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:entity")aIssuer.setValue(Constants.LOCALDOMAIN)artifactResolve.setIssuer(aIssuer)artifactResolve.setVersion(SAMLVersion.VERSION_20)
// artifactResolve.setDestination(Constants.SP_ARTIFACT_RESOLUTION_SERVICE)artifactResolve.setIssueInstant(new DateTime(2005, 1, 31, 12, 0, 0, 0, ISOChronology.getInstanceUTC()))return artifactResolve}static ArtifactResponse buildArtifactResponse() {String artifactResponseId = UUIDFactory.INSTANCE.getUUID()ArtifactResponse artifactResponse = (ArtifactResponse) buildXMLObject(ArtifactResponse.DEFAULT_ELEMENT_NAME)artifactResponse.setID(artifactResponseId)artifactResponse.setVersion(SAMLVersion.VERSION_20)artifactResponse.setIssueInstant(new DateTime(2005, 1, 31, 12, 0, 0, 0, ISOChronology.getInstanceUTC()))return artifactResponse}static AttributeQuery buildAttributeQuery() {AttributeQuery attributeQuery = (AttributeQuery) buildXMLObject(AttributeQuery.DEFAULT_ELEMENT_NAME)return attributeQuery}static Status getStatusCode(boolean success) {Status status = (Status) buildXMLObject(Status.DEFAULT_ELEMENT_NAME)StatusCode statusCode = (StatusCode) buildXMLObject(StatusCode.DEFAULT_ELEMENT_NAME)statusCode.setValue(success ? StatusCode.SUCCESS_URI : StatusCode.AUTHN_FAILED_URI)status.setStatusCode(statusCode)return status}}
页面部分代码
<form id="myForm" method="POST" th:action="${redirectUrl}"><input type="hidden" name="SAMLResponse" th:value="${samlResponse}" /><input type="hidden" name="RelayState" th:value="${relayState}" /><input type="submit" hidden value="Submit" /></form>
</body>
<script type="text/javascript" th:inline="javascript">window. function(){document.getElementById('myForm').submit();}</script>
POM.xml部分
<!-- case --><dependency><groupId>net.unicon.cas</groupId><artifactId>cas-client-autoconfig-support</artifactId><version>1.5.0-GA</version></dependency><!-- case --><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-web</artifactId></dependency><!-- spring boot --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>fluent-hc</artifactId><version>4.3.6</version></dependency><dependency><groupId>commons-httpclient</groupId><artifactId>commons-httpclient</artifactId><version>3.1</version></dependency><dependency><groupId>org.opensaml</groupId><artifactId>opensaml</artifactId><version>2.6.4</version></dependency>
















