Статьи

Интеграция GWT с Spring Security


Вчера я писал о том,
как сделать междоменный GWT RPC с ProxyServlet . Сегодня я расскажу, как изменить ProxyServlet для аутентификации в Spring Security. Для приложения, над которым я работаю, ProxyServlet используется только в разработке (при работе в режиме хоста GWT) и не требуется при развертывании клиента и сервера на одном сервере. Использование ProxyServlet разрешает междоменные запросы, поэтому вы можете запускать GWT в размещенном режиме и общаться со своим бэкэндом, работающим на другом сервере. Эта настройка может быть особенно удобна тем, что вы можете легко указать свой размещенный клиент на разных бэкэндах (например, если у вас есть тестирование и промежуточные среды).

В этом примере внутренним приложением является приложение JSF / Spring, в котором Spring Security подключен для защиты служб как с базовой, так и с проверкой подлинности на основе форм. Обычная аутентификация срабатывает, если отправляется заголовок «Авторизация», иначе используется аутентификация на основе форм. Вот контекстный файл Spring Security, который делает это:

<?xml version="1.0" encoding="UTF-8"?>

<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="...">

<http auto-config="true" realm="My Web Application">
<intercept-url pattern="/faces/welcome.jspx" access="ROLE_USER"/>
<intercept-url pattern="/*.rpc" access="ROLE_USER"/>
<http-basic/>
<form-login login-page="/faces/login.jspx" authentication-failure-url="/faces/accessDenied.jspx"
login-processing-url="/j_spring_security_check" default-target-url="/redirect.jsp"
always-use-default-target="true"/>
</http>

<authentication-provider>
<user-service >
<user name="admin" password="admin" authorities="ROLE_USER"/>
</user-service>
</authentication-provider>
</beans:beans>

Самый простой способ настроить приложение GWT для взаимодействия с защищенным ресурсом Spring Security — это защитить HTML-страницу, в которую встроен GWT . Это документированный способ интеграции GWT с Spring Security (ссылка: LoginSecurityFAQ GWT , поиск «Acegi»). Это хорошо работает для производства, но не для разработки в режиме хоста.

Базовая аутентификация
Для аутентификации с помощью базовой аутентификации вы можете использовать RequestBuilder от GWT и установить заголовок «Аутентификация», который содержит учетные данные пользователя (в кодировке base64).

private class LoginRequest {
public LoginRequest(RequestCallback callback) {
String url = "/services/faces/welcome.jspx";

RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, url);
rb.setHeader("Authorization", createBasicAuthToken());
rb.setCallback(callback);
try {
rb.send();
} catch (RequestException e) {
Window.alert(e.getMessage());
}
}
}

protected String createBasicAuthToken() {
byte[] bytes = stringToBytes(username.getValue() + ":" + password.getValue());
String token = Base64.encode(bytes);
return "Basic " + token;
}

protected byte[] stringToBytes(String msg) {
int len = msg.length();
byte[] bytes = new byte[len];
for (int i = 0; i < len; i++)
bytes[i] = (byte) (msg.charAt(i) & 0xff);
return bytes;
}

Чтобы использовать этот класс LoginRequest, создайте его с обратным вызовом и найдите код ответа 401, чтобы определить, не прошла ли аутентификация.

new LoginRequest(new RequestCallback() {
public void onResponseReceived(Request request, Response response) {
if (response.getStatusCode() != Response.SC_UNAUTHORIZED &&
response.getStatusCode() != Response.SC_OK) {
onError(request, new RequestException(response.getStatusText() + ":\n" + response.getText()));
return;
}

if (response.getStatusCode() == Response.SC_UNAUTHORIZED) {
Window.alert("You have entered an incorrect username or password. Please try again.");
} else {
// authentication worked, show a fancy dashboard screen
}
}

public void onError(Request request, Throwable throwable) {
Window.alert(throwable.getMessage());
}
});

Если ваше приложение GWT включено в «сервисную» войну, все должно работать на этом этапе. Однако, если вы попытаетесь войти с неверными учетными данными, откроется диалоговое окно входа в браузер. Чтобы подавить это в вышеупомянутом ProxyServlet, вам нужно внести изменения в его метод executeProxyRequest (), чтобы заголовок «WWW-Authenticate» не копировался.

// Pass the response code back to the client
httpServletResponse.setStatus(intProxyResponseCode);

// Pass response headers back to the client
Header[] headerArrayResponse = httpMethodProxyRequest.getResponseHeaders();
for (Header header : headerArrayResponse) {
if (header.getName().equals("Transfer-Encoding") && header.getValue().equals("chunked") ||
header.getName().equals("Content-Encoding") && header.getValue().equals("gzip") ||
header.getName().equals("WWW-Authenticate")) { // don't copy WWW-Authenticate header
} else {
httpServletResponse.setHeader(header.getName(), header.getValue());
}
}

Я не уверен, как подавить запрос браузера, когда не используется ProxyServlet. Если у вас есть решение, пожалуйста, дайте мне знать .

Базовая аутентификация хорошо работает для приложений GWT, потому что вам не нужна дополнительная логика для сохранения аутентифицированного состояния после первоначального входа в систему. Хотя базовая аутентификация через SSL может предложить достойное решение, недостатком является то, что вы не можете выйти из системы. Аутентификация на основе форм позволяет вам выйти из системы.

Аутентификация на основе форм

Прежде чем я покажу вам, как реализовать аутентификацию на основе форм, вы должны знать, что Google не рекомендует этого. Ниже приведено предупреждение от их LoginSecurityFAQ.

Как НЕ пытайтесь использовать заголовок Cookie для передачи SESSIONID от GWT на сервере; это чревато проблемами безопасности, которые станут понятны в оставшейся части этой статьи. Вы ДОЛЖНЫ перенести sessionID в полезную нагрузку запроса. Для примера того, почему это может потерпеть неудачу, см. CrossSiteRequestForgery.

В моем эксперименте я не хотел менять конфигурацию Spring Security на стороне сервера, поэтому проигнорировал это предупреждение. Если вы знаете, как настроить Spring Security так, чтобы он искал sessionID в полезной нагрузке запроса (а не в cookie), я хотел бы услышать об этом. Достоинством приведенного ниже примера является то, что оно должно работать и с аутентификацией, управляемой контейнером.

Класс LoginRequest для проверки подлинности на основе форм аналогичен предыдущему, за исключением того, что он имеет другой URL-адрес и отправляет учетные данные пользователя в теле запроса.

private class LoginRequest {
public LoginRequest(RequestCallback callback) {
String url = "/services/j_spring_security_check";

RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, url);
rb.setHeader("Content-Type", "application/x-www-form-urlencoded");
rb.setRequestData("j_username=" + URL.encode(username.getValue()) +
"&j_password=" + URL.encode(password.getValue()));

rb.setCallback(callback);
try {
rb.send();
} catch (RequestException e) {
Window.alert(e.getMessage());
}
}
}

Если вы развернете свое приложение GWT в той же WAR, в которой размещены ваши сервисы, это все, что вам нужно сделать. Если вы используете ProxyServlet, вам нужно внести пару изменений, чтобы установить / отправить куки при работе в размещенном режиме.

Прежде всего, вам нужно убедиться, что вы настроили сервлет на перенаправление (путем создания подклассов или просто изменив его по умолчанию). После этого добавьте следующую логику в строку 358 (или просто найдите «if (followRedirects)»), чтобы предоставить sessionID клиенту. Наиболее важной частью является установка пути к cookie для «/», чтобы клиент (работающий по адресу localhost: 8888) мог его видеть.

if (followRedirects) {
// happens on first login attempt
if (stringLocation.contains("jsessionid")) {
Cookie cookie = new Cookie("JSESSIONID",
stringLocation.substring(stringLocation.indexOf("jsessionid=") + 11));
cookie.setPath("/");
httpServletResponse.addCookie(cookie);
// the following happens if you refresh your GWT app after already logging in once
} else if (httpMethodProxyRequest.getResponseHeader("Set-Cookie") != null) {
Header header = httpMethodProxyRequest.getResponseHeader("Set-Cookie");
String[] cookieDetails = header.getValue().split(";");
String[] nameValue = cookieDetails[0].split("=");

Cookie cookie = new Cookie(nameValue[0], nameValue[1]);
cookie.setPath("/");
httpServletResponse.addCookie(cookie);
}
httpServletResponse.sendRedirect(stringLocation.replace(getProxyHostAndPort() +
this.getProxyPath(), stringMyHostName));
return;
}

Нажмите здесь, чтобы увидеть скриншот diff ProxyServlet после добавления этого кода.

Выяснить, что заголовки нужно было проанализировать после успешной аутентификации и перед перенаправлением, было самой сложной частью для меня. Если вы берете JSESSIONID из заголовка «Set-Cookie» где-либо еще, JSESSIONID — это тот, который не был аутентифицирован. Пока логин будет работать, последующие вызовы к сервисам не удастся.

Чтобы сделать последующие вызовы с файлом cookie в заголовке, вам нужно будет внести дополнительные изменения в ProxyServlet для отправки файлов cookie в качестве заголовков. Прежде всего, добавьте метод setProxyRequestCookies () :

/**
* Retrieves all of the cookies from the servlet request and sets them on
* the proxy request
*
* @param httpServletRequest The request object representing the client's
* request to the servlet engine
* @param httpMethodProxyRequest The request that we are about to send to
* the proxy host
*/
@SuppressWarnings("unchecked")
private void setProxyRequestCookies(HttpServletRequest httpServletRequest,
HttpMethod httpMethodProxyRequest) {
// Get an array of all of all the cookies sent by the client
Cookie[] cookies = httpServletRequest.getCookies();
if (cookies == null) {
return;
}

for (Cookie cookie : cookies) {
cookie.setDomain(stringProxyHost);
cookie.setPath(httpServletRequest.getServletPath());
httpMethodProxyRequest.setRequestHeader("Cookie", cookie.getName() +
"=" + cookie.getValue() + "; Path=" + cookie.getPath());
}
}

Затем в методах doGet () и doPost () добавьте следующую строку сразу после вызова setProxyRequestHeaders () .

setProxyRequestCookies(httpServletRequest, getMethodProxyRequest);

После внесения этих изменений в ProxyServlet вы можете создать LoginRequest и попытаться пройти аутентификацию. Чтобы обнаружить неудачную попытку, я ищу текст на странице «Security-fail-url» Spring Security.

new LoginRequest(new RequestCallback() {

public void onResponseReceived(Request request, Response response) {
if (response.getStatusCode() != Response.SC_OK) {
onError(request, new RequestException(response.getStatusText() + ":\n" + response.getText()));
return;
}

if (response.getText().contains("Access Denied")) {
Window.alert("You have entered an incorrect username or password. Please try again.");
} else {
// authentication worked, show a fancy dashboard screen
}
}

public void onError(Request request, Throwable throwable) {
Window.alert(throwable.getMessage());
}
});

После внесения этих изменений вы сможете пройти аутентификацию с помощью конфигурации Spring Security на основе форм. Хотя в этом примере не показано, как выйти из системы, это должно быть достаточно просто: 1) удалить cookie-файл JSESSIONID или 2) вызвать URL-адрес выхода, который вы настроили в WAR своих служб.

Надеемся, что это руководство даст вам достаточно информации, чтобы настроить приложение GWT для взаимодействия с Spring Security без изменения существующего внутреннего приложения. Вполне возможно, что Spring Security предлагает более дружественный GWT механизм аутентификации. Если вы знаете, как лучше интегрировать GWT с Spring Security, я бы хотел услышать об этом.

От  http://raibledesigns.com/rd/entry/integrating_gwt_with_spring_security