【问题标题】:How do I correctly prepare an 'HTTP Redirect Binding' SAML Request using Java?如何使用 Java 正确准备“HTTP 重定向绑定”SAML 请求?
【发布时间】:2025-12-23 03:40:12
【问题描述】:

有一些测试资源,例如http://sometestresource.com 我请求在http://sometestresource.com/Login 上进行授权,然后我重定向到了一个 idP 提供者:http://someidpprovoder.com/idp/authn/CommonLogin

嗅探器检测到重定向到https://someidpprovoder.com/idp/profile/SAML2/Redirect/SSO

SAMLRequest=fZJda...D&RelayState=https%3A%2F%2Fwww.sometestresource.com%2Fcms%2Fextentions%2Fsimplesaml%2Fwww%2Fmodule.php%2Fcore%2Fauthenticate.php%3Fas%3D100000za-sp
&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1
&Signature=dAx0M...D

好的,我编写了一个简单的 servlet 应用程序,我想获得相同的功能 - Web Browser SSO Profile

我的 ServiceProvider servlet:

@WebServlet(urlPatterns={"/ServiceProvider"},
            initParams={ @WebInitParam(name="Issuer", value="some.issuer"),
                         @WebInitParam(name="IdpUrl", value="https://someidpprovoder.com/idp/profile/SAML2/Redirect/SSO"),
                         @WebInitParam(name="ConsumerUrl", value="http://localhost:8080/com.secure.isusia-1.0-SNAPSHOT/ServiceProvider")
            } )
public class ServiceProvider  extends HttpServlet {
    private static final long serialVersionUID = 1L;
    private SamlConsumerManager consumer;

    public void init(ServletConfig config) throws ServletException {
        try {
            consumer = new SamlConsumerManager(config);
        } catch (ConfigurationException e) {
            throw new ServletException("Errow while configuring SAMLConsumerManager", e);
        }
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException,
            IOException {
        doPost(request, response);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String responseMessage = request.getParameter("SAMLResponse");
        if (responseMessage != null) { 
            // response from the identity provider
            if (result == null) {
                // lets logout the user
            } else if (result.size() == 1) {
                 //
                 // No user attributes are returned, so just goto the default
                 //home page.
                 //
            } else if (result.size() > 1) {
                 // We have received attributes, so lets show them in the
                 // attribute home page.
                 //

            } else {
                // something wrong, re-login
            }
        } else { 
            /* time to create the authentication request or logout request */
        }
    }
}

我的 SamlConsumerManager:

public class SamlConsumerManager {
    private String consumerUrl;
    private String authReqRandomId;
    private String relayState;
    private String issuerId;
    private String idpUrl;

    private char[] password = "...".toCharArray();
    private String alias = "...";

    private String sigAlg = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";

    private Signature signature;
    private BasicX509Credential credential;
    private Element authDOM;

    public SamlConsumerManager(ServletConfig servletConfig) throws ConfigurationException {
        authReqRandomId = Integer.toHexString(new Double(Math.random()).intValue());        
        consumerUrl = servletConfig.getInitParameter("ConsumerUrl");
        idpUrl = servletConfig.getInitParameter("IdpUrl");
        issuerId = servletConfig.getInitParameter("Issuer");

        DefaultBootstrap.bootstrap();
    }

    public String buildRequestMessage(HttpServletRequest request) {

        RequestAbstractType requestMessage = null;

        if (request.getParameter("logout") == null) {
            // time to build the authentication request message
        } else { 
            // ok, user needs to be single logged out
        }

        String encodedRequestMessage = encodeRequestMessage(requestMessage);

        /* SAML2 Authentication Request is appended to IP's URL */
        return idpUrl + "?SAMLRequest=" + encodedRequestMessage + "&SigAlg=" + sigAlg;
    }

    private LogoutRequest buildLogoutRequest(String user) {
        LogoutRequest logoutReq = new LogoutRequestBuilder().buildObject();
        logoutReq.setID(Util.createID());

        DateTime issueInstant = new DateTime();
        logoutReq.setIssueInstant(issueInstant);
        logoutReq.setNotOnOrAfter(new DateTime(issueInstant.getMillis() + 5 * 60 * 1000));

        IssuerBuilder issuerBuilder = new IssuerBuilder();
        Issuer issuer = issuerBuilder.buildObject();
        issuer.setValue(issuerId);
        logoutReq.setIssuer(issuer);

        NameID nameId = new NameIDBuilder().buildObject();
        nameId.setFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:entity");
        nameId.setValue(user);
        logoutReq.setNameID(nameId);

        SessionIndex sessionIndex = new SessionIndexBuilder().buildObject();
        sessionIndex.setSessionIndex(UIDGenerator.generateUID());
        logoutReq.getSessionIndexes().add(sessionIndex);

        logoutReq.setReason("Single Logout");

        return logoutReq;
    }

    private AuthnRequest buildAuthnRequestObject() {
        /* Building Issuer object */
        IssuerBuilder issuerBuilder = new IssuerBuilder();
        Issuer issuer = issuerBuilder.buildObject("urn:oasis:names:tc:SAML:2.0:assertion", "Issuer", "samlp");
        issuer.setValue(issuerId);

        /* NameIDPolicy */
        NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder();
        NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject();
        nameIdPolicy.setFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:persistent");
        nameIdPolicy.setSPNameQualifier("Isser");
        nameIdPolicy.setAllowCreate(new Boolean(true));

        /* AuthnContextClass */
        AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder();
        AuthnContextClassRef authnContextClassRef = authnContextClassRefBuilder.buildObject(
                        "urn:oasis:names:tc:SAML:2.0:assertion",
                        "AuthnContextClassRef",
                        "saml");

        authnContextClassRef.setAuthnContextClassRef("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport");

        /* AuthnContex */
        RequestedAuthnContextBuilder requestedAuthnContextBuilder =
                new RequestedAuthnContextBuilder();
        RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject();
        requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT);
        requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef);

        DateTime issueInstant = new DateTime();

        /* Creation of AuthRequestObject */
        AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder();
        AuthnRequest authRequest =
                authRequestBuilder.buildObject("urn:oasis:names:tc:SAML:2.0:protocol",
                        "AuthnRequest", "samlp");
        authRequest.setForceAuthn(new Boolean(false));
        authRequest.setIsPassive(new Boolean(false));
        authRequest.setIssueInstant(issueInstant);
        authRequest.setProtocolBinding("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST");
        authRequest.setAssertionConsumerServiceURL(consumerUrl);
        authRequest.setIssuer(issuer);
        authRequest.setNameIDPolicy(nameIdPolicy);
        authRequest.setRequestedAuthnContext(requestedAuthnContext);
        authRequest.setID(authReqRandomId);
        authRequest.setVersion(SAMLVersion.VERSION_20);

        KeyStore  keyStore = KeyStore.getInstance(KeyStore.getDefaultType());

        FileInputStream fileInputStream = new FileInputStream(new File("....jks"));        

        keyStore.load(fileInputStream, password);
        fileInputStream.close();

        KeyStore.PrivateKeyEntry privateKeyEntry = null;
        privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, new KeyStore.PasswordProtection(password));

        PrivateKey privateKey = privateKeyEntry.getPrivateKey();

        X509Certificate certificate = (X509Certificate) privateKeyEntry.getCertificate();

        credential = new BasicX509Credential();
        credential.setEntityCertificate(certificate);
        credential.setPrivateKey(privateKey);

        try {
            DefaultBootstrap.bootstrap();
        } catch (ConfigurationException e) {
            e.printStackTrace();
        }

        signature = (Signature) org.opensaml.xml.Configuration.getBuilderFactory().getBuilder(org.opensaml.xml.signature.Signature.DEFAULT_ELEMENT_NAME)
                .buildObject(org.opensaml.xml.signature.Signature.DEFAULT_ELEMENT_NAME);

        signature.setSigningCredential(credential);
        signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1);
        signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);

        authRequest.setSignature(signature);

        return authRequest;
    }

    private String encodeRequestMessage(RequestAbstractType requestMessage)
            throws MarshallingException,
            IOException {

        Marshaller marshaller = Configuration.getMarshallerFactory().getMarshaller(requestMessage);
        Element authDOM = marshaller.marshall(requestMessage);

        Deflater deflater = new Deflater(Deflater.DEFLATED, true);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        DeflaterOutputStream deflaterOutputStream =
                new DeflaterOutputStream(byteArrayOutputStream,
                        deflater);

        StringWriter rspWrt = new StringWriter();
        XMLHelper.writeNode(authDOM, rspWrt);
        deflaterOutputStream.write(rspWrt.toString().getBytes());
        deflaterOutputStream.close();

        /* Encoding the compressed message */
        String encodedRequestMessage =
                Base64.encodeBytes(byteArrayOutputStream.toByteArray(),
                        Base64.DONT_BREAK_LINES);
        return URLEncoder.encode(encodedRequestMessage, "UTF-8").trim();
    }

}

但是,我得到一个错误页面,而不是来自我的 idP 的登录页面。

我做错了什么?我想知道如何正确准备授权请求。例如,我不明白如何在 URL 中添加签名:

&Signature=dAx0M...D

更新

  • 我正在使用 SP 发起的 Web SSO。
  • 基于Shibboleth的远程idP(在客户端,关于它我什么都不知道。只是它是基于Shibboleth编写的)。

【问题讨论】:

    标签: java single-sign-on saml-2.0 shibboleth


    【解决方案1】:

    您需要签名,

    buildAuthnRequestObjectauthRequest.setSignature(signature);

    添加

    Configuration.getMarshallerFactory().getMarshaller(authRequest).marshall(authRequest);`
        if(signature!=null) {
            Signer.signObject(signature);
        }
    

    Signerorg.opensaml.xml.signature.Signer

    ** 更新**

    使用SAMLMessageContextHTTPRedirectDeflateEncoder 发送重定向

    public void doAuthenticationRedirect (HttpServletResponse response, final HttpSession currentSessiond) throws Exception {  
    
        /** Generate your authnrequest **/
        AuthnRequest authnRequest = generateAuthnRequest();  
    
        /** Create an adapter from tomcat response servlet **/
        HttpServletResponseAdapter responseAdapter = new HttpServletResponseAdapter(response, true); 
    
        /** 302 status code **/
        responseAdapter.setStatusCode(HttpServletResponse.SC_MOVED_TEMPORARILY);
    
        /** Build blank context **/
        SAMLMessageContext<?, AuthnRequest, ?> context =  SAMLUtility.makeSamlMessageContext();
    
        /** Set entity end point**/
        context.setPeerEntityEndpoint(endpointObject);  
    
        context.setOutboundSAMLMessage(authnRequest);  
    
       /** Sign this request **/
        context.setOutboundSAMLMessageSigningCredential(getSigningCredential());  
        context.setOutboundMessageTransport(responseAdapter);
         /** Run encoder **/
        try {  
            runEncoder(new HTTPRedirectDeflateEncoder(), context);
        } catch (Throwable t) {  
            Log.error("Error endcoding AuthnRequest: ",t);
        }  
    }  
    
    
     /** 
      * Encode our Context and send it
      * 
      * @param encoder
      * @param context
      * @throws IOException
      * @throws MessageEncodingException
      */
      private void runEncoder(MessageEncoder encoder, MessageContext context)      throws IOException, MessageEncodingException {
          encoder.encode(context);
      }
    

    如果你需要,这里是makeSamlMessageContext()

     public static <TI extends SAMLObject, TO extends SAMLObject, TN extends SAMLObject>
        SAMLMessageContext<TI, TO, TN> makeSamlMessageContext() {
          return new BasicSAMLMessageContext<TI, TO, TN>();
     }
    

    【讨论】:

    • 我认为问题在于您的encodeRequestMessage 方法,既然您使用的是OPENSAML,为什么不让它为您重定向SAMLMessageContextHTTPRedirectDeflateEncoder
    • 我更新了我的答案,抱歉在工作中,但我尽量解释了