如何在Java Servlet过滤器中安全地处理密码?

时间:2014-02-12 16:50:17

标签: java security servlets filter passwords

我有一个通过HTTPS处理BASIC身份验证的过滤器。这意味着有一个名为“Authorization”的标题,其值类似于“Basic aGVsbG86c3RhY2tvdmVyZmxvdw ==”。

我不关心如何处理身份验证,401加WWW-Authenticate响应头,JDBC查找或类似的东西。我的过滤器效果很好。

我担心的是我们永远不应该在java.lang.String中存储用户密码,因为它们是不可变的。一旦我完成身份验证,我就无法将String归零。该对象将驻留在内存中,直到垃圾收集器运行。这为一个坏人提供了一个更宽的窗口,可以获得核心转储,或以其他方式观察堆。

问题是我看到的唯一方法是通过javax.servlet.http.HttpServletRequest.getHeader(String)方法读取Authorization标头,但它返回一个String。我需要一个getHeader方法,它返回一个字节或字符数组。理想情况下,请求永远不应该是任何时间点的字符串,从Socket到HttpServletRequest以及它们之间的任何地方。

如果我转向某种基于表单的安全性,问题仍然存在。 javax.servlet.ServletRequest.getParameter(String)也返回一个字符串。

这仅仅是Java EE的限制吗?

3 个答案:

答案 0 :(得分:4)

实际上只有字符串文字保留在Permgen的String Pool区域中。创建的字符串是一次性用品。

所以......可能内存转储是基本身份验证的一个小问题。其他人是:

  • 密码以明文形式通过网络发送。
  • 为每个请求重复发送密码。 (更大的攻击窗口)
  • 密码由webbrowser缓存,至少为窗口/进程的长度。 (可以通过服务器的任何其他请求静默重用,例如CSRF)。
  • 如果用户请求,密码可以永久存储在浏览器中。 (与上一点相同,另外可能被共享计算机上的其他用户窃取)。
  • 即使使用SSL,内部服务器(SSL协议后面)也可以访问纯文本可缓存密码。

同时,Java容器已经解析了HTTP请求并填充了对象。所以,这就是你从请求标题中获取String的原因。您可能应该重写Web容器来解析安全HTTP请求。

更新

我错了。至少对Apache Tomcat来说。

http://alvinalexander.com/java/jwarehouse/apache-tomcat-6.0.16/java/org/apache/catalina/authenticator/BasicAuthenticator.java.shtml

如何看,Tomcat项目的BasicAuthenticator使用MessageBytes(即避免使用String)来执行身份验证。

/**
 * Authenticate the user making this request, based on the specified
 * login configuration.  Return <code>true if any specified
 * constraint has been satisfied, or <code>false if we have
 * created a response challenge already.
 *
 * @param request Request we are processing
 * @param response Response we are creating
 * @param config    Login configuration describing how authentication
 *              should be performed
 *
 * @exception IOException if an input/output error occurs
 */
public boolean authenticate(Request request,
                            Response response,
                            LoginConfig config)
    throws IOException {

    // Have we already authenticated someone?
    Principal principal = request.getUserPrincipal();
    String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE);
    if (principal != null) {
        if (log.isDebugEnabled())
            log.debug("Already authenticated '" + principal.getName() + "'");
        // Associate the session with any existing SSO session
        if (ssoId != null)
            associate(ssoId, request.getSessionInternal(true));
        return (true);
    }

    // Is there an SSO session against which we can try to reauthenticate?
    if (ssoId != null) {
        if (log.isDebugEnabled())
            log.debug("SSO Id " + ssoId + " set; attempting " +
                      "reauthentication");
        /* Try to reauthenticate using data cached by SSO.  If this fails,
           either the original SSO logon was of DIGEST or SSL (which
           we can't reauthenticate ourselves because there is no
           cached username and password), or the realm denied
           the user's reauthentication for some reason.
           In either case we have to prompt the user for a logon */
        if (reauthenticateFromSSO(ssoId, request))
            return true;
    }

    // Validate any credentials already included with this request
    String username = null;
    String password = null;

    MessageBytes authorization = 
        request.getCoyoteRequest().getMimeHeaders()
        .getValue("authorization");

    if (authorization != null) {
        authorization.toBytes();
        ByteChunk authorizationBC = authorization.getByteChunk();
        if (authorizationBC.startsWithIgnoreCase("basic ", 0)) {
            authorizationBC.setOffset(authorizationBC.getOffset() + 6);
            // FIXME: Add trimming
            // authorizationBC.trim();

            CharChunk authorizationCC = authorization.getCharChunk();
            Base64.decode(authorizationBC, authorizationCC);

            // Get username and password
            int colon = authorizationCC.indexOf(':');
            if (colon < 0) {
                username = authorizationCC.toString();
            } else {
                char[] buf = authorizationCC.getBuffer();
                username = new String(buf, 0, colon);
                password = new String(buf, colon + 1, 
                        authorizationCC.getEnd() - colon - 1);
            }

            authorizationBC.setOffset(authorizationBC.getOffset() - 6);
        }

        principal = context.getRealm().authenticate(username, password);
        if (principal != null) {
            register(request, response, principal, Constants.BASIC_METHOD,
                     username, password);
            return (true);
        }
    }


    // Send an "unauthorized" response and an appropriate challenge
    MessageBytes authenticate = 
        response.getCoyoteResponse().getMimeHeaders()
        .addValue(AUTHENTICATE_BYTES, 0, AUTHENTICATE_BYTES.length);
    CharChunk authenticateCC = authenticate.getCharChunk();
    authenticateCC.append("Basic realm=\"");
    if (config.getRealmName() == null) {
        authenticateCC.append(request.getServerName());
        authenticateCC.append(':');
        authenticateCC.append(Integer.toString(request.getServerPort()));
    } else {
        authenticateCC.append(config.getRealmName());
    }
    authenticateCC.append('\"');        
    authenticate.toChars();
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    //response.flushBuffer();
    return (false);

}

只要您有权访问org.apache.catalina.connector.Request,就不用担心了。

那么,如何避免解析HTTP请求

在stackoverflow详细说明中有一个惊人的答案

Use servlet filter to remove a form parameter from posted data

和一个重要的解释:

  

方法

     

代码遵循正确的方法:

     

在wrapRequest()中,它实例化HttpServletRequestWrapper并覆盖触发请求解析的4个方法:

     

public String getParameter(String name)   公共地图getParameterMap()   public Enumeration getParameterNames()   public String [] getParameterValues(String name)   doFilter()方法使用包装请求调用过滤器链,这意味着后续过滤器以及目标servlet(URL映射)将被提供包装请求。

答案 1 :(得分:0)

多数民众赞成正确,但它绝不应该在db中存储要检查的实际密码,而是密码本身的哈希值然后运行哈希以确定两个哈希值是否相同,密码从未使用过但是原始用户使用。

答案 2 :(得分:0)

如果您有疑问,请在ServletRequest.getInputStream()中使用HttpServletRequest.getHeader(String)代替Filter。您应该能够以流形式获取HTTP请求,直到获得Authorization标题并在char []中获取密码。

但是所有这些努力可能都是徒劳的,因为底层对象仍然是HTTPServletRequest并且可能包含所有标头作为映射中的键值对,细节受servlet实现的影响。