cas4.0集成 saml2协议如何针对断言进行签名?
MyGoogleAccountsService.java
```java
package com.gilight.cas.authorize.saml2;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.jasig.cas.authentication.principal.AbstractWebApplicationService;
import org.jasig.cas.authentication.principal.Response;
import org.jasig.cas.support.saml.util.SamlUtils;
import org.jdom.Document;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
public class MyGoogleAccountsService extends AbstractWebApplicationService {
private static final long serialVersionUID = 6678711809842282833L;
private static SecureRandom RANDOM_GENERATOR = new SecureRandom();
private static final char[] CHAR_MAPPINGS = {
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p'};
private static final String CONST_PARAM_SERVICE = "SAMLRequest";
private static final String CONST_RELAY_STATE = "RelayState";
private static final String TEMPLATE_SAML_RESPONSE =
"<samlp:Response ID=\"<RESPONSE_ID>\" IssueInstant=\"<ISSUE_INSTANT>\" Version=\"2.0\""
+ " xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\""
+ " xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\""
+ " Destination=\"https://login.microsoftonline.com/login.srf\""
+ " xmlns:xenc=\"http://www.w3.org/2001/04/xmlenc#\">"
+ "<samlp:Status>"
+ "<samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\" />"
+ "</samlp:Status>"
+ "<samlp:Assertion ID=\"<ASSERTION_ID>\""
+ " IssueInstant=\"<ISSUE_INSTANT>\" Version=\"2.0\""
+ " xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\""
// + " xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\""
+ ">"
+ "<Issuer>https://365.sias.edu.cn/cas</Issuer>"
+ "<Subject>"
// + "<NameID Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress\">"
+ "<NameID Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\">"
+ "<USERNAME_STRING>"
+ "</NameID>"
+ "<SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\">"
+ "<SubjectConfirmationData Recipient=\"<ACS_URL>\" NotOnOrAfter=\"<NOT_ON_OR_AFTER>\" InResponseTo=\"<REQUEST_ID>\" />"
+ "</SubjectConfirmation>"
+ "</Subject>"
+ "<Conditions NotBefore=\"<ISSUE_INSTANT>\""
+ " NotOnOrAfter=\"<NOT_ON_OR_AFTER>\">"
+ "<AudienceRestriction>"
+ "<Audience><ACS_URL></Audience>"
+ "</AudienceRestriction>"
+ "</Conditions>"
// 人员属性
+ "<AttributeStatement>"
+ "<Attribute Name=\"IDPEmail\"><AttributeValue><USER_ATTRIBUTEVALUE_EMAIL></AttributeValue></Attribute>"
+ "</AttributeStatement>"
+ "<AuthnStatement AuthnInstant=\"<AUTHN_INSTANT>\">"
+ "<AuthnContext>"
+ "<AuthnContextClassRef>"
+ "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
+ "</AuthnContextClassRef>"
+ "</AuthnContext>"
+ "</AuthnStatement>"
+ "</samlp:Assertion></samlp:Response>";
private final String relayState;
private final PublicKey publicKey;
private final PrivateKey privateKey;
private final String requestId;
private final String alternateUserName;
private static X509Certificate x509;
protected MyGoogleAccountsService(final String id, final String relayState, final String requestId,
final PrivateKey privateKey, final PublicKey publicKey, final String alternateUserName) {
this(id, id, null, relayState, requestId, privateKey, publicKey, alternateUserName);
}
protected MyGoogleAccountsService(final String id, final String originalUrl,
final String artifactId, final String relayState, final String requestId,
final PrivateKey privateKey, final PublicKey publicKey, final String alternateUserName) {
super(id, originalUrl, artifactId);
this.relayState = relayState;
this.privateKey = privateKey;
this.publicKey = publicKey;
this.requestId = requestId;
this.alternateUserName = alternateUserName;
}
public static MyGoogleAccountsService createServiceFrom(
final HttpServletRequest request, final PrivateKey privateKey,
final PublicKey publicKey, final String alternateUserName, X509Certificate x509Certificate) {
x509 = x509Certificate;
final String relayState = request.getParameter(CONST_RELAY_STATE);
final String xmlRequest = decodeAuthnRequestXML(request.getParameter(CONST_PARAM_SERVICE));
if (!StringUtils.hasText(xmlRequest)) {
return null;
}
final Document document = SamlUtils.constructDocumentFromXmlString(xmlRequest);
if (document == null) {
return null;
}
// 获取sp url
String assertionConsumerServiceUrl = document.getRootElement().getAttributeValue("AssertionConsumerServiceURL");
if(null == assertionConsumerServiceUrl){
assertionConsumerServiceUrl = request.getHeader("Referer");
}
if(null == assertionConsumerServiceUrl){
assertionConsumerServiceUrl = request.getHeader("Origin");
}
String requestId = document.getRootElement().getAttributeValue("ID");
if(null == requestId ){
requestId = "1";
}
return new MyGoogleAccountsService(assertionConsumerServiceUrl,
relayState, requestId, privateKey, publicKey, alternateUserName);
}
@Override
public Response getResponse(final String ticketId) {
final Map<String, String> parameters = new HashMap<String, String>();
String uuid = createID();
final String samlResponse = constructSamlResponse(uuid);
String signedResponse = MySamlUtils.signSamlResponse(samlResponse, this.privateKey, this.publicKey, x509);
parameters.put("SAMLResponse", new String(Base64.encodeBase64(signedResponse.getBytes())));
parameters.put("RelayState", this.relayState);
return Response.getPostResponse(getOriginalUrl(), parameters);
}
/**
* Return true if the service is already logged out.
*
* @return true if the service is already logged out.
*/
@Override
public boolean isLoggedOutAlready() {
return true;
}
private String constructSamlResponse(String uuid) {
String samlResponse = TEMPLATE_SAML_RESPONSE;
final Calendar c = Calendar.getInstance();
c.setTime(new Date());
c.add(Calendar.YEAR, 1);
String userId;
if (this.alternateUserName == null) {
userId = getPrincipal().getId();
} else {
final String attributeValue = (String) getPrincipal().getAttributes().get(this.alternateUserName);
if (attributeValue == null) {
userId = getPrincipal().getId();
} else {
userId = attributeValue;
}
}
samlResponse = samlResponse.replace("<USERNAME_STRING>", userId );
samlResponse = samlResponse.replace("<RESPONSE_ID>", createID());
// samlResponse = samlResponse.replace("<ISSUE_INSTANT>", SamlDateUtils.getCurrentDateAndTime());
// samlResponse = samlResponse.replace("<AUTHN_INSTANT>", SamlDateUtils.getCurrentDateAndTime());
// samlResponse = samlResponse.replaceAll("<NOT_ON_OR_AFTER>", SamlDateUtils.getFormattedDateAndTime(c.getTime()));
samlResponse = samlResponse.replace("<ISSUE_INSTANT>", getCurrentDateAndTime());
samlResponse = samlResponse.replace("<AUTHN_INSTANT>", getCurrentDateAndTime());
samlResponse = samlResponse.replaceAll("<NOT_ON_OR_AFTER>", getFormattedDateAndTime(c.getTime()));
samlResponse = samlResponse.replace("<ASSERTION_ID>", uuid);
samlResponse = samlResponse.replaceAll("<ACS_URL>", getId());
samlResponse = samlResponse.replace("<REQUEST_ID>", this.requestId);
// 人员属性
Map<String, Object> attributeMap = getPrincipal().getAttributes();
// 邮箱
if(null != attributeMap && attributeMap.size() > 0 ){
String mailbox = null != attributeMap.get("mailbox") ? (String)attributeMap.get("mailbox") : null;
if(null != mailbox){
samlResponse = samlResponse.replace("<USER_ATTRIBUTEVALUE_EMAIL>", mailbox);
}
}
return samlResponse;
}
private static String createID() {
final byte[] bytes = new byte[20]; // 160 bits
RANDOM_GENERATOR.nextBytes(bytes);
final char[] chars = new char[40];
for (int i = 0; i < bytes.length; i++) {
int left = bytes[i] >> 4 & 0x0f;
int right = bytes[i] & 0x0f;
chars[i * 2] = CHAR_MAPPINGS[left];
chars[i * 2 + 1] = CHAR_MAPPINGS[right];
}
return String.valueOf(chars);
}
private static String decodeAuthnRequestXML(
final String encodedRequestXmlString) {
if (encodedRequestXmlString == null) {
return null;
}
// final byte[] decodedBytes = base64Decode(encodedRequestXmlString);
final byte[] decodedBytes = compressString(encodedRequestXmlString);
if (decodedBytes == null) {
return null;
}
final String inflated = inflate(decodedBytes);
if (inflated != null) {
return inflated;
}
return zlibDeflate(decodedBytes);
}
private static String zlibDeflate(final byte[] bytes) {
final ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final InflaterInputStream iis = new InflaterInputStream(bais);
final byte[] buf = new byte[1024];
try {
int count = iis.read(buf);
while (count != -1) {
baos.write(buf, 0, count);
count = iis.read(buf);
}
return new String(baos.toByteArray());
} catch (final Exception e) {
return null;
} finally {
IOUtils.closeQuietly(iis);
}
}
private static byte[] base64Decode(final String xml) {
try {
final byte[] xmlBytes = xml.getBytes("UTF-8");
return Base64.decodeBase64(xmlBytes);
} catch (final Exception e) {
return null;
}
}
private static String inflate(final byte[] bytes) {
final Inflater inflater = new Inflater(true);
final byte[] xmlMessageBytes = new byte[10000];
final byte[] extendedBytes = new byte[bytes.length + 1];
System.arraycopy(bytes, 0, extendedBytes, 0, bytes.length);
extendedBytes[bytes.length] = 0;
inflater.setInput(extendedBytes);
try {
final int resultLength = inflater.inflate(xmlMessageBytes);
inflater.end();
if (!inflater.finished()) {
throw new RuntimeException("buffer not large enough.");
}
inflater.end();
return new String(xmlMessageBytes, 0, resultLength, "UTF-8");
} catch (final DataFormatException e) {
return null;
} catch (final UnsupportedEncodingException e) {
throw new RuntimeException("Cannot find encoding: UTF-8", e);
}
}
public static byte[] compressString(String input) {
try {
if(isBase64String(input)){
input = new String(Base64.decodeBase64(input));
}
byte[] inputData = input.getBytes("UTF-8");
Deflater deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true);
deflater.setInput(inputData);
deflater.finish();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(inputData.length);
byte[] buffer = new byte[2048];
while (!deflater.finished()) {
int count = deflater.deflate(buffer);
outputStream.write(buffer, 0, count);
}
deflater.end();
return outputStream.toByteArray();
} catch (Exception e) {
// TODO: handle exception
}
return null;
}
/**
* 判断是否为base64
* @param str
* @return
* @author gy
*/
public static boolean isBase64String(String str) {
try {
byte[] decodedBytes = Base64.decodeBase64(str);
String encodedString = Base64.encodeBase64String(decodedBytes);
return str.equals(encodedString);
} catch (IllegalArgumentException e) {
return false;
}
}
public static String getCurrentDateAndTime() {
return getFormattedDateAndTime(new Date());
}
public static String getFormattedDateAndTime(final Date date) {
final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return dateFormat.format(date);
}
}
MySamlUtils.java
```java
package com.gilight.cas.authorize.saml2;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.DOMBuilder;
import org.jdom.input.SAXBuilder;
import org.jdom.output.DOMOutputter;
import org.jdom.output.XMLOutputter;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import javax.xml.crypto.dsig.*;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
import javax.xml.crypto.dsig.spec.TransformParameterSpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* saml工具
* @ClassName: MySamlUtils
* @author: gy
* @date: 2024年4月1日
*/
public class MySamlUtils {
private static final String JSR_105_PROVIDER = "org.jcp.xml.dsig.internal.dom.XMLDSigRI";
private static final String SAML_PROTOCOL_NS_URI_V20 = "urn:oasis:names:tc:SAML:2.0:protocol";
/**
* The constructor is intentionally marked as private.
*/
private MySamlUtils() {
// nothing to do
}
public static String signSamlResponse(final String samlResponse, final PrivateKey privateKey, final PublicKey publicKey, X509Certificate x509Certificate) {
Document doc = constructDocumentFromXmlString(samlResponse);
if (doc != null) {
// 获取断言
Element element = (Element) doc.getRootElement().getContent().get(1);
// 断言签名
final Element signedElement = signSamlElement(element, privateKey, publicKey, x509Certificate);
// doc.setRootElement((Element) signedElement.detach());
// return new XMLOutputter().outputString(doc);
// final Element signedElement = signSamlElement(doc.getRootElement(), privateKey, publicKey, x509Certificate);
//element.addContent((Element)signedElement.getContent().get(2));
// 1.断言签名结果转字符串
String signed = getElementXML(toDom2(signedElement)).replace("<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "");
System.out.println(signed);
// TODO 2.通过samlResponse
StringBuilder sb = new StringBuilder();
sb.append(samlResponse.substring(0,samlResponse.indexOf("<samlp:Assertion")));
sb.append(signed);
sb.append(samlResponse.substring(samlResponse.indexOf("</samlp:Assertion>") + 18));
String xml = sb.toString();
return xml;
}
throw new RuntimeException("Error signing SAML Response: Null document");
}
public static Document constructDocumentFromXmlString(final String xmlString) {
try {
final SAXBuilder builder = new SAXBuilder();
builder.setFeature("http://xml.org/sax/features/external-general-entities", false);
builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
return builder.build(new ByteArrayInputStream(xmlString.getBytes()));
} catch (final Exception e) {
return null;
}
}
private static Element signSamlElement(final Element element, final PrivateKey privKey, final PublicKey pubKey, X509Certificate x509Certificate) {
try {
final String providerName = System.getProperty("jsr105Provider", JSR_105_PROVIDER);
final XMLSignatureFactory sigFactory = XMLSignatureFactory.getInstance("DOM", (Provider) Class.forName(providerName).newInstance());
List envelopedTransform = new ArrayList();
envelopedTransform.add(sigFactory.newTransform(Transform.ENVELOPED,(TransformParameterSpec) null));
envelopedTransform.add(sigFactory.newTransform("http://www.w3.org/2001/10/xml-exc-c14n#",(TransformParameterSpec) null));
// Element element1 = (Element) element.getContent().get(1);
// System.out.println(getElementXML(toDom(element1)));
// final Reference ref = sigFactory.newReference("#" + element1.getAttribute("ID").getValue(), sigFactory.newDigestMethod(DigestMethod.SHA256, null), envelopedTransform, null, null);
final Reference ref = sigFactory.newReference("", sigFactory.newDigestMethod(DigestMethod.SHA256, null), envelopedTransform, null, null);
// final Reference ref = sigFactory.newReference("" , sigFactory.newDigestMethod(DigestMethod.SHA1, null), envelopedTransform, null, null);
// Create the SignatureMethod based on the type of key
SignatureMethod signatureMethod;
if (pubKey instanceof DSAPublicKey) {
signatureMethod = sigFactory.newSignatureMethod(SignatureMethod.DSA_SHA1, null);
} else if (pubKey instanceof RSAPublicKey) {
signatureMethod = sigFactory.newSignatureMethod("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", null);
// signatureMethod = sigFactory.newSignatureMethod(SignatureMethod.RSA_SHA1, null);
} else {
throw new RuntimeException("Error signing SAML element: Unsupported type of key");
}
final CanonicalizationMethod canonicalizationMethod =
sigFactory.newCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE, (C14NMethodParameterSpec) null);
// 创建签名信息
final SignedInfo signedInfo = sigFactory.newSignedInfo(canonicalizationMethod, signatureMethod, Collections.singletonList(ref));
//创建包含DSA或RSA PublicKey的KeyValue
final KeyInfoFactory keyInfoFactory = sigFactory.getKeyInfoFactory();
// 使用 KeyInfoFactory 创建 KeyInfo 对象,并添加 X509Data
KeyInfo keyInfo = keyInfoFactory.newKeyInfo(Collections.singletonList(keyInfoFactory.newX509Data(Collections.singletonList(x509Certificate))));
// 在指定位置进行数字签名
// org.w3c.dom.Element w3cElement = toDom(element);
org.w3c.dom.Element w3cElement = toDom2(element);
System.err.println("----1:" + getElementXML(w3cElement));
// 创建签名内容对象privKey私钥, w3cElement 待签名xml
DOMSignContext signContext = new DOMSignContext(privKey, w3cElement);
// xml签名对象signedInfo签名信息, keyInfo公钥相关信息
XMLSignature signature = sigFactory.newXMLSignature(signedInfo, keyInfo);
// 使用提供的私钥对指定的XML元素进行数字签名,将签名结果附加到XML文档中
signature.sign(signContext);
System.err.println("----2:" + getElementXML(w3cElement));
return toJdom(w3cElement);
} catch (final Exception e) {
throw new RuntimeException("Error signing SAML element: " + e.getMessage(), e);
}
}
private static Node getXmlSignatureInsertLocation(final org.w3c.dom.Element elem) {
Node insertLocation = null;
org.w3c.dom.NodeList nodeList = elem.getElementsByTagNameNS(SAML_PROTOCOL_NS_URI_V20, "Extensions");
if (nodeList.getLength() != 0) {
insertLocation = nodeList.item(nodeList.getLength() - 1);
} else {
nodeList = elem.getElementsByTagNameNS(SAML_PROTOCOL_NS_URI_V20, "Status");
insertLocation = nodeList.item(nodeList.getLength() - 1);
}
return insertLocation;
}
private static org.w3c.dom.Element toDom2(final Element element) {
try {
// 克隆要转换的元素
Element clonedElement = (Element) element.clone();
// 将克隆后的元素添加到新的 org.jdom.Document 中
Document jdomDocument = new Document(clonedElement);
// 使用 DOMOutputter 进行转换
DOMOutputter domOutputter = new DOMOutputter();
org.w3c.dom.Document w3cDocument = domOutputter.output(jdomDocument);
// 从 org.w3c.dom.Document 获取 org.w3c.dom.Element
return w3cDocument.getDocumentElement();
}catch (Exception e){
return null;
}
}
private static org.w3c.dom.Element toDom(final Element element) {
return toDom(element.getDocument()).getDocumentElement();
}
private static org.w3c.dom.Document toDom(final Document doc) {
try {
final XMLOutputter xmlOutputter = new XMLOutputter();
final StringWriter elemStrWriter = new StringWriter();
xmlOutputter.output(doc, elemStrWriter);
final byte[] xmlBytes = elemStrWriter.toString().getBytes();
final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
return dbf.newDocumentBuilder().parse(new ByteArrayInputStream(xmlBytes));
} catch (final Exception e) {
return null;
}
}
private static Element toJdom(final org.w3c.dom.Element e) {
return new DOMBuilder().build(e);
}
/**
* 仅测试
* @param w3cElement
* @return
* @author gy
*/
public static String getElementXML(org.w3c.dom.Element w3cElement) {
try {
// 创建转换器工厂
TransformerFactory transformerFactory = TransformerFactory.newInstance();
// 创建转换器
Transformer transformer = transformerFactory.newTransformer();
// 将DOMSource包装W3C Element
DOMSource domSource = new DOMSource(w3cElement);
// 创建StringWriter来保存XML内容
StringWriter writer = new StringWriter();
// 将DOMSource转换为StreamResult,输出到StringWriter
StreamResult result = new StreamResult(writer);
// 执行转换
transformer.transform(domSource, result);
// 返回XML字符串
return writer.toString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* xml转element
* @param xmlString
* @return
* @author gy
*/
public static Element createElement(String xmlString){
try {
// 创建DocumentBuilder
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
// 将XML字符串转换为InputSource
InputSource is = new InputSource(new StringReader(xmlString));
// 解析XML并获取Document对象
Document doc = (Document) builder.parse(is);
// 获取Document的DocumentElement,即根元素<samlp:Response>
Element rootElement = doc.getRootElement();
return rootElement;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* xml转element
* @param xmlString
* @return
* @author gy
*/
public static Element createElement2(String xmlString){
try {
SAXBuilder builder = new SAXBuilder();
Document jdomDocument = builder.build(new StringReader(xmlString));
Element rootElement = jdomDocument.getRootElement();
return rootElement;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}