会话固定
企业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