Статьи

Контроль доступа на уровне приложений JSF

После попытки выяснить, как это сделать в общем или, по крайней мере, легко настраивать, я написал небольшой удобный инструмент для JSF, который позволяет вам выполнять контроль безопасности, аналогично тому, как вы можете в web.xml, используя теги ограничения безопасности, но с учетом приложения Уровень аутентификации.

Хитрость заключается в том, чтобы использовать PhaseListener, прослушивая фазу RESTORE_VIEW и проверяя viewId в afterPhase (). Все запросы проходят этот жизненный цикл, включая результат действия, позволяющего выполнять фильтрацию для viewId (URL). Например, очень простой проверкой будет:

public class AccessControlPhaseListener implements PhaseListener
{
public void afterPhase(PhaseEvent event)
{
FacesContext context = event.getFacesContext();
HttpSession session = (HttpSession) context.getExternalContext().getSession(true);
SessionBean sessionBean = (SessionBean) session.getAttribute("sessionBean");
if (!sessionBean.isLoggedIn() && !"/login.xhtml".equals(context.getViewRoot().getViewId()))
context.getApplication().getNavigationHandler().handleNavigation(context, null, "login");
}
public PhaseId getPhaseId()
{
//ALL access go through RESTORE_VIEW and RENDER_VIEW (even direct url)
return PhaseId.RESTORE_VIEW;
}
}

Чтобы увидеть мою полную публикацию кода, ознакомьтесь с моей записью в блоге Контроль доступа в JSF с использованием PhaseListener . Я написал его, чтобы использовать настраиваемые фильтры URL для установки различных необходимых уровней безопасности.

package devgrok.jsf;

import static devgrok.jsf.AccessControlPhaseListener.AccessLevel.ADMIN;
import static devgrok.jsf.AccessControlPhaseListener.AccessLevel.LOGGED_IN;
import static devgrok.jsf.AccessControlPhaseListener.AccessLevel.NONE;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.servlet.http.HttpSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sun.faces.util.MessageFactory;

import devgrok.jsf.SessionForm;
import devgrok.jsf.UrlFilter;

/**
* Phase Listener that checks the viewId (URL) against a set of filters to determine the required access level. If the
* correct level is not there then redirect.
*
* See {@link UrlFilter} for details on the url matching.
*
* @author Chris Watts 2009
*
*/
public class AccessControlPhaseListener implements PhaseListener
{
/** Logger for this class */
private static final Logger log = LoggerFactory.getLogger(AccessControlPhaseListener.class);

/** */
private static final long serialVersionUID = 1L;
private final static String SESSION_BEAN = "sessionBean";
private final HashMap<AccessLevel, List<UrlFilter>> levelFilters = new HashMap<AccessLevel, List<UrlFilter>>();

public enum AccessLevel
{
NONE, LOGGED_IN, USER_ACTIVE, ADMIN;
}

/**
*
*/
public AccessControlPhaseListener()
{
initLevels();

requires(LOGGED_IN)
.include("*")
.exclude("/index.xhtml")
.exclude("/login.xhtml")
.exclude("/user/newUser.xhtml");

requires(USER_ACTIVE)
.include("/user/*")
.exclude("/user/newUser.xhtml");

requires(ADMIN)
.include("/admin/*");
}

private void initLevels()
{
AccessLevel[] levels = AccessLevel.values();
for (int i = 1; i < levels.length; i++)
{
levelFilters.put(levels[i], new ArrayList<UrlFilter>());
}
}

private UrlFilter requires(AccessLevel level)
{
//ALL is default
if (level == NONE)
return null;

UrlFilter filter = new UrlFilter();
List<UrlFilter> list = levelFilters.get(level);
list.add(filter);
return filter;
}

/*
* (non-Javadoc)
*
* @see javax.faces.event.PhaseListener#afterPhase(javax.faces.event.PhaseEvent)
*/
public void afterPhase(PhaseEvent event)
{
try
{
//check have correct access
FacesContext context = event.getFacesContext();
HttpSession session = (HttpSession) context.getExternalContext().getSession(true);
SessionForm sessionBean = (SessionForm) session.getAttribute(SESSION_BEAN);
if (sessionBean == null)
{
log.error("Could not obtain instance of sessionBean");
return;
}

//can't use this here. only valid at render response phase?
String viewId = context.getViewRoot().getViewId();
AccessLevel required = requiredLevel(viewId);
log.debug("Required level={} for viewId={}", required, viewId);

//check if page require access:
switch (required) {
case NONE:
break;
case LOGGED_IN:
if (!sessionBean.isLoggedIn())
redirectLogin(event.getFacesContext(), sessionBean);
break;
case USER_ACTIVE:
if (!sessionBean.isActive())
redirectActive(event.getFacesContext());
break;
case ADMIN:
if (!sessionBean.isAdmin())
redirectAdmin(event.getFacesContext());
break;
default:
//error
log.error("huh?");
throw new IllegalArgumentException("Not a valid access level");
}
}
catch (Exception e)
{
// TODO Auto-generated catch block
log.error("beforePhase caught exception", e);
}

}

/*
* (non-Javadoc)
*
* @see javax.faces.event.PhaseListener#beforePhase(javax.faces.event.PhaseEvent)
*/
public void beforePhase(PhaseEvent event)
{

}

private void redirectLogin(FacesContext context, SessionForm sessionForm)
{
//trigger login popup to be shown on render.
sessionForm.logIn();
addError(context, "access.loginrequired");
context.getApplication().getNavigationHandler().handleNavigation(context, null, "index");
}

private void redirectActive(FacesContext context)
{
addError(context, "access.activerequired");
context.getApplication().getNavigationHandler().handleNavigation(context, null, "userActivate");
}

private void redirectAdmin(FacesContext context)
{
addError(context, "access.adminrequired");
context.getApplication().getNavigationHandler().handleNavigation(context, null, "home");
}

/**
* Add keyed error/message.
*
* @param level
* @param key
* message key
*/
private void addError(FacesContext context, String key)
{
FacesMessage fMessage = MessageFactory.getMessage(key);
if (fMessage != null)
{
FacesContext facesContext = FacesContext.getCurrentInstance();
fMessage.setSeverity(FacesMessage.SEVERITY_ERROR);
facesContext.addMessage(null, fMessage);
}
}

/**
* Checks defined filters for view id, checks starting at the highest level down to NONE.
*
* @return the matching level or {@link AccessLevel#NONE} if none matching.
*/
private AccessLevel requiredLevel(String viewId)
{
AccessLevel[] levels = AccessLevel.values();
for (int i = levels.length - 1; i > 0; i--)
{
if (checkLevel(levels[i], viewId))
return levels[i];
}

return AccessLevel.NONE;
}

private boolean checkLevel(AccessLevel level, String viewId)
{
return matchUri(levelFilters.get(level), viewId);
}

private boolean matchUri(List<UrlFilter> list, String uri)
{
for (UrlFilter filter : list)
{
if (filter.matches(uri))
return true;
}
return false;
}

/*
* (non-Javadoc)
*
* @see javax.faces.event.PhaseListener#getPhaseId()
*/
public PhaseId getPhaseId()
{
//ALL access go through RESTORE_VIEW and RENDER_VIEW (even direct url)
return PhaseId.RESTORE_VIEW;
}

}

 

package devgrok.jsf;

import java.util.ArrayList;
import java.util.regex.Pattern;

/**
* An inclusion/exclusion filterset, similar to ant's fileset but does not support directories in the same style(**,
* etc).
*
* For example:
* <ul>
* <li>/servlet/* matches all urls starting with "/servlet/" e.g. /servlet/this.html
* <li>*.do matches all urls that end in ".do" - e.g. mypage.do
* <li>/servlet/*.do matches all urls starting with "/servlet/" and end in ".do" - e.g. /servlet/mypage.do
* </ul>
*
* @author Chris Watts 2009
*
*/
public class UrlFilter
{
private ArrayList<Pattern> include = new ArrayList<Pattern>();
private ArrayList<Pattern> exclude = new ArrayList<Pattern>();

public UrlFilter()
{

}

/**
* Include the wildcard(*) built pattern.
*
* @param pattern
* @return
*/
public UrlFilter include(String pattern)
{
include.add(generateExpression(pattern));
return this;
}

/**
* Exclude the wildcard(*) built pattern.
*
* @param pattern
* @return
*/
public UrlFilter exclude(String pattern)
{
exclude.add(generateExpression(pattern));
return this;
}

/**
* Checks to see if uri matches at least ONE inclusion filter and doesn't match ANY exclusion filters.
*
* @param uri
* @return
*/
public boolean matches(String uri)
{
boolean match = false;

//check inclusions
for (Pattern pattern : include)
{
match = match || pattern.matcher(uri).matches();
}

if (!match)
return false;

//check exclusions
for (Pattern pattern : exclude)
{
match = match && !pattern.matcher(uri).matches();
}
return match;
}

/** regular expression special character */
private static char[] specialChars = { '[', '\\', '^', '$', '.', '|', '?', '*', '+', '(', ')' };

/**
*
* @param input
* @return
*/
private static Pattern generateExpression(String input)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < input.length(); i++)
{
char letter = input.charAt(i);
if (letter == '*')
{
sb.append(".*");
}
else if (contains(specialChars, letter))
{
sb.append("\\" + letter);
}
else
{
sb.append(letter);
}
}
return Pattern.compile(sb.toString());
}

private static boolean contains(char[] array, char value)
{
if (array == null || array.length == 0)
{
return false;
}

for (int i = 0; i < array.length; i++)
{
char o = array[i];
if (o == value)
{
return true;
}
}

return false;
}
}