V úvodu do PrimeFaces a JSF 2 jsem se zmínil, že používáte-li Spring Security, nepotřebujete managed beanu pro login, stačí jen submit javascriptem (POST request na url /j_spring_security_check). Mnohem zapeklitější je ošetření ajaxu nad vypršenou HTTP session. Nejspíš si říkáte, žádná věda. Spring Security mě prostě přesměruje na login. Taky že ano, ale v případě ajaxu dostanete jako partial response právě onen login formulář. View ho není schopné zpracovat a uživatel je zmaten, protože se nic neděje. Jak to vyřešit?

Klíčovým bodem je implementace rozhraní InvalidSessionStrategy. Začneme tím jednodušším, pro neajaxový request zavoláme HttpServletResponse#sendRedirect(String). A jak vůbec ajaxový request rozlišit? V HTTP hlavičce faces-request dostanete hodnotu partial/ajax. Pak už stačí vytvořit xml odpověď s direktivou pro přesměrování.

Zbývá vyřešit několik drobností. Především ve web descriptoru musí být zaregistrován HttpSessionEventPublisher, abyste vůbec dostávali eventy o vypršené session. Naši implementaci InvalidSessionStrategy injektujeme do filteru SessionManagementFilter, který zaregistrujeme na pozici

before="SESSION_MANAGEMENT_FILTER"

Problém je, že jako vedlejší efekt samotného Ajax POSTu, se InvalidSessionStrategy volá na GET requesty způsobené nějakými obrázky (přestože je to cache hit - HTTP status 304 Not Modified). Stačí neaplikovat zabezpečení na resources (css, javascript a obrázky).

<sec:http pattern="/javax.faces.resource/**" security="none"/>

Provedete-li GET request a nejste-li autentizovaní, jste přesměrováni na login. Po úspěšném přihlášení jste ovšem opět přesměrováni, tentokrát na stránku, kterou jste původně žádali. To je standardní chování. Naše implementace má však potíž se zacyklením. Sice jste přesměrováni ale se stále stejnou, vypršenou, session. Vytvoříme tedy novou.

request.getSession(true);

S ajaxovým POSTem je to složitější. Je potřeba uživatele informovat, že došlo k vypršení session s možností prokliku na view, kde byl naposled, ačkoliv původní kontext je ztracen. View dostaneme z referera: #{header[‘referer’]}

Zde je kompletní konfigurace.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.web.session.InvalidSessionStrategy;
import org.springframework.util.StringUtils;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Inspired by <a href="http://stackoverflow.com/questions/10143539/jsf-2-spring-security-3-x-and-richfaces-4-redirect-to-login-page-on-session-tim">StackOverflow.com</a>
* and by <a href="http://www.icesoft.org/wiki/display/ICE/Spring+Security#SpringSecurity-Step4%3AConfigureYourSpringSecurityredirectStrategy">Spring Security 3 and ICEfaces 3 Tutorial</a>.
*
* @author banterCZ
*/
public class JsfRedirectStrategy implements InvalidSessionStrategy {
private Logger logger = LoggerFactory.getLogger(getClass());
private static final String FACES_REQUEST_HEADER = "faces-request";
private String invalidSessionUrl;
/**
* {@inheritDoc}
*/
@Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
boolean ajaxRedirect = "partial/ajax".equals(request.getHeader(FACES_REQUEST_HEADER));
if(ajaxRedirect) {
String contextPath = request.getContextPath();
String redirectUrl = contextPath + invalidSessionUrl;
logger.debug("Session expired due to ajax request, redirecting to '{}'", redirectUrl);
String ajaxRedirectXml = createAjaxRedirectXml(redirectUrl);
logger.debug("Ajax partial response to redirect: {}", ajaxRedirectXml);
response.setContentType("text/xml");
response.getWriter().write(ajaxRedirectXml);
} else {
String requestURI = getRequestUrl(request);
logger.debug("Session expired due to non-ajax request, starting a new session and redirect to requested url '{}'", requestURI);
request.getSession(true);
response.sendRedirect(requestURI);
}
}
private String getRequestUrl(HttpServletRequest request) {
StringBuffer requestURL = request.getRequestURL();
String queryString = request.getQueryString();
if (StringUtils.hasText(queryString)) {
requestURL.append("?").append(queryString);
}
return requestURL.toString();
}
private String createAjaxRedirectXml(String redirectUrl) {
return new StringBuilder()
.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
.append("<partial-response><redirect url=\"")
.append(redirectUrl)
.append("\"></redirect></partial-response>")
.toString();
}
public void setInvalidSessionUrl(String invalidSessionUrl) {
this.invalidSessionUrl = invalidSessionUrl;
}
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sec="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
<sec:http pattern="/pages/public/**" security="none"/>
<sec:http pattern="/javax.faces.resource/**" security="none"/>
<sec:http use-expressions="true" >
<sec:custom-filter ref="sessionManagementFilter" before="SESSION_MANAGEMENT_FILTER" />
<sec:intercept-url pattern="/pages/protected/**" access="isAuthenticated()" />
<sec:form-login login-page='/pages/public/login.xhtml' default-target-url="/pages/protected/index.xhtml"
authentication-failure-url="/pages/public/login.xhtml?failure=true" />
<sec:logout logout-success-url="/pages/public/login.xhtml"/>
</sec:http>
<sec:authentication-manager>
<sec:authentication-provider>
<sec:user-service>
<sec:user name="joe" password="password" authorities="ROLE_USER"/>
</sec:user-service>
</sec:authentication-provider>
</sec:authentication-manager>
<bean id="sessionManagementFilter" class="org.springframework.security.web.session.SessionManagementFilter">
<constructor-arg name="securityContextRepository" ref="httpSessionSecurityContextRepository" />
<property name="invalidSessionStrategy" ref="jsfRedirectStrategy" />
</bean>
<bean id="jsfRedirectStrategy" class="JsfRedirectStrategy">
<property name="invalidSessionUrl" value="/pages/public/error/viewExpired.xhtml" />
</bean>
<bean id="httpSessionSecurityContextRepository" class="org.springframework.security.web.context.HttpSessionSecurityContextRepository"/>
</beans>
view raw security.xml hosted with ❤ by GitHub
<ui:composition template="/WEB-INF/templates/public_layout.xhtml"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
>
<ui:param name="title" value="#{msg['error.viewExpired.title']}" />
<ui:define name="content">
#{msg['error.viewExpired.text']}
<h:outputLink value="#{header['referer']}">#{msg['link.back']}</h:outputLink>
</ui:define>
</ui:composition>
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:security.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.xhtml</url-pattern>
</servlet-mapping>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
view raw web.xml hosted with ❤ by GitHub