Wildfly ограничение безопасности web.xml, блокирующее базовый заголовок аутентификации для методов JAX-RS с использованием ContainerRequestFilter
Разрабатываемое мной веб-приложение состоит из нескольких сервлетов, а также веб-сервисов JAX-RS. До сих пор я использовал ContainerRequestFilter для аутентификации вызовов методов REST, но теперь мне также нужно защитить сервлеты, поэтому я решил использовать web.xml для определения ограничений безопасности. Мой web.xml выглядит так:
<web-app 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"
version="3.0">
<security-constraint>
<web-resource-collection>
<web-resource-name>rest</web-resource-name>
<url-pattern>/rest/*</url-pattern>
</web-resource-collection>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>protected</web-resource-name>
<url-pattern>/protected/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
<security-role>
<role-name>admin</role-name>
</security-role>
<security-role>
<role-name>user</role-name>
</security-role>
<!-- Configure login to be HTTP Basic -->
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>Restricted Zone</realm-name>
</login-config>
</web-app>
Если я правильно понимаю синтаксис web.xml, то, что я определил, означает, что доступ к /rest/* (где используются все мои методы JAX-RS) неограничен в том, что касается модулей LoginModules, и весь доступ к /protected/* путь (где я храню свои защищенные сервлеты) требует базовой авторизации.
Когда я пытаюсь открыть один из безопасных сервлетов, например, / protected / test, в браузере появляется диалоговое окно основного входа авторизации, и поведение корректное - если я ввожу учетные данные для пользователя 'admin', мне будет разрешен доступ. В противном случае я получаю сообщение "Запрещено".
Кроме того, когда я пытаюсь получить доступ к чему-либо в / rest / path, я не получаю основного диалога аутентификации, чего я и ожидал. Однако заголовок Authorization, который я получаю в ContainerRequestFilter, - это не тот, который я отправляю в запросе REST, а тот, который я использовал ранее, чтобы попасть в /protected/ servlet.
Ниже приведены другие части головоломки:
standalone.xml (раздел защищенных доменов)
<security-domain name="PaloSecurityDomain" cache-type="default">
<authentication>
<login-module code="com.palo.security.PaloLoginModule" flag="required"/>
</authentication>
</security-domain>
JBoss-web.xml
<?xml version="1.0" encoding="UTF-8"?>
<jboss-web>
<security-domain>PaloSecurityDomain</security-domain>
</jboss-web>
PaloLoginModule.java
package com.palo.security;
import java.security.acl.Group;
import java.util.Set;
import javax.inject.Inject;
import javax.naming.NamingException;
import javax.security.auth.login.LoginException;
import org.apache.log4j.Logger;
import org.jboss.security.SimpleGroup;
import org.jboss.security.SimplePrincipal;
import org.jboss.security.auth.spi.UsernamePasswordLoginModule;
import com.palo.PaloRealmRole;
import com.palo.model.PaloRealmUser;
import com.palo.utils.CdiHelper;
import com.palo.utils.PasswordHandler;
public class PaloRealmLoginModule extends UsernamePasswordLoginModule {
private static Logger logger = Logger
.getLogger(PaloRealmLoginModule.class);
@Inject
private PaloRealmLogic realmLogic;
@Override
protected String getUsersPassword() throws LoginException {
if (null == realmLogic) {
try {
CdiHelper.programmaticInjection(PaloRealmLoginModule.class,
this);
} catch (NamingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
logger.debug("Getting password for user " + super.getUsername());
PaloRealmUser user = realmLogic.getUserByName(super.getUsername());
if (null == user) {
logger.error("User not found");
throw new LoginException("User " + super.getUsername()
+ " not found");
}
logger.debug("Found " + user.getPassword());
return user.getPassword();
}
@Override
protected Group[] getRoleSets() throws LoginException {
logger.debug("Getting roles for user " + super.getUsername());
if (null == realmLogic) {
try {
CdiHelper.programmaticInjection(PaloRealmLoginModule.class,
this);
} catch (NamingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
PaloRealmUser user = realmLogic.getUserByName(super.getUsername());
if (null == user) {
throw new LoginException("User " + super.getUsername()
+ " not found");
}
Set<PaloRealmRole> roles = user.getRoles();
Group[] groups = { new SimpleGroup("Roles") };
for (PaloRealmRole role : roles) {
logger.debug("Found role " + role.getRole());
SimplePrincipal prole = new SimplePrincipal(role.getRole());
groups[0].addMember(prole);
}
return groups;
}
@Override
protected boolean validatePassword(String inputPassword,
String expectedPassword) {
logger.debug("Validating password " + inputPassword + "|"
+ expectedPassword);
return PasswordHandler.getInstance().verifyPassword(inputPassword,
expectedPassword);
}
}
SecurityInterceptor.java
package com.palo.web.rest;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.StringTokenizer;
import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.inject.Inject;
import javax.json.JsonObjectBuilder;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
import org.apache.log4j.Logger;
import org.jboss.resteasy.annotations.interception.ServerInterceptor;
import org.jboss.resteasy.core.Headers;
import org.jboss.resteasy.core.ResourceMethodInvoker;
import org.jboss.resteasy.core.ServerResponse;
import com.palo.analytics.GoogleAnalyticsEvent;
import com.palo.logic.UserLogic;
import com.palo.web.utils.HttpUtils;
@Provider
@ServerInterceptor
public class SecurityInterceptor implements ContainerRequestFilter {
private static Logger logger = Logger.getLogger(SecurityInterceptor.class);
private static final String AUTHORIZATION_PROPERTY = "Authorization";
private static final ServerResponse ACCESS_DENIED = new ServerResponse(
"Access denied for this resource", 401, new Headers<Object>());
private static final ServerResponse ACCESS_DENIED_FOR_USER = new ServerResponse(
"User not authorized", 401, new Headers<Object>());
private static final ServerResponse ACCESS_FORBIDDEN = new ServerResponse(
"Nobody can access this resource", 403, new Headers<Object>());
@Inject
private UserLogic ul;
@Override
/**
* The request filter is called automatically called for each incoming request. It checks which method is being called by the client and, based on that method's annotations, restricts access, verifies the identity of the caller, checks the validity of the session token, etc.
*/
public void filter(ContainerRequestContext requestContext)
throws IOException {
logger.debug("------------- request filter ------------");
ResourceMethodInvoker methodInvoker = (ResourceMethodInvoker) requestContext
.getProperty("org.jboss.resteasy.core.ResourceMethodInvoker");
Method method = methodInvoker.getMethod();
String methodName = method.getName();
String uri = requestContext.getUriInfo().getPath();
logger.debug("Accessing method " + methodName + " via URI " + uri);
for (String str : requestContext.getPropertyNames()) {
logger.debug(str);
}
// Get request headers
final MultivaluedMap<String, String> headers = requestContext
.getHeaders();
for (String key : headers.keySet()) {
for (String value : headers.get(key)) {
logger.debug(key + " - " + value);
}
}
// Access allowed for all
if (method.isAnnotationPresent(PermitAll.class)) {
return;
}
// Access denied for all
if (method.isAnnotationPresent(DenyAll.class)) {
requestContext.abortWith(ACCESS_FORBIDDEN);
return;
}
// Fetch authorization header
final List<String> authorization = headers.get(AUTHORIZATION_PROPERTY);
// If no authorization information present; block access
if (null == authorization || authorization.isEmpty()) {
requestContext.abortWith(ACCESS_DENIED);
return;
}
final String username = HttpUtils.getUsernameFromAuthorizationHeader(
authorization, HttpUtils.AUTHENTICATION_SCHEME_BASIC);
final String password = HttpUtils.getPasswordFromAuthenticationHeader(
authorization, HttpUtils.AUTHENTICATION_SCHEME_BASIC);
if (null == username || null == password || username.isEmpty()
|| password.isEmpty()) {
requestContext.abortWith(ACCESS_DENIED_FOR_USER);
return;
}
boolean authenticated = ul.authenticate(username, password);
if (false == authenticated) {
requestContext.abortWith(ACCESS_DENIED);
return;
}
return;
}
}
Я использую RESTClient для Firefox для отправки запросов REST к методам JAX-RS. Поскольку я регистрирую все заголовки, я ясно вижу, что происходит с фильтром, и значение не меняется между вызовами, даже если я изменяю его в RESTClient. Более того, значение все еще там, даже если я не использую заголовок авторизации в RESTClient.
У меня вопрос, почему заголовок авторизации заблокирован, и он не перенаправляется в мой фильтр? Если я удаляю файл web.xml, я получаю правильный заголовок авторизации в ContainerRequestFilter. Есть ли способ переместить часть / rest части приложения в зону, на которую не влияет login-config в web.xml?
Любая помощь очень ценится!
1 ответ
Насколько я понимаю, если вы укажете login-config, то он будет использоваться для всех ресурсов, указанных в web-resource-collection. И / отдых / и / защищенный / в вашем случае.
Первый подход Одна вещь, которую вы могли бы сделать, это изменить модуль входа в систему, чтобы он назначал admin
роль для тех пользователей, которые предоставили действительные учетные данные и назначают anonymous
роль для тех, кто не предоставил действительные полномочия. Тогда вы можете изменить свой web.xml следующим образом
<security-constraint>
<web-resource-collection>
<web-resource-name>rest</web-resource-name>
<url-pattern>/rest/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>anonymous</role-name>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>protected</web-resource-name>
<url-pattern>/protected/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
Второй подход вместо изменения модуля входа в систему - это добавление еще одного модуля входа в домен безопасности, который назначит anonymous
роль каждому
Третий подход Использование механизма пользовательской аутентификации http://undertow.io/documentation/core/security.html Механизм аутентификации BASIC предполагает, что пользователь отправляет учетные данные в заголовке http в формате Authorization: Basic: base64encodedCredentials
При использовании настраиваемого механизма аутентификации у вас есть доступ к пути запроса, и вы можете заставить свой настраиваемый механизм аутентификации пропустить вызов к модулям входа в систему, если запрос сделан по пути, который вы не хотите защищать. Но я не думаю, что это хороший подход, так как такие решения должны приниматься с помощью модулей входа в систему +web.xml.
Четвертый подход (не уверен, работает ли он, но, надеюсь, он работает. Ресурсы, которые не указаны в ограничениях безопасности, не проверяются модулями входа в систему. Поэтому, чтобы сделать /rest/ resource незащищенным, удалите эти строки из файла web.xml.:
<security-constraint>
<web-resource-collection>
<web-resource-name>rest</web-resource-name>
<url-pattern>/rest/*</url-pattern>
</web-resource-collection>
</security-constraint>