最近在工作中和海外一家公司对接单点登录,用到了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]
2.代码示例
下面是groovy代码,java类似
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties(prefix = "sso")
class SSOProperties {
String responseAdp
String audienceAdp
String relayStateAdp
boolean testAdp
String[] apps
}
@Service("adpService")
@Slf4j
@EnableConfigurationProperties([SSOProperties.class])
class AdpService implements SamlService{
@Resource
SSOProperties ssoProperties
@Resource
AccountService 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 builderFactory
static {
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 authDOM
try {
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 keyDescriptors = _SPSSODescriptor.getKeyDescriptors()
KeyDescriptor keyDescriptor = keyDescriptors.get(0)
KeyInfo keyInfo = keyDescriptor.getKeyInfo()
List x509Datas = keyInfo.getX509Datas()
List 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 symmetricCredential
EncryptedAssertion encrypted = null
try {
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 dkey
Assertion assertion = null
try {
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
}
}
页面部分代码
POM.xml部分
net.unicon.cas
cas-client-autoconfig-support
1.5.0-GA
org.springframework.security
spring-security-web
org.springframework.boot
spring-boot-starter-web
org.apache.httpcomponents
fluent-hc
4.3.6
commons-httpclient
commons-httpclient
3.1
org.opensaml
opensaml
2.6.4