繁体   English   中英

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

[英]How do I securely handle passwords in a Java Servlet Filter?

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

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

我担心的是,我们永远不要将用户密码存储在java.lang.String中,因为它们是不可变的。 完成身份验证后,我无法将String归零。 该对象将一直位于内存中,直到垃圾回收器运行为止。 这就为坏人打开了一个更大的窗口,让他可以获取核心转储或以其他方式观察堆。

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

如果我改用某种形式的基于表单的安全性,那么问题仍然存在。 javax.servlet.ServletRequest.getParameter(String)返回一个String。

这仅仅是Java EE的限制吗?

实际上,只有字符串文字保留在Permgen的“字符串池”区域中。 创建的字符串是一次性的。

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

  • 密码以明文形式通过网络发送。
  • 对于每个请求,密码都会重复发送。 (较大的攻击窗口)
  • 密码由网络浏览器缓存,最小长度为窗口/进程的长度。 (可以由对服务器的任何其他请求以静默方式重用,例如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详细信息中有一个了不起的答案

使用Servlet过滤器从发布的数据中删除表单参数

还有一个重要的解释:

方法

该代码遵循正确的方法:

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

public String getParameter(String name)public Map getParameterMap()public Enumeration getParameterNames()public String [] getParameterValues(String name)doFilter()方法使用包装的请求调用过滤器链,这意味着后续的过滤器以及目标servlet(URL) -mapped)。

没错,但是永远不要在数据库中存储要检查的实际密码,而是对密码本身进行哈希运算,然后运行哈希运算来确定两个哈希值是否相同,即从未使用过但由原始用户使用的密码。

如果您担心的话,请在Filter使用ServletRequest.getInputStream()而不是HttpServletRequest.getHeader(String) 您应该可以将HTTP请求作为流获取,跳过直到获得Authorization标头并在char []获取密码。

但是,所有这些工作可能都是徒劳的,因为基础对象仍然是HTTPServletRequest并且可能在映射中包含所有标头作为键val对,详细信息取决于servlet的实现方式。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM