會話固定
企業Java應用程序最熱門的話題是安全性。 由于它具有許多不同的方面,因此我決定從一個非常簡單但經常需要的功能入手:防止會話固定。 這不是Java或JSF特有的,而是基于Web的應用程序的普遍問題。 當會話ID易于發現或猜測時,就會出現會話固定。 攻擊的主要方法是URL或響應的任何其他部分中存在會話ID。 攻擊者可以捕獲一個會話,然后將鏈接嵌入到其頁面中,誘使用戶訪問該會話并成為其會話的一部分。 然后,當用戶認證時,會話即被認證。 在這里使用Cookies只能提供一定的安全性,因為大多數情況下還通過暗示保密性丟失的方法進行設置。 大多數應用服務器會根據第一個請求生成一個新的會話ID。 認證通過后,可以再次使用。 防止這種情況的唯一方法是在成功的身份驗證請求之后發出新的隨機會話。
一般來說,這很容易做到。 轉到galleria-jsf項目并找到info.galleria.view.user.Authenticator bean。 將以下行添加到authenticate()方法的開頭:
String result = null;
ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext();// Session Fixation Prevention
HttpSession session = (HttpSession) externalContext.getSession(false);if (logger.isDebugEnabled()) {logger.debug("Session before authentication request: " + session.getId());}session.invalidate();
session = (HttpSession) externalContext.getSession(true);if (logger.isDebugEnabled()) {logger.debug("Session after authentication request: " + session.getId());}
就是這樣 第一次接觸代碼庫就很容易進行更改。 切換到軟件包信息info.galleria的調試級別FINE應該在日志文件中揭示魔術:
[#|2012-03-27T17:17:25.298+0200|FINE|glassfish3.1.2|info.galleria.view.user.Authenticator|_ThreadID=27;
_ThreadName=Thread-4;ClassName=info.galleria.view.user.Authenticator;MethodName=
authenticate;|Session before authentication request: 33b1205d7ad740631978ed211bce|#][#|2012-03-27T17:17:25.301+0200|FINE|glassfish3.1.2|info.galleria.view.user.Authenticator
|_ThreadID=27
;_ThreadName=Thread-4;ClassName=info.galleria.view.user.Authenticator;MethodName
=authenticate;|Session after authentication request: 33b1f344ad1730c69bccc35e752e|#]
如預期的那樣,我們在身份驗證請求期間更改了http會話。 您也可以使用您選擇的瀏覽器插件(在本例中為“編輯此Cookie”)進行檢查:

通過執行此操作,Galleria應用程序變得更加安全。 如果您想了解有關會話固定的更多信息,請閱讀OWASP頁面 。
防止多次登錄
下一個要求要復雜一些。 我已經看過幾次了,即使對用戶來說不方便,出于安全原因也可能是必需的。 正如您可能已經猜到的,沒有一個單獨的開關。 您必須持有會話圖,并檢查用戶是否已經登錄。 在登錄過程中應進行檢查,并顯示有意義的錯誤消息。
其中有一些棘手的部分。 第一個是,您需要一種方法來存儲應用程序的所有用戶和HttpSession信息。 第二個是,您需要一個人來照顧它。 讓我們從最新開始。
您在這里需要著名的辛格爾頓。 一個地方來存儲相關的HttpSession信息。 首先想到的是使用.getExternalContext()。getApplicationMap()。 這可能有效。 我們在此處設置的登錄限制有一些副作用。 想象一下,一個用戶沒有登錄就登錄并崩潰了他/她的瀏覽器。 他/她最終將無法重新登錄,直到進行一些清理或重新啟動應用程序為止。 因此,在HttpSessionListener中訪問它也至關重要。 鑒于事實,即JSF ExternalContext是ServletContext,我們在這里很安全。
在繼續進行有關聚類的更多討論之前。 我們將在這里構建一個非集群構造。 根據Servlet規范,上下文屬性對于創建它們的JVM是本地的。 因此,如果您在集群環境中運行此命令,將會失去保護,因為您可以在集群的每個節點上進行會話。 使該群集安全將意味著使用數據庫,ejb組件或分布式緩存。
轉到info.galleria.view.util并創建一個名為SessionConcierge的新最終類。 它需要添加和刪除會話的方法。 我們顯然需要一些東西來處理應用程序映射。 從addSession方法開始,稍后將從info.galleria.view.user.Authenticator托管Bean中調用該方法:
public static boolean addSession(HttpSession session) {String account = FacesContext.getCurrentInstance().getExternalContext().getRemoteUser();String sessionId = session.getId();if (account != null && !getApplicationMap(session).containsKey(account)) {getApplicationMap(session).put(account, sessionId);if (logger.isDebugEnabled()) {logger.debug("Added Session with ID {} for user {}", sessionId, account);}return true;} else {logger.error("Cannot add sessionId, because current logged in account is NULL or session already assigned!");return false;}}
基本上,這將檢查我們是否在這里有登錄用戶,以及該用戶是否已經分配了會話。 如果有一個用戶并且他沒有正在使用的會話,我們將把當前會話添加到該帳戶下的應用程序映射中作為鍵。 接下來一點刪除邏輯:
public static void removeSession(HttpSession session) {String sessionId = session.getId();String account = getKeyByValue(getApplicationMap(session), sessionId);if (account != null) {getApplicationMap(session).remove(account);if (logger.isDebugEnabled()) {logger.debug("Removed Session with ID {} for user {}", sessionId, account);}}}
這有點棘手。 您注意到,我使用該帳戶作為在地圖中綁定會話的密鑰。 因此,我必須花一點點技巧來反轉地圖并通過值找到鍵。 這個小魔術在這里發生:
private static <T, E> T getKeyByValue(Map<T, E> map, E value) {for (Entry<T, E> entry : map.entrySet()) {if (value.equals(entry.getValue())) {return entry.getKey();}}return null;}
做完了 一件事失蹤。 getApplicationMap(HttpSession session)方法。 這不是很神奇。 它只是試圖弄清楚我們是否需要通過FacesContext或ServletContext獲取它。 如果您感到好奇,請查看SessionConcierge源。 最后要做的是將SessionConcierge添加到Authenticator中 。 將此代碼添加到try {request.login()}中(我為您的定位添加了前兩行:
request.login(userId, new String(password));result = "/private/HomePage.xhtml?faces-redirect=true";// save sessionId to disable multiple sessions per userif (!SessionConcierge.addSession(session)) {request.logout();logger.error("User {} allready logged in with another session", userId);FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, Messages.getString("Login.AllreadyLoggedIn", locale), null);FacesContext.getCurrentInstance().addMessage(null, facesMessage);}
如果通過SessionConcierge添加HttpSession失敗,則立即注銷用戶并添加FacesMessage。 請記住將其添加到galleria-jsf \ src \ main \ resources \ resources messages.properties及其翻譯中。 并且不要忘記添加
SessionConcierge.removeSession(session);
到公共String logout()。 精細。 就是這樣,不是嗎? 至少它現在正在工作。 但是我們仍然必須解決那些崩潰的瀏覽器問題。 如果某人未通過該應用程序注銷,會話超時或瀏覽器崩潰,則在重新啟動該應用程序之前,您將無法再次登錄。 那是不可思議的。 需要某種清理機制。 HttpSessionListener呢? 聽起來不錯! 將其添加到info.galleria.listeners中,并將其命名為SessionExpirationListener 。
@Overridepublic void sessionDestroyed(HttpSessionEvent se) {HttpSession session = se.getSession();SessionConcierge.removeSession(session);if (logger.isDebugEnabled()) {logger.debug("Session with ID {} destroyed", session.getId());}}
精細。 現在應該可以了。 繼續嘗試一下。 打開兩個不同的瀏覽器,然后嘗試同時登錄。 只有一個可以讓您訪問該應用程序。 第二個應該以您放入messages.properties中的錯誤消息作為響應。 請注意,這不是多窗口預防措施。 您仍然可以根據需要自由地為每個HttpSession打開盡可能多的窗口。
一個小的補充:如果您嚴重依賴HttpSessionListener清理,則應確保它具有正確的生存期。 通過產品特定的Web應用程序部署描述符(例如weblogic.xml或glassfish-web.xml)進行配置。 我建議將其設置為合理的較低值(例如30分鐘或更短),以免用戶等待太長時間。 這是Glassfish(glassfish-web.xml)的外觀:
<session-config><session-properties><property name="timeoutSeconds" value="1800" /></session-properties></session-config>
和用于WebLogic(weblogic.xml)
<session-descriptor><timeout-secs>180</timeout-secs></session-descriptor>
Galleria Java EE 6示例應用程序正在增長。 今天,我將寫關于如何優雅地處理錯誤的文章。 關于用戶輸入驗證,已經做了很多工作,但是仍然有很多失敗情況沒有得到解決,應該解決。 如果您對過去發生的事情感到好奇,請查看本系列的第一部分: 基礎知識 , 在GlassFish上 運行,在WebLogic 上 運行 , 測試和增強安全性 。
通用異常機制
應用程序使用檢查的異常在層之間傳遞錯誤。 ApplicationException是所有可能的業務異常的根源。

這些業務異常在域和表示層之間傳達驗證沖突和所有已知錯誤。 galleria-jsf視圖項目中的<domain> Manager(例如AlbumManger)類將其捕獲,并使用ExceptionPrecessor將錯誤消息填充到視圖中。 在這兩層之間可能發生的另一種異常是RuntimeExceptions。 那些被容器包裝到EJBException中,并且還被<domain> Manager類捕獲。 這些會生成更一般的錯誤消息,并顯示給用戶。

在這里,我不會涉及檢查與未檢查的異常(如果您好奇的話,Google會介紹一下 )。 當應用程序有機會從錯誤中恢復時,我傾向于使用檢查異常。 當某些事情無法恢復時,將引發未經檢查的檢查。 這就是原因,我對目前內置的異常處理機制不滿意。 我稍后再討論。
有什么不見了? ViewExpired等。
似乎現在一切都已處理。 但只有第一印象。 打開登錄屏幕,稍等片刻,讓您的http會話超時。 現在,您會看到一個不太漂亮的ViewExpired異常屏幕。

如果您以登錄用戶的身份嘗試登錄,則只需將其重定向到登錄頁面。 無論如何,對于表示層中的一些其他意外情況,可能會出現相同的錯誤頁面。 因此,讓我們修復此問題。 最明顯的事情是簡單地引入專用的錯誤頁面。
<error-page><exception-type>javax.faces.application.ViewExpiredException</exception-type><location>/viewExpired.xhtml</location></error-page>
現在,您將用戶重定向到專用頁面,該頁面可以告訴他/她有關工作場所安全性的一些知識,并且不會使應用長時間處于無人看管狀態。 這適用于大多數應用程序。 如果您愿意在頁面上獲得一些其他信息,或者只是想捕獲多個異常并單獨處理它們而不必靜態配置它們,則需要一種稱為ExceptionHandler的東西。 這是JSF 2中的新功能,您所需要做的就是實現ExceptionHandler,并且它是工廠。 工廠本身在facex-config.xml中配置,因為沒有任何注釋。
打開faces-config.xml,并在底部添加以下幾行:
<factory><exception-handler-factory>info.galleria.handlers.GalleriaExceptionHandlerFactory</exception-handler-factory></factory>
現在,我們將在專用包中實現GalleriaExceptionHandlerFactory 。 有趣的方法是:
@Overridepublic ExceptionHandler getExceptionHandler() {ExceptionHandler result = parent.getExceptionHandler();result = new GalleriaExceptionHandler(result);return result;}
每個請求調用一次,每次調用必須返回一個新的ExceptionHandler實例。 在這里,真正的ExceptionHandlerFactory被調用并被要求創建實例,然后將該實例包裝在自定義的GalleriaExceptionHandler類中。 這是真正有趣的事情發生的地方。
@Overridepublic void handle() throws FacesException {for (Iterator<ExceptionQueuedEvent> i = getUnhandledExceptionQueuedEvents().iterator(); i.hasNext();) {ExceptionQueuedEvent event = i.next();ExceptionQueuedEventContext context = (ExceptionQueuedEventContext) event.getSource();Throwable t = context.getException();if (t instanceof ViewExpiredException) {ViewExpiredException vee = (ViewExpiredException) t;FacesContext fc = FacesContext.getCurrentInstance();Map<String, Object> requestMap = fc.getExternalContext().getRequestMap();NavigationHandler nav =fc.getApplication().getNavigationHandler();try {// Push some stuff to the request scope for later use in the pagerequestMap.put("currentViewId", vee.getViewId());nav.handleNavigation(fc, null, "viewExpired");fc.renderResponse();} finally {i.remove();}}}// Let the parent handle all the remaining queued exception events.getWrapped().handle();}
使用從getUnhandledExceptionQueuedEvents()。iterator()返回的迭代器迭代非處理程序異常。 ExeceptionQueuedEvent是一個SystemEvent,您可以從中獲取實際的ViewExpiredException。 最后,您從異常中提取了一些其他信息,并將其放在請求范圍內,以便稍后通過頁面中的EL進行訪問。 ViewExpiredException要做的最后一件事是使用JSF隱式導航系統(“ viewExpired”解析為“ viewExpired.xhtml”)并通過NavigationHandler導航至“ viewExpired”頁面。 不要忘記在finally塊中刪除已處理的異常。 您不希望父異常處理程序再次處理此問題。 現在,我們必須創建viewExpired.xhtml頁面。 在galleria-jsf \ src \ main \ webapp文件夾中執行此操作。
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns:ui="http://java.sun.com/jsf/facelets"template="./templates/defaultLayout.xhtml"xmlns:f="http://java.sun.com/jsf/core"xmlns:h="http://java.sun.com/jsf/html"><ui:define name="title"><h:outputText value="#{msg['Exception.page.title']}" /></ui:define><ui:define name="content"><h:form><h:outputText value="#{msg['Exception.page.message']}" /><p>You were on page #{currentViewId}. Maybe that's useful.</p><p>Please re-login via the <h:outputLink styleClass="homepagelink" value="#{request.contextPath}/Index.xhtml" ><h:outputText value="Homepage" /></h:outputLink>.</p></h:form></ui:define>
</ui:composition>
請注意,我在此處添加了新的消息屬性,因此您需要確保將它們放在galleria-jsf \ src \ main \ resources \ resources \ messages.properties和翻譯中。
到目前為止,這顯然只處理一種特殊的異常實例。 您可以將其擴展為也可以處理其他內容。 現在我們已經有了基本的機制,您可以自由地執行此操作。

重構RuntimeException處理
如我所說,我對應用程序處理RuntimeExceptions的方式不滿意。 現在我們已經有了一個很好的中央異常處理,我們可以將這些內容稍微移動一下并重構* Manager類。 從所有它們中刪除所有這些catch(EJBException ejbEx){塊。 我們將在一分鐘內在GalleriaExceptionHandler中進行處理。 只需將另一個檢查添加到GalleriaExceptionHandler即可,如果引發了ViewExpiredException以外的任何其他異常,則將用戶重定向到另一個頁面。
// check for known Exceptionsif (t instanceof ViewExpiredException) {ViewExpiredException vee = (ViewExpiredException) t;// Push some stuff to the request scope for later use in the pagerequestMap.put("currentViewId", vee.getViewId());} else {forwardView = "generalError";Locale locale = fc.getViewRoot().getLocale();String key = "Excepetion.GeneralError";logger.error(Messages.getLoggerString(key), t);String message = Messages.getString(key, locale);FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, message, null);fc.addMessage(null, facesMessage);}
這種方法具有一些優點。 它減少了* Manager類中所需的代碼,并且我們終于有了一個中心位置來處理那些不可恢復的異常。 這還是不是很像企業。 想象一下,您的第一級支持團隊需要照顧客戶,他們開始抱怨他們收到的唯一消息是“ GeneralError”。 那不是很有幫助。 您的支持團隊將需要升級它,第二或第三級需要檢查日志和and and ..所有這些都是由于我們已知的錯誤。 首先要做的是找出導致錯誤的原因。 解析堆棧跟蹤并不是很大的樂趣。 特別是不是包含在EJBExceptions中以及在FacesExceptions中的RuntimeExceptions中。 感謝上帝提供Apache Commons ExceptionUtils 。 打開您的galleria-jsf pom.xml并將其添加為依賴項:
<dependency><groupId>commons-lang</groupId><artifactId>commons-lang</artifactId><version>2.6</version></dependency>
現在,您可以開始檢查根本原因:
} else {forwardView = "generalError";// no known instance try to specifyThrowable causingEx = ExceptionUtils.getRootCause(t);if (causingEx == null) {causingEx = t;}
//...logger.error(Messages.getLoggerString(key), t);requestMap.put("errorCode", errorCode);
別忘了在這里也記錄完整的堆棧跟蹤(t,不僅是causeEx)。 通常,讓用戶知道異常是一件壞事。 沒有人真正希望看到錯誤發生(因為我們討厭犯錯誤),并且在所有異常之后,堆棧跟蹤都可以泄露您不希望在屏幕上某個地方看到的敏感信息。 因此,您需要找到一種方法來顯示對用戶有意義的內容,而又不會過多披露。 那就是著名的錯誤代碼起作用的地方。 使用根本原因異常作為消息鍵,或自行決定要為此付出的努力。 它可能是一個錯誤類別的系統(數據庫,接口系統等),它們為第一級支持提供了有關導致錯誤的原因的良好提示。 從一開始我就堅持一個簡單的解決方案。 只需為每個捕獲的異常生成一個UUID并將其跟蹤到日志和UI。 以下是一個非常簡單的示例。
String errorCode = String.valueOf(Math.abs(new Date().hashCode()));
這也應該添加到消息屬性中,并且不要忘記,您還需要另一個用于generalError模板。 如果slf4j將使用與jdk日志記錄相同的消息格式,那么您只需要一個屬性..
Exception.generalError.log=General Error logged: {}.
Exception.generalError.message=A general error with id {0} occured. Please call our hotline.
將此添加到generalError.xhtml并查看如何將錯誤代碼傳遞到消息模板。
<h:outputFormat value="#{msg['Exception.generalError.message']}" ><f:param value="#{errorCode}"/></h:outputFormat>

這里還有很多需要改進的地方。 您可以使用javax.faces.application.ProjectStage查找應用程序正在運行的當前模式。如果您在ProjectStage.Development中運行,則還可以將完整的堆棧跟蹤信息放到UI上,并使調試工作變得容易一些。 以下代碼段嘗試從JNDI獲取ProjectStage。
public static boolean isProduction() {ProjectStage stage = ProjectStage.Development;String stageValue = null;try {InitialContext ctx = new InitialContext();stageValue = (String) ctx.lookup(ProjectStage.PROJECT_STAGE_JNDI_NAME);stage = ProjectStage.valueOf(stageValue);} catch (NamingException | IllegalArgumentException | NullPointerException e) {logger.error("Could not lookup JNDI object with name 'javax.faces.PROJECT_STAGE'. Using default 'production'");}return ProjectStage.Production == stage;}
那三位數的Http錯誤頁呢?
那是另一件事。 其余所有3位http錯誤代碼,這些錯誤代碼將返回看起來不太好看的錯誤頁面之一。 唯一要做的就是將它們映射到web.xml中,如下所示:
<error-page><error-code>404</error-code><location>/404.xhtml</location></error-page>
您應該確保已放置這些映射,并向用戶顯示有意義的錯誤。 始終提供一種從那里進一步導航的方法應該成為最佳實踐。
參考: Java EE 6示例–使用Galleria增強安全性–第5部分 , Java EE 6示例–優雅地處理Galleria中的錯誤–我們JCG合作伙伴 Markus Eisele在Java企業軟件開發博客上的第6部分 。
翻譯自: https://www.javacodegeeks.com/2012/04/java-ee-6-example-galleria-part-3.html