[英]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的“字符串池”區域中。 創建的字符串是一次性的。
所以...內存轉儲可能是基本身份驗證的次要問題之一。 其他是:
同時,Java容器已經解析了HTTP請求並填充了對象。 因此,這就是為什么您從請求標頭中獲取String的原因。 您可能應該重寫Web容器以解析安全HTTP請求。
我錯了。 至少對於Apache Tomcat。
可以看到,來自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,就不用擔心。
在stackoverflow詳細信息中有一個了不起的答案
還有一個重要的解釋:
方法
該代碼遵循正確的方法:
在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.