基于SAML 2.0实现大疆司空2 SSO 登录

时序图

sequenceDiagram
    actor U as 用户(浏览器)
    participant IdP as 第三方平台(IdP)
    participant SP as 大疆司空2(SP)
    participant ACS as 大疆ACS

    Note over U,IdP: 用户可能已登录(有Cookie)或未登录(无Cookie)

    U->>SP: 1. 在大疆SSO登录页输入邮箱
    SP-->>U: 重定向到IdP<br/>/dji-sso/sso?SAMLRequest=xxx
    U->>IdP: 2. 携带SAMLRequest访问IdP

    IdP->>IdP: 检查平台Session Cookie

    alt 用户已登录(Cookie有效)
        IdP->>IdP: 根据Cookie获取用户信息
        IdP->>IdP: 根据用户信息查询SAML配置
        IdP->>IdP: 构建SAMLResponse
        IdP-->>U: 返回包含SAMLResponse的自动提交HTML
        U->>ACS: 3. POST SAMLResponse
        ACS-->>U: 登录成功
    else 用户未登录(无Cookie或Cookie失效)
        IdP-->>U: 返回平台登录页面
        U->>IdP: 输入账号密码
        IdP->>IdP: 验证账号密码
        IdP->>IdP: 创建Session并设置Cookie
        IdP->>IdP: 根据用户信息查询SAML配置
        IdP->>IdP: 构建SAMLResponse
        IdP-->>U: 返回包含SAMLResponse的自动提交HTML
        U->>ACS: 3. POST SAMLResponse
        ACS-->>U: 登录成功
    end

司空说明

image-20260818110816695

示例代码

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!-- 大疆司空2 SSO SAML 依赖 -->
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-core</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-saml-impl</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-saml-api</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-security-impl</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-xmlsec-impl</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-xmlsec-api</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-messaging-api</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.apache.santuario</groupId>
<artifactId>xmlsec</artifactId>
<version>2.1.7</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>

OpenSAML 初始化配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Slf4j
@Configuration
public class OpenSAMLConfig {

@PostConstruct
public void init() {
try {
InitializationService.initialize();
log.info("OpenSAML 初始化成功");
} catch (Exception e) {
log.error("OpenSAML 初始化失败", e);
throw new RuntimeException("OpenSAML 初始化失败", e);
}
}

@Bean
public ParserPool parserPool() {
BasicParserPool pool = new BasicParserPool();
pool.setMaxPoolSize(50);
pool.setNamespaceAware(true);
try {
pool.initialize();
} catch (ComponentInitializationException e) {
throw new RuntimeException("ParserPool 初始化失败", e);
}
return pool;
}
}

SAML 自签名证书生成工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
public class CertificateGenerator {

static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}

/**
* 生成 RSA 密钥对
*/
public static KeyPair generateKeyPair() throws Exception {
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
keyPairGen.initialize(2048, new SecureRandom());
return keyPairGen.generateKeyPair();
}

/**
* 生成自签名证书
*
* @param keyPair 密钥对
* @param entityId 实体ID(用于证书主体名称)
* @param validityDays 证书有效天数
*/
public static X509Certificate generateSelfSignedCertificate(KeyPair keyPair, String entityId, int validityDays) throws Exception {
X500Name issuer = new X500Name("CN=" + entityId);
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
Date notBefore = new Date();
Date notAfter = new Date(System.currentTimeMillis() + validityDays * 24L * 60L * 60L * 1000L);

X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
issuer, serial, notBefore, notAfter, issuer, keyPair.getPublic()
);

certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
certBuilder.addExtension(Extension.keyUsage, true,
new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment));

ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSA")
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(keyPair.getPrivate());

X509CertificateHolder certHolder = certBuilder.build(signer);
return new JcaX509CertificateConverter()
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.getCertificate(certHolder);
}

/**
* 将证书转换为 PEM 格式字符串
*/
public static String certificateToPem(X509Certificate cert) throws Exception {
StringWriter stringWriter = new StringWriter();
try (JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) {
pemWriter.writeObject(cert);
}
return stringWriter.toString();
}

/**
* 将私钥转换为 PEM 格式字符串(PKCS#8格式)
*/
public static String privateKeyToPem(PrivateKey privateKey) throws Exception {
// privateKey.getEncoded()已经是PKCS#8
return "-----BEGIN PRIVATE KEY-----\n"
+ Base64.getMimeEncoder(64, "\n".getBytes())
.encodeToString(privateKey.getEncoded())
+ "\n-----END PRIVATE KEY-----\n";
}

/**
* 生成证书和私钥,返回 PEM 格式字符串数组 [证书PEM, 私钥PEM]
*/
public static String[] generateCertificatePem(String entityId, int validityDays) throws Exception {
KeyPair keyPair = generateKeyPair();
X509Certificate cert = generateSelfSignedCertificate(keyPair, entityId, validityDays);
String certPem = certificateToPem(cert);
String keyPem = privateKeyToPem(keyPair.getPrivate());
return new String[]{certPem, keyPem};
}

public static void main(String[] args) {
try {
int days = 3650;
String entityId = "example.com";
String[] certAndKey = CertificateGenerator.generateCertificatePem(entityId, days);
log.info("大疆SSO证书已生成");
log.info("certPem: \n{}", certAndKey[0]);
log.info("keyPem: \n{}", certAndKey[1]);
} catch (Exception e) {
log.error("生成大疆SSO证书失败", e);
}
}
}

证书工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class CertificateUtils {

/**
* 从类路径或文件系统路径加载 X.509 证书(PEM 格式)
* @param path 路径,支持:
* - 以 "/" 开头的类路径资源,如 "/cert/sp.crt"
* - 绝对文件路径,如 "/opt/certs/sp.crt"
* - 相对文件路径,如 "certs/sp.crt"
* @return X509Certificate 对象
* @throws Exception 解析失败或文件不存在
*/
public static X509Certificate loadCertificate(String path) throws Exception {
try (InputStream is = getInputStream(path);
PEMParser parser = new PEMParser(new InputStreamReader(is))) {
Object obj = parser.readObject();
if (obj instanceof X509Certificate) {
return (X509Certificate) obj;
} else if (obj instanceof X509CertificateHolder) {
return new JcaX509CertificateConverter().getCertificate((X509CertificateHolder) obj);
}
throw new IllegalArgumentException("Unsupported certificate type: " + obj.getClass());
}
}

/**
* 从类路径或文件系统路径加载私钥(PEM 格式)
* @param path 路径,支持格式同 loadCertificate
* @return PrivateKey 对象
* @throws Exception 解析失败或文件不存在
*/
public static PrivateKey loadPrivateKey(String path) throws Exception {
try (InputStream is = getInputStream(path);
PEMParser parser = new PEMParser(new InputStreamReader(is))) {
Object obj = parser.readObject();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();

if (obj instanceof PEMKeyPair) {
return converter.getPrivateKey(((PEMKeyPair) obj).getPrivateKeyInfo());
} else if (obj instanceof PrivateKeyInfo) {
return converter.getPrivateKey((PrivateKeyInfo) obj);
} else if (obj instanceof RSAPrivateKey) {
return new JcaPEMKeyConverter().getPrivateKey(PrivateKeyInfo.getInstance(obj));
}
throw new IllegalArgumentException("Unsupported private key type: " + obj.getClass());
}
}

// 内部方法:根据路径获取 InputStream
private static InputStream getInputStream(String path) throws IOException {
// 1. 尝试作为类路径资源(支持以 '/' 开头)
InputStream is = CertificateUtils.class.getResourceAsStream(path);
if (is != null) {
return is;
}

// 2. 如果类路径没找到,尝试作为文件系统路径
File file = new File(path);
if (file.exists() && file.isFile()) {
return Files.newInputStream(file.toPath());
}

// 3. 最后尝试相对类路径(去掉开头的 '/' 再试一次,适用于某些容器环境)
String classpathPath = path.startsWith("/") ? path.substring(1) : path;
is = CertificateUtils.class.getResourceAsStream("/" + classpathPath);
if (is != null) {
return is;
}

throw new FileNotFoundException("Certificate file not found: " + path);
}
}

IdP 元数据 XML 生成

司空2 SSO需要配置的元数据文档

image-20260818112042452

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/**
* 签名证书路径
*/
private static final String CERTIFICATE_PATH = "/cert/public.crt";

/**
* 签名私钥路径
*/
private static final String PRIVATE_KEY_PATH = "/cert/private.key";

public String generateMetadataXml() throws Exception {
// 创建EntityDescriptor
EntityDescriptor entityDescriptor = (EntityDescriptor) XMLObjectSupport.buildXMLObject(EntityDescriptor.DEFAULT_ELEMENT_NAME);
entityDescriptor.setEntityID(entityId);

// 创建IDPSSODescriptor
IDPSSODescriptor idpSsoDescriptor = (IDPSSODescriptor) XMLObjectSupport.buildXMLObject(IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
idpSsoDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
idpSsoDescriptor.setWantAuthnRequestsSigned(true);

// 添加SSO服务
SingleSignOnService ssoService = (SingleSignOnService) XMLObjectSupport.buildXMLObject(SingleSignOnService.DEFAULT_ELEMENT_NAME);
ssoService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
ssoService.setLocation(ssoLocation);
idpSsoDescriptor.getSingleSignOnServices().add(ssoService);

// 添加证书到KeyDescriptor
X509Certificate cert = CertificateUtils.loadCertificate(CERTIFICATE_PATH);
KeyDescriptor signingKeyDescriptor = createKeyDescriptor(cert, UsageType.SIGNING);
KeyDescriptor encryptionKeyDescriptor = createKeyDescriptor(cert, UsageType.ENCRYPTION);
idpSsoDescriptor.getKeyDescriptors().add(signingKeyDescriptor);
idpSsoDescriptor.getKeyDescriptors().add(encryptionKeyDescriptor);

// 设置NameID格式
NameIDFormat emailFormat = (NameIDFormat) XMLObjectSupport.buildXMLObject(NameIDFormat.DEFAULT_ELEMENT_NAME);
emailFormat.setFormat("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress");
idpSsoDescriptor.getNameIDFormats().add(emailFormat);

entityDescriptor.getRoleDescriptors().add(idpSsoDescriptor);

// 序列化为XML
return marshallObject(entityDescriptor);
}

private KeyDescriptor createKeyDescriptor(X509Certificate cert, UsageType usage) throws Exception {
KeyDescriptor keyDescriptor = (KeyDescriptor) XMLObjectSupport.buildXMLObject(KeyDescriptor.DEFAULT_ELEMENT_NAME);
keyDescriptor.setUse(usage);

// 创建KeyInfo
KeyInfo keyInfo = (org.opensaml.xmlsec.signature.KeyInfo)
XMLObjectSupport.buildXMLObject(KeyInfo.DEFAULT_ELEMENT_NAME);

// 创建X509Data
X509Data x509Data = (X509Data)
XMLObjectSupport.buildXMLObject(X509Data.DEFAULT_ELEMENT_NAME);

// 创建X509Certificate
org.opensaml.xmlsec.signature.X509Certificate x509Cert = (org.opensaml.xmlsec.signature.X509Certificate)
XMLObjectSupport.buildXMLObject(org.opensaml.xmlsec.signature.X509Certificate.DEFAULT_ELEMENT_NAME);

// 设置证书值 (Base64编码)
x509Cert.setValue(Base64.getEncoder().encodeToString(cert.getEncoded()));
x509Data.getX509Certificates().add(x509Cert);
keyInfo.getX509Datas().add(x509Data);
keyDescriptor.setKeyInfo(keyInfo);

return keyDescriptor;
}

private String marshallObject(SAMLObject object) throws MarshallingException, TransformerException {
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
Element dom = marshaller.marshall(object);

Transformer transformer = TransformerFactory.newInstance().newTransformer();
StringWriter writer = new StringWriter();
transformer.transform(new DOMSource(dom), new StreamResult(writer));
return writer.toString();
}

IdP 登录服务接口

接收大疆的 SAML AuthnRequest,接口需公开请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
// 获取DjiSsoConfigResp逻辑自行实现。主要就是通过浏览器里存储的Session传到后端做校验获取用户信息

/**
* 大疆ACS URL(SP端点)
*/
public static final String DJI_ACS_URL = "https://es-flight-api.djigate.com/manage/api/v1/sso/acs";

/**
* 校验 SAML IssueInstant 是否有效,允许 5 分钟偏差
*/
private static final long ALLOWED_CLOCK_SKEW_SECONDS = 5 * 60;

/**
* SAMLRequest 解压后最大字节数,防止 zip bomb 攻击
*/
private static final int MAX_INFLATED_SIZE = 100 * 1024;

public void handleSsoRequestWithConfig(
String samlRequest,
DjiSsoConfigResp config,
HttpServletResponse response) throws Exception {

// 1. 解码 SAML AuthnRequest
AuthnRequest authnRequest = decodeSamlRequest(samlRequest);
log.info("收到大疆SAML认证请求,ID: {}, 使用邮箱: {}", authnRequest.getID(), config.getSsoEmail());

// 2. 校验 AuthnRequest 合法性
validateAuthnRequest(authnRequest);

// 3. 使用配置的邮箱构建 SAML Response
Response samlResponse = buildSamlResponseWithEmail(authnRequest, config.getSsoEmail());

// 4. 发送 Response 到大疆 ACS
sendResponseToAcs(samlResponse, response);

log.info("SAML Response已发送到大疆ACS,邮箱: {}", config.getSsoEmail());
}

/**
* 解码 SAMLRequest(Base64 + inflate)
*/
public AuthnRequest decodeSamlRequest(String samlRequest) throws Exception {
byte[] decoded = Base64.getDecoder().decode(samlRequest);
byte[] inflated = inflate(decoded);
AuthnRequest authnRequest = (AuthnRequest) XMLObjectSupport.unmarshallFromInputStream(
XMLObjectProviderRegistrySupport.getParserPool(),
new ByteArrayInputStream(inflated)
);
return authnRequest;
}

/**
* 校验 AuthnRequest 的合法性
*/
private void validateAuthnRequest(AuthnRequest authnRequest) {
// 校验 IssueInstant
DateTime issueInstant = authnRequest.getIssueInstant();
if (issueInstant == null) {
log.warn("SAML AuthnRequest 缺少 IssueInstant");
throw new ServiceException(ResultCodeEnum.PARAMETER_ERROR, "AuthnRequest 缺少 IssueInstant");
}
DateTime now = DateTime.now();
int secondsDiff = Math.abs(Seconds.secondsBetween(issueInstant, now).getSeconds());
if(secondsDiff > ALLOWED_CLOCK_SKEW_SECONDS) {
log.warn("IssueInstant 偏差超过 5 分钟");
throw new ServiceException(ResultCodeEnum.PARAMETER_ERROR, "IssueInstant 偏差超过 5 分钟");
}

// 校验 Issuer
Issuer issuer = authnRequest.getIssuer();
if (issuer == null || StrUtil.isBlank(issuer.getValue())) {
log.warn("SAML AuthnRequest 缺少 Issuer");
throw new ServiceException(ResultCodeEnum.PARAMETER_ERROR, "AuthnRequest 缺少 Issuer");
}
URI uri = URI.create(issuer.getValue());
String path = uri.getPath();
String newPath = path.replaceFirst("^/[^/]+", "");
String result = uri.getScheme() + "://" + uri.getHost() + newPath;
if (!DJI_ACS_URL.equals(result)) {
log.warn("SAML AuthnRequest Issuer 不匹配,期望: {}, 实际: {}", DJI_ACS_URL, result);
throw new ServiceException(ResultCodeEnum.PARAMETER_ERROR, "AuthnRequest Issuer 不合法");
}

// 校验 Destination
String destination = authnRequest.getDestination();
if (StrUtil.isNotBlank(destination) && !destination.equals(ssoLocation)) {
log.warn("SAML AuthnRequest Destination 不匹配,期望: {}, 实际: {}", ssoLocation, destination);
throw new ServiceException(ResultCodeEnum.PARAMETER_ERROR, "AuthnRequest Destination 不合法");
}
}

/**
* 使用指定邮箱构建 SAML Response
*/
private Response buildSamlResponseWithEmail(AuthnRequest authnRequest, String ssoEmail) throws Exception {
log.info("构建SAML Response,邮箱: {}", ssoEmail);

Response response = (Response) XMLObjectSupport.buildXMLObject(Response.DEFAULT_ELEMENT_NAME);
response.setID("_" + generateId());
response.setIssueInstant(new DateTime());
response.setDestination(DJI_ACS_URL);
response.setIssuer(buildIssuer());
response.setStatus(buildStatus());

Assertion assertion = buildAssertion(authnRequest, ssoEmail);
response.getAssertions().add(assertion);

signResponse(response);

return response;
}

/**
* 发送SAML Response到大疆ACS
*/
private void sendResponseToAcs(Response samlResponse, HttpServletResponse response) throws Exception {
MessageContext<SAMLObject> context = new MessageContext<>();
context.setMessage(samlResponse);

// 设置签名参数
SignatureSigningParameters signingParameters = new SignatureSigningParameters();
signingParameters.setSigningCredential(buildCredential());
signingParameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
signingParameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);

SecurityParametersContext securityContext = context.getSubcontext(SecurityParametersContext.class, true);
securityContext.setSignatureSigningParameters(signingParameters);

VelocityEngine velocityEngine = new VelocityEngine();
velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADER, "class");
velocityEngine.setProperty("class.resource.loader.class",
"org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
velocityEngine.init();

SAMLPeerEntityContext peerEntityContext = context.getSubcontext(SAMLPeerEntityContext.class, true);
SAMLEndpointContext endpointContext = peerEntityContext.getSubcontext(SAMLEndpointContext.class, true);

SingleSignOnService endpoint = new SingleSignOnServiceBuilder().buildObject();
endpoint.setLocation(DJI_ACS_URL);
endpoint.setBinding(SAMLConstants.SAML2_POST_BINDING_URI);
endpointContext.setEndpoint(endpoint);

HTTPPostEncoder encoder = new HTTPPostEncoder();
encoder.setHttpServletResponse(response);
encoder.setMessageContext(context);
encoder.setVelocityEngine(velocityEngine);
encoder.initialize();
encoder.encode();
}

private String generateId() {
return UUID.randomUUID().toString().replace("-", "");
}

private Issuer buildIssuer() {
Issuer issuer = (Issuer) XMLObjectSupport.buildXMLObject(Issuer.DEFAULT_ELEMENT_NAME);
issuer.setValue(entityId);
return issuer;
}

private Status buildStatus() {
Status status = (Status) XMLObjectSupport.buildXMLObject(Status.DEFAULT_ELEMENT_NAME);
StatusCode statusCode = (StatusCode) XMLObjectSupport.buildXMLObject(StatusCode.DEFAULT_ELEMENT_NAME);
statusCode.setValue(StatusCode.SUCCESS);
status.setStatusCode(statusCode);
return status;
}

private Assertion buildAssertion(AuthnRequest authnRequest, String ssoEmail) {
Assertion assertion = (Assertion) XMLObjectSupport.buildXMLObject(Assertion.DEFAULT_ELEMENT_NAME);
assertion.setID("_" + generateId());
assertion.setIssueInstant(new DateTime());
assertion.setIssuer(buildIssuer());

// 添加Subject
Subject subject = (Subject) XMLObjectSupport.buildXMLObject(Subject.DEFAULT_ELEMENT_NAME);
NameID nameID = (NameID) XMLObjectSupport.buildXMLObject(NameID.DEFAULT_ELEMENT_NAME);
nameID.setFormat("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress");
nameID.setValue(ssoEmail);

SubjectConfirmation subjectConfirmation = (SubjectConfirmation) XMLObjectSupport.buildXMLObject(SubjectConfirmation.DEFAULT_ELEMENT_NAME);
subjectConfirmation.setMethod(SubjectConfirmation.METHOD_BEARER);

SubjectConfirmationData data = (SubjectConfirmationData) XMLObjectSupport.buildXMLObject(SubjectConfirmationData.DEFAULT_ELEMENT_NAME);
String acsUrl = authnRequest.getAssertionConsumerServiceURL();
data.setRecipient(StrUtil.isNotBlank(acsUrl) ? acsUrl : DJI_ACS_URL);
data.setNotOnOrAfter(new DateTime().plusMinutes(5));
data.setInResponseTo(authnRequest.getID());

subjectConfirmation.setSubjectConfirmationData(data);
subject.getSubjectConfirmations().add(subjectConfirmation);
subject.setNameID(nameID);
assertion.setSubject(subject);

// 添加条件
Conditions conditions = (Conditions) XMLObjectSupport.buildXMLObject(Conditions.DEFAULT_ELEMENT_NAME);
conditions.setNotBefore(new DateTime().minusSeconds(60));
conditions.setNotOnOrAfter(new DateTime().plusMinutes(5));
assertion.setConditions(conditions);

// 添加认证声明
AuthnStatement authnStatement = (AuthnStatement) XMLObjectSupport.buildXMLObject(AuthnStatement.DEFAULT_ELEMENT_NAME);
authnStatement.setAuthnInstant(new DateTime());
authnStatement.setSessionIndex("session_" + generateId());
authnStatement.setSessionNotOnOrAfter(new DateTime().plusHours(8));

AuthnContext authnContext = (AuthnContext) XMLObjectSupport.buildXMLObject(AuthnContext.DEFAULT_ELEMENT_NAME);
AuthnContextClassRef classRef = (AuthnContextClassRef) XMLObjectSupport.buildXMLObject(AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
classRef.setAuthnContextClassRef(AuthnContext.PASSWORD_AUTHN_CTX);
authnContext.setAuthnContextClassRef(classRef);
authnStatement.setAuthnContext(authnContext);
assertion.getAuthnStatements().add(authnStatement);

return assertion;
}

private void signResponse(Response response) throws Exception {
// 1. 创建签名对象
Signature signature = (Signature) XMLObjectSupport.buildXMLObject(Signature.DEFAULT_ELEMENT_NAME);
BasicX509Credential credential = buildCredential();
signature.setSigningCredential(credential);
signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);

// 2. 将签名对象附加到Response
response.setSignature(signature);

// 3. 重要:在签名前必须先marshall对象
// 这将构建DOM结构,使签名对象有XMLSignature实例
marshallResponse(response);

// 4. 执行签名
Signer.signObject(signature);
}

private void marshallResponse(Response response) throws MarshallingException {
// 获取Response的Marshaller
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(response);

// 将Response对象marshall到DOM
marshaller.marshall(response);
}

private BasicX509Credential buildCredential() throws Exception {
X509Certificate cert = CertificateUtils.loadCertificate(CERTIFICATE_PATH);
PrivateKey privateKey = CertificateUtils.loadPrivateKey(PRIVATE_KEY_PATH);

BasicX509Credential credential = new BasicX509Credential(cert);
credential.setEntityCertificate(cert);
credential.setPrivateKey(privateKey);
credential.setEntityId(entityId);
return credential;
}

private byte[] inflate(byte[] input) throws IOException, java.util.zip.DataFormatException {
Inflater inflater = new Inflater(true);
inflater.setInput(input);

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];

while (!inflater.finished()) {
int count = inflater.inflate(buffer);
outputStream.write(buffer, 0, count);
if (outputStream.size() > MAX_INFLATED_SIZE) {
throw new IOException("SAMLRequest 解压后数据超过最大限制");
}
}

outputStream.close();
return outputStream.toByteArray();
}