【问题标题】:How do I connect to a remote URL which requires Spring Security forms authentication (Java)?如何连接到需要 Spring Security 表单身份验证 (Java) 的远程 URL?
【发布时间】:2011-02-23 22:58:10
【问题描述】:

我进行了多次搜索,但似乎无法找到看似简单的身份验证方案的答案。

我们有一个现有的 Java Web 应用程序,它使用 Spring 提供的基于表单的授权。我们正在尝试通过我们的门户网站访问此应用程序,而不要求用户输入他们的凭据 (SSO)。

门户有一个凭证保险库,我们可以成功地访问服务器端远程 Web 应用程序的机密。我们正在使用 Apache 的 HTTP 组件实用程序将登录请求发布到 j_spring_security_check 并成功进行身份验证。对这篇文章的响应会返回一个 302 重定向到应用程序主页,并设置一个带有会话 id 的 cookie。

现在我们必须以某种方式将此经过身份验证的会话发送回浏览器,这就是我们遇到问题的地方。简单地将浏览器重定向到主页是行不通的——它会将我们重定向到登录页面。将所有响应标头完全按照在服务器端接收到的方式转发回浏览器也不起作用 - 仍然返回到登录页面。

那么,我们如何在服务器端进行身份验证并且仍然能够在客户端加载目标页面?

我对此比较陌生,所以如果这是一个愚蠢的问题,我深表歉意。感谢您提供有关替代方法的任何帮助或建议。

注意事项:


HttpComponent 客户端代码:

DefaultHttpClient httpclient = new DefaultHttpClient();
    try {
        // try to get the home page
        HttpGet httpget = new HttpGet("http://<host>/<root>/home.action");
        HttpResponse httpClientResponse = httpclient.execute(httpget);
        HttpEntity entity = httpClientResponse.getEntity();

        // check status and close entity stream
        System.out.println("Login form get: " + httpClientResponse.getStatusLine());
        EntityUtils.consume(entity);

        // check cookies
        System.out.println("Initial set of cookies:");
        List<Cookie> cookies = httpclient.getCookieStore().getCookies();
        printCookies(cookies);

        /***  Login ***/
        HttpPost httppost = new HttpPost("http://<host>/<root>/j_spring_security_check");

        // Prepare post parameters
        List <NameValuePair> nvps = new ArrayList <NameValuePair>();
        nvps.add(new BasicNameValuePair("j_username", getUserFromVault()));
        nvps.add(new BasicNameValuePair("j_password", getPasswordFromVault()));
        httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));

        httpClientResponse = httpclient.execute(httppost);

        // copy response headers and determine redirect location
        Header[] allHeaders = httpClientResponse.getAllHeaders();
        System.out.println("Headers: ");
        String location = "";
        for (Header header : allHeaders) {
            System.out.println(header);
            if("location".equalsIgnoreCase(header.getName())) location = header.getValue();
            response.addHeader(header.getName(), header.getValue());
        }

        // check response body
        entity = httpClientResponse.getEntity();
        System.out.println("Response content: " + httpClientResponse.getStatusLine());
        System.out.println(EntityUtils.toString(entity)); // always empty
        EntityUtils.consume(entity);

        // check cookies
        System.out.println("Post logon cookies:");
        cookies = httpclient.getCookieStore().getCookies();
        printCookies(cookies);

        // populate redirect information in response
        System.out.println("Redirecting to: " + locationHeaderValue);
        response.setStatus(httpClientResponse.getStatusLine().getStatusCode()); // 302

        // test if server-side get works for home page at this point (it does)
        httpget = new HttpGet(location);
        httpClientResponse = httpclient.execute(httpget);
        entity = httpClientResponse.getEntity();

        // print response body (all home content is loaded)
        System.out.println("home get: " + httpClientResponse.getStatusLine());
        System.out.println("Response content: " + httpClientResponse.getStatusLine());
        System.out.println(EntityUtils.toString(entity));
        EntityUtils.consume(entity);

    } finally {
        httpclient.getConnectionManager().shutdown();
    }

服务端登录成功返回的Headers:

HTTP/1.1 302 Found
Date: Wed, 23 Feb 2011 22:09:03 GMT
Server: Apache/2.2.3 (CentOS)
Set-Cookie: JSESSIONID=6F98B0B9A65BA6AFA0472714A4C816E5; Path=<root>
Location: http://<host>/<root>/home.action
Content-Type: text/plain; charset=UTF-8
Content-Length: 0
Via: 1.1 PPWebFilter.<host>:80 (IronPort-WSA/7.0.0-825)
Connection: keep-alive

来自客户端请求和响应的标头:
要求:

GET /<root>/home.action HTTP/1.1  
Host: <host>  
Connection: keep-alive  
Referer: http://localhost:10039/SCMViewer/TestLoginServlet?launchScm=Launch+SCM+servlet  
Accept:application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5  
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.98 Safari/534.13  
Accept-Encoding: gzip,deflate,sdch  
Accept-Language: en-US,en;q=0.8  
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3  
Cookie: JSESSIONID=FC8E823AB1A1545BE8518DB4D097E665  

响应(重定向到登录):

HTTP/1.1 302 Found
Date: Wed, 23 Feb 2011 22:09:03 GMT
Server: Apache/2.2.3 (CentOS)
Location: http://<host>/<root>/security/login.action
Content-Type: text/plain; charset=UTF-8
Content-Length: 0
Via: 1.1 PPWebFilter.<host>:80 (IronPort-WSA/7.0.0-825)
Connection: keep-alive

作为测试,我们编写了一些看似可行的 hack,但由于太不安全而无法实现:

  • 在 jsp 中嵌入了一个表单,它将登录凭据直接发布到远程站点的 j_spring_security_check。
  • 编写了一个 servlet 方法来从保管库中检索凭据。
  • 将客户端的凭据填充到隐藏的表单字段中,并通过 javascript 提交表单。

【问题讨论】:

  • 您知道 JSESSIONID cookie 不匹配吗?
  • @donal-fellows:很好。 JSESSIONID 是不同的,因为 cookie 没有在客户端更新 - 大概是因为我试图保存一个归属于不同域的 cookie。

标签: java apache forms-authentication spring-security


【解决方案1】:

有点难以理解您的应用程序正在尝试做什么,但我最好的猜测是您的“门户”位于用户的浏览器和应用程序之间,并且您正在尝试为应用程序使用一些存储的凭据代表用户进行身份验证。

您需要注意/处理两件事。

来自应用程序的响应将包含某种SetCookie 标头。 cookie 需要小心处理。根据您使用的安全模型:

  • 它们可以保存在门户中并用于将来对应用程序的请求。
  • 它们可以转发到用户的浏览器。门户还需要在未来的请求中将 cookie 传递给应用程序。 (这种方法需要小心处理,以处理会话令牌泄漏可能出现的问题。)

另外,请注意 SpringSecurity 会在登录成功时更改会话 cookie。如果您不捕获新的会话 cookie 并将它们用于后续对应用程序的请求,则这些请求将不会被验证。

应用程序的登录机制显然试图在登录后将您(门户)重定向到“默认”位置,这是不恰当的。有两个简单的解决方法:

  • 让门户检测到最终重定向并将其视为您已成功登录的指示。然后让门户使用新 cookie 重复您最初从应用程序请求的页面的请求(请参阅以上)。

  • IIRC,有一个额外的参数可以添加到 j_spring_security_check 请求中,告诉应用程序在成功登录时返回到哪里。我记不起细节了……


我认为将来自 RA 的 setCookie 响应标头转发到门户网站对浏览器的响应中,这就是将 cookie/会话 ID 传输到用户的新浏览器窗口所需的全部内容。这不正确吗?

这将导致浏览器为门户上下文设置 RA 的 cookie。除非 RA 和门户网站在 cookie 的“范围”内(为了更好的词),否则这将不起作用。

问题是,如何在门户上/通过门户显示此内容?我是否只需要复制所有内容并相应地映射所有相关链接?而且,正如您所说,继续通过门户代理对应用程序的所有请求,每次都传递 cookie?有什么方法可以避免复制/修改标记?

确实需要按摩标记。但究竟需要什么按摩并不完全清楚。我认为您需要映射相对链接,以便当用户的浏览器看到它们时它们指向门户。然后,安排门户使用适当的 cookie 将请求中继到 RA。

您可以用来处理相对链接的一个工具是HTML &lt;base&gt; element。事实上,这可能比绝对链接更容易处理……如果您通过门户映射所有内容。

但要注意,在这个过程中,有各种各样的事情会导致悲伤。例如,您必须注意“相同来源”限制,以及带有 RA 嵌入 URL 的 javascript。

【讨论】:

  • 您好斯蒂芬,感谢您的回复。你的假设有些正确。远程应用程序 (RA) 将由门户启动并进行身份验证,但之后它将不再充当中间人。此时,我们不希望通过门户访问 RA - 只需验证并直接在新的浏览器窗口中打开 RA。我认为将 RA 中的 setCookie 响应标头转发到门户网站对浏览器的响应中,这就是将 cookie/会话 ID 传输到用户的新浏览器窗口所需的全部内容。这不正确吗?
  • @stephen-c 我开始意识到这不会按计划进行(没有中间人)。但是,我已经能够完成您的第三个项目符号:“......让门户重复您最初使用新 cookie 从应用程序请求的页面的请求”。问题是,我如何在门户上/通过门户显示这个?我是否只需要复制所有内容并相应地映射所有相关链接?而且,正如您所说,继续通过门户代理对应用程序的所有请求,每次都传递 cookie?有什么方法可以避免复制/修改标记?
【解决方案2】:

如果有人感兴趣,这就是结果。

一旦我们意识到设置外部 cookie 的问题,我们决定我们有几个选择:

  1. 代理 - 通过传送门通往 远程应用程序,使用门户 作为代理。这个选项最 逻辑上直截了当,但它 有上述并发症 (即您必须修改每个 请求和每个响应 - 添加 cookie 和必要的标记)。 这种方法结果很痛苦 对我们来说,与我们无关 使用 IBM WebSphere Portal 7。
  2. 第三方 SSO 解决方案 - 使用 CAS 或 Tivoli 或其他一些企业解决方案。这是我们的 理想的最终解决方案,但它是 仍在研究以确定 与我们的环境兼容。
  3. Cookie Monster - 我们的临时解决方案,为了 让 IBM 门户成为 中间人,是部署一个小 新的远程应用程序在同一个 服务器作为我们的目标应用程序 接受 JSON 格式的 cookie 和 将其吐回浏览器 302 重定向响应。

cookie monster 解决方案的工作原理如下:当 用户点击链接中的 门户,我们的 portlet 将在内部 查找用户的凭据, 向远程验证 应用程序,并返回 身份验证 cookie/令牌。我们 转换它(以及 目标 URL) 到 JSON 并返回 它到浏览器。然后浏览器 将此 JSON 发布到远程 cookie 在新窗口中申请。这 cookie 被重组并放置 在响应中连同 302 和目标位置。瞧, 页面重定向到应用程序 主页和用户已登录。耶!

给使用 IBM WebSphere Portal 的任何人的一些注意事项:

  • 我们通过以下方式处理了身份验证 资源服务 portlet。
  • 确保来自资源服务 portlet 的响应没有被缓存(我们让缓存立即过期,因为我们无法返回 no-cache)
  • 确保在进行 ajax 调用之前 ping 门户,因为会话可能已过期。

我确信还有其他更优雅的解决方案,但这对我们有用,直到我们启动并运行 CAS/Tivoli。

【讨论】:

    猜你喜欢
    • 2010-10-04
    • 1970-01-01
    • 2012-05-02
    • 1970-01-01
    • 1970-01-01
    • 2017-09-14
    • 1970-01-01
    • 2014-01-05
    相关资源
    最近更新 更多