代码之家  ›  专栏  ›  技术社区  ›  bmauter

如何在Java Servlet筛选器中安全地处理密码?

  •  5
  • bmauter  · 技术社区  · 12 年前

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

    我不关心如何处理身份验证、401加WWW身份验证响应头、JDBC查找或类似的任何事情。我的过滤器工作得很好。

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

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

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

    这仅仅是Java EE的一个限制吗?

    3 回复  |  直到 12 年前
        1
  •  4
  •   Community Mohan Dere    9 年前

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

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

    • 密码以明文形式通过网络发送。
    • 每次请求都会重复发送密码。(较大的攻击窗口)
    • 密码由Web浏览器缓存,至少为窗口/进程的长度。(可以通过对服务器的任何其他请求(例如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(字符串名称) 公共映射getParameterMap() 公共枚举getParameterName() public String[]getParameterValues(字符串名称) doFilter()方法使用包装的请求调用过滤器链,这意味着后续过滤器以及目标servlet(URL映射)将被提供包装的请求。

        2
  •  0
  •   user2991535    12 年前

    这是正确的,但它不应该在数据库中存储一个实际的密码来进行检查,而是在密码本身上存储一个哈希,然后运行一个哈希来确定这两个哈希是否相同,这是一个原始用户从未使用过的密码。

        3
  •  0
  •   mzzzzb    12 年前

    如果您对此感到担忧,请使用 ServletRequest.getInputStream() 而不是 HttpServletRequest.getHeader(String) 在您的 Filter 。您应该能够以流的形式获取HTTP请求,跳到 Authorization 标头并在 char [] .

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