SAML认证demo示例
一、工具使用示例
因为在SAML中, sp 与 idp 之间交互数据都是通过浏览器实现的,借助SAML工具可以较为方便的生成、查看参数。 SAML工具, 包含Base64编码、Deflate+Base64、url编码、签名、加密解密等认证过程中可能会使用到的工具。
1.SAMLRequest编码
首先生成自己的SAMLRequest参数,根据请求方式选择Base64(post)或Deflate+Base64(get)进行编码。
(1) get请求
Deflate+Base64生成编码后,需要再进行url编码,最后通过拼接url访问。示例:
http://sso.xxx.edu.cn/idp/profile/SAML2/Redirect/SSO?SAMLRequest=fZFfa8IwFMXfBb9DyHvb%2FGklBqt0G2OCg6F1D3uLMZ0Fm3S5qezjL%2BgcwsC3y73nwDm%2FO1t8d0d0Mh5aZ0tMU4KRsdrtW%2FtZ4m39nAi8mI9HM1DdsZfVEA52bb4GAwFFpwV5PpR48FY6BS1IqzoDMmi5qV5XkqVE9t4Fp90R31juOxSA8SFGwqi6jo%2FOwtAZvzH%2B1GqzXa9KPB4dQuhlltEpS%2BlEpEWe5kIKIlgGfaaisVE6YISWTyVWNNcFMVwoUuwbzkjDG0F2VHCV7wpN91xMCzMxHKMlwGCWFoKyocSMMJpQljBR00IyIbn4wOjtt9ZDay%2B47jXaXUQgX%2Br6Lan%2Bgr1f0UcRjqARQmfW8pzAz0MkPctuN%2FEZ2f9vzH8A
(2) post请求
生成base64的编码后,放在请求参数中通过XHTML表单请求。示例:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<title>POST data</title>
<script src="/simplesaml/resources/post.js"></script>
<link type="text/css" rel="stylesheet" href="/simplesaml/resources/post.css"/>
</head>
<body onload="document.forms[0].submit()">
<noscript>
<p><strong>Note:</strong>
Since your browser does not support JavaScript,
you must press the button below once to proceed.</p>
</noscript>
<form method="post"
action="http://localhost/idp/profile/SAML2/POST/SSO">
<input type="hidden" name="SAMLRequest"
value="PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHNhbWxwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBBc3NlcnRpb25Db25zdW1lclNlcnZpY2VVUkw9Imh0dHBzOi8vc3B0ZXN0LmlhbXNob3djYXNlLmNvbS9hY3MiIERlc3RpbmF0aW9uPSJodHRwczovL2lkMDgucmdoYWxsLmNvbS5jbi9pZHAvcHJvZmlsZS9TQU1MMi9QT1NUL1NTTyIgSUQ9ImExNGM1MGUzOGEwNWRmMzIwZjNmODBiMTgzYTRiNWMxZDM4OTVlNmUzIiBJc3N1ZUluc3RhbnQ9IjIwMjEtMTItMjhUMTU6Mjg6MzhaIiBQcm90b2NvbEJpbmRpbmc9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpiaW5kaW5nczpIVFRQLVBPU1QiIFZlcnNpb249IjIuMCI+CiAgIDxzYW1sOklzc3Vlcj5JQU1TaG93Y2FzZTwvc2FtbDpJc3N1ZXI+Cjwvc2FtbHA6QXV0aG5SZXF1ZXN0Pg=="/>
<noscript>
<button type="submit" class="btn">Submit</button>
</noscript>
</form>
</body>
</html>
2.SAMLResponse解码
返回的SAMLResponse是Base64编码后的结果,可通过工具解码查看。
3.签名
如果想要SAMLRequest带签名,使用如下工具,之后步骤同上。
4.解密
SAML对接配置勾选加密后,返回的信息需要对应的私钥解密才能查看。解密工具使用如下:
二、java版实现
因为其他模式都可以通过工具简化操作,所以没有具体的实现,该部分为 HTTP-Artifact 示例。
1.回调接口
@GetMapping("/sp/artifact")
public String test(final HttpServletRequest request, final HttpServletResponse response) {
Artifact artifact = buildArtifactFromRequest(request);
log.info("create artifact: [{}]", artifact.getArtifact());
ArtifactResolve artifactResolve = buildArtifactResolve(artifact);
log.info("create artifactResolve");
ArtifactResponse artifactResponse = sendAndReceiveArtifactResolve(artifactResolve, response);
log.info("ArtifactResponse received");
log.info("ArtifactResponse: ");
OpenSAMLUtils.logSAMLObject(artifactResponse);
return artifact.getArtifact();
}
2.生成artifact
/**
* SAML消息中有敏感信息
*/
private Artifact buildArtifactFromRequest(final HttpServletRequest req) {
Artifact artifact = OpenSAMLUtils.buildSAMLObject(Artifact.class);
artifact.setArtifact(req.getParameter("SAMLart"));
return artifact;
}
3.生成ArtifactResolve
private ArtifactResolve buildArtifactResolve(final Artifact artifact) {
ArtifactResolve artifactResolve = OpenSAMLUtils.buildSAMLObject(ArtifactResolve.class);
//Issuer:发送方的身份表示,同AuthnRequest中的issuer;
Issuer issuer = OpenSAMLUtils.buildSAMLObject(Issuer.class);
issuer.setValue(SPConstants.SP_ENTITY_ID);
artifactResolve.setIssuer(issuer);
//Time of the Request
artifactResolve.setIssueInstant(new DateTime());
//ID of the request:
artifactResolve.setID(OpenSAMLUtils.generateSecureRandomId());
//destination URL
// artifactResolve.setDestination(IDPConstants.ARTIFACT_RESOLUTION_SERVICE);
artifactResolve.setArtifact(artifact);
return artifactResolve;
}
4.发送 ArtifactResolve 和获取 ArtifactResponse
/**
* 使用SOAP协议发送 ArtifactResolve
*/
private ArtifactResponse sendAndReceiveArtifactResolve(final ArtifactResolve artifactResolve, HttpServletResponse servletResponse) {
try {
MessageContext<ArtifactResolve> contextout = new MessageContext<ArtifactResolve>();
contextout.setMessage(artifactResolve);
//加入数据签名以增强安全性
SignatureSigningParameters signatureSigningParameters = new SignatureSigningParameters();
signatureSigningParameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
signatureSigningParameters.setSigningCredential(no.steras.opensamlbook.sp.SPCredentials.getCredential());
signatureSigningParameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
SecurityParametersContext securityParametersContext = contextout.getSubcontext(SecurityParametersContext.class, true);
if (securityParametersContext != null) {
securityParametersContext.setSignatureSigningParameters(signatureSigningParameters);
}
log.info("artifactResolve signature signing");
//创建InOutOperationContext来处理输入输出的信息
InOutOperationContext<ArtifactResponse, ArtifactResolve> context = new ProfileRequestContext<ArtifactResponse, ArtifactResolve>();
context.setOutboundMessageContext(contextout);
//为了能发送SOAP消息,还需要设置SOAP Client。
// 这个Client将会调用消息的处理器,编码器以及解码等来传送消息
AbstractPipelineHttpSOAPClient<SAMLObject, SAMLObject> soapClient = new AbstractPipelineHttpSOAPClient<SAMLObject, SAMLObject>() {
@Nonnull
protected HttpClientMessagePipeline newPipeline() throws SOAPException {
//创建输入输出用的编码器和解码器
HttpClientRequestSOAP11Encoder encoder = new HttpClientRequestSOAP11Encoder();
HttpClientResponseSOAP11Decoder decoder = new HttpClientResponseSOAP11Decoder();
//创建管道
BasicHttpClientMessagePipeline pipeline = new BasicHttpClientMessagePipeline(
encoder,
decoder
);
//为输出的内容签名
pipeline.setOutboundPayloadHandler(new SAMLOutboundProtocolMessageSigningHandler());
return pipeline;
}};
// HTTP帮助SOAPClient编码和解码
HttpClientBuilder clientBuilder = new HttpClientBuilder();
soapClient.setHttpClient(clientBuilder.buildClient());
soapClient.send(IDPConstants.ARTIFACT_RESOLUTION_SERVICE, context);
DateTime time = new DateTime();
log.info("time: {}", time);
log.info("final artifactResolve:");
OpenSAMLUtils.logSAMLObject(context.getOutboundMessageContext().getMessage());
log.info("send artifactResolve");
return context.getInboundMessageContext().getMessage();
} catch (SecurityException e) {
throw new RuntimeException(e);
} catch (ComponentInitializationException e) {
throw new RuntimeException(e);
} catch (MessageEncodingException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
5.OpenSAMLUtils
public class OpenSAMLUtils {
private static Logger logger = LoggerFactory.getLogger(OpenSAMLUtils.class);
private static RandomIdentifierGenerationStrategy secureRandomIdGenerator;
static {
secureRandomIdGenerator = new RandomIdentifierGenerationStrategy();
}
public static <T> T buildSAMLObject(final Class<T> clazz) {
T object = null;
try {
XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory();
QName defaultElementName = (QName)clazz.getDeclaredField("DEFAULT_ELEMENT_NAME").get(null);
object = (T)builderFactory.getBuilder(defaultElementName).buildObject(defaultElementName);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("Could not create SAML object");
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException("Could not create SAML object");
}
return object;
}
public static String generateSecureRandomId() {
return secureRandomIdGenerator.generateIdentifier();
}
public static void logSAMLObject(final XMLObject object) {
Element element = null;
if (object instanceof SignableSAMLObject && ((SignableSAMLObject)object).isSigned() && object.getDOM() != null) {
element = object.getDOM();
} else {
try {
Marshaller out = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
out.marshall(object);
element = object.getDOM();
} catch (MarshallingException e) {
logger.error(e.getMessage(), e);
}
}
try {
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StreamResult result = new StreamResult(new StringWriter());
DOMSource source = new DOMSource(element);
transformer.transform(source, result);
String xmlString = result.getWriter().toString();
logger.info(xmlString);
} catch (TransformerConfigurationException e) {
e.printStackTrace();
} catch (TransformerException e) {
e.printStackTrace();
}
}
}
6.证书获取
public class SPCredentials {
private static final String KEY_STORE_PASSWORD = "SPKeystore";
private static final String KEY_STORE_ENTRY_PASSWORD = "SPKeystore";
private static final String KEY_STORE_PATH = "/SPKeystore.jks";
private static final String KEY_ENTRY_ID = "SPKeystore";
private static final Credential credential;
private static FilesystemMetadataProvider spMetaDataProvider;
// KeyStoreCredentialResolver
static {
try {
KeyStore keystore = readKeystoreFromFile(KEY_STORE_PATH, KEY_STORE_PASSWORD);
Map<String, String> passwordMap = new HashMap<String, String>();
passwordMap.put(KEY_ENTRY_ID, KEY_STORE_ENTRY_PASSWORD);
KeyStoreCredentialResolver resolver = new KeyStoreCredentialResolver(keystore, passwordMap);
Criterion criterion = new EntityIdCriterion(KEY_ENTRY_ID);
CriteriaSet criteriaSet = new CriteriaSet();
criteriaSet.add(criterion);
credential = resolver.resolveSingle(criteriaSet);
} catch (ResolverException e) {
throw new RuntimeException("Something went wrong reading credentials", e);
}
}
private static KeyStore readKeystoreFromFile(String pathToKeyStore, String keyStorePassword) {
try {
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream inputStream = SPCredentials.class.getResourceAsStream(pathToKeyStore);
keystore.load(inputStream, keyStorePassword.toCharArray());
inputStream.close();
return keystore;
} catch (Exception e) {
throw new RuntimeException("Something went wrong reading keystore", e);
}
}
public static Credential getCredential() {
return credential;
}
}