четверг, 31 января 2013 г.

Spring security в GWT-приложении

Хочется, чтобы всё делали за нас...
Хочется, чтобы нашим приложением пользовались только те, кто имеет в нём аккаунт. И ещё хочется знать, какой именно пользователь сейчас пользуется нашим приложением, чтобы предоставить ему всю относящуюся к нему информацию, и никакую другую.

Хорошая новость в том, что для java-разработчиков эти желания были решены технологией Spring Security. Плохая новость в том, что её надо грамотно интегрировать в своё приложение.

Влияние того, что приложение также будет GWT-приложением совсем небольшое, может быть процентов 10. Вообще-то это вполне независимые технологии.

Англоязычный источник находится здесь.

1. Очевидно, у нас уже есть Eclipse с установленным GWT-плагином, поэтому не надо объяснять как создать новый GWT-проект:
File > New > Web Application Project
Назовём порект: securegwt
Укажем пакет: ru.andrew.securegwt
Уберём галочку с: Use Google App engine

2. В проект надо добавить необходимые библиотеки. 7 библиотек - это собственно Spring, а 3 - это Spring security. Скачивать их скорее всего придётся по отдельности, но желательно следить, чтобы их версии расходились не слишком, а в идеальном случае вообще совпадали. Ссылки для скачивания Spring Security и Spring Framework.
Вот список требуемых библиотек:

    1. org.springframework.aop-3.1.1.RELEASE
    2. org.springframework.asm-3.1.1.RELEASE
    3. org.springframework.beans-3.1.1.RELEASE
    4. org.springframework.context-3.1.1.RELEASE
    5. org.springframework.core-3.1.1.RELEASE
    6. org.springframework.expression-3.1.1.RELEASE
    7. org.springframework.web-3.1.1.RELEASE
    8. spring-security-config-3.1.3.RELEASE
    9. spring-security-core-3.1.3.RELEASE
    10. spring-security-web-3.1.3.RELEASE

3. Редактируем /war/WEB-INF/web.xml
Декларируем фильтр, являющийся экземпляром класса FilterChainProxy, который перехватывает все входящие запросы, подходящие под шаблон URL, и делегирует управление запросом соответствующему обработчику Spring. Соответствующий кусок файла выглядит так:

01
02<filter>
03        <filter-name>springSecurityFilterChain</filter-name>
04        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
05</filter>
06
07<filter-mapping>
08      <filter-name>springSecurityFilterChain</filter-name>
09      <url-pattern>/*</url-pattern>
10</filter-mapping>
11...


Нам также нужно определить ContextLoaderListener, чтобы загрузить контекст Spring. Это делается с помощью следующего куска кода:

1
2    <listener>
3        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
4    </listener>
5...

Немного от GWT. Добавим описание авторизационного сервлета, который будет принимать и обрабатывать запросы на авторизацию:


  <servlet>
      <servlet-name>authServlet</servlet-name>
      <servlet-class>ru.andrew.securegwt.server.AuthServiceImpl</servlet-class>
  </servlet>
  <servlet-mapping>
      <servlet-name>authServlet</servlet-name>
      <url-pattern>/securegwt/auth</url-pattern>
  </servlet-mapping>


Также нужно упомянуть о загрузке файла applicationContext.xml, из которого приложение узнает, что мы вообще-то хотим пользоваться модулем Spring security. (Этот кусок кода в англоязычном оригинале почему-то не упомянут)

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
                  /WEB-INF/applicationContext.xml
            </param-value>
</context-param>

Итоговый web.xml будет выглядеть так:

<?xml version="1.0" encoding="UTF-8"?>
<web-app 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_2_5.xsd"
version="2.5" xmlns="http://java.sun.com/xml/ns/javaee">

<!-- Servlets -->
<servlet>
<servlet-name>greetServlet</servlet-name>
<servlet-class>ru.andrew.securegwt.server.GreetingServiceImpl</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>greetServlet</servlet-name>
<url-pattern>/securegwt/greet</url-pattern>
</servlet-mapping>

<!-- Default page to serve -->
<welcome-file-list>
<welcome-file>Securegwt.html</welcome-file>
</welcome-file-list>

<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>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
<servlet-name>authServlet</servlet-name>
<servlet-class>ru.andrew.securegwt.server.AuthServiceImpl</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>authServlet</servlet-name>
<url-pattern>/securegwt/auth</url-pattern>
</servlet-mapping>

<conetxt-param>
<param-name>contextConfiguration</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</conetxt-param>

</web-app>

4. Создаём в /war/WEB-INF и редактируем applicationContext.xml

<?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://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
                        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd">

<beans:bean id="customAuthListener"
class="ru.andrew.securegwt.server.auth.CustomAuthListener" />

<http auto-config="true">
<intercept-url pattern="/securegwt/**" access="ROLE_USER" />
<intercept-url pattern="/gwt/**" access="ROLE_USER" />
<intercept-url pattern="/**/*.html" access="ROLE_USER" />
<intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />
</http>

<beans:bean id="customAuthenticationProvider"
class="ru.andrew.securegwt.server.auth.CustomAuthenticationProvider" />

<authentication-manager alias="authenticationManager">
<authentication-provider ref="customAuthenticationProvider" />
</authentication-manager>

</beans:beans>
Intercept-url - это шаблоны урлов, подлежащих перехвату. Например, если запрашиваемый урл подходит под шаблон /securegwt/**, то спринг проверит в своём контектсте, есть ли у него аутентификация (экземпляр класса Authentication) с ролью ROLE_USER. Если она есть, вы спокойненько увидите свою желаемую страничку. Если же такой аутентификации нет, вы будете перенаправлены на страницу авторизации, и сможете получить свою желаемую страницу только после того как предоставите соответствующую пару логин/пароль

Под шаблон /** традиционно подходят все урлы, и если бы строка с IS_AUTHENTICATED_ANONYMOUSLY была первой в списке, то на все страницы предоставлялся бы анонимный доступ без авторизации. Важно эту строку помещать в конец, чтобы все защищённые страницы своевременно перехватывались бы спрингом с проверкой наличия соответствующей авторизации.

С таким простым использованием http”-элемента будет использована дефолтная страница логина.

5. Все аутентификационные запросы обрабатываются AuthenticationManager, так что его экземпляр должен быть объявлен в файле applicationContext.xml. Более точно, запросы обычно делегируются AuthenticationProvider. Можно использовать некоторые уже созданные реализации, такие как DaoAuthenticationProvider(при работе с ролями и юзерами, определёнными в базе данных) или LdapAuthenticationProvider (который аутентифицирует пользователей с помощью LDAP-сервера). для целей данной статьи, однако, мы создадим своего аутентификационного провайдера и интегрируем его с инфраструктурой Spring Scurity.

Дело AuthenticationProvider выдать экземпляр класса аутентификации (Authentication) с нужными ролями, или выбросить исключение AuthenticationException, если пользователь предоставил некорректные авторизационные данные. Мы создадим свой класс CustomAuthenticationProvider.java:
package ru.andrew.securegwt.server.auth;

import java.util.HashMap;
import java.util.Map;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

public class CustomAuthenticationProvider implements AuthenticationProvider {
private static Map<String, String> users = new HashMap<String, String>();
static {
users.put("fabrizio", "javacodegeeks");
users.put("justin", "javacodegeeks");
}

@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
if (users.get(username) == null)
throw new UsernameNotFoundException("User not found");
String storePass = users.get(username);
if (!storePass.equals(password))
throw new BadCredentialsException("invalid password");
Authentication customAuthentication = new CustomAuthentication(
"ROLE_USER", authentication);
customAuthentication.setAuthenticated(true);
return customAuthentication;
}

@Override
public boolean supports(Class<? extends Object> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}

6. Реализуем свою аутентификацию CustomUserAuthentication.java
package ru.andrew.securegwt.server.auth;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.GrantedAuthorityImpl;

public class CustomUserAuthentication implements Authentication {
private static final long serialVersionUID = 2835775828631400069L;
private boolean authenticated;
private GrantedAuthority grantedAuthority;
private Authentication authentication;

public CustomUserAuthentication(String role, Authentication authentication){
this.grantedAuthority = new GrantedAuthorityImpl(role);
this.authentication = authentication;
}

@Override
public String getName() {
return this.getClass().getSimpleName();
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(grantedAuthority);
return authorities;
}

@Override
public Object getCredentials() {
return authentication.getCredentials();
}

@Override
public Object getDetails() {
return authentication.getDetails();
}

@Override
public Object getPrincipal() {
return authentication.getPrincipal();
}

@Override
public boolean isAuthenticated() {
return authenticated;
}

@Override
public void setAuthenticated(boolean authenticated) throws IllegalArgumentException {
this.authenticated = authenticated;
}
}

7. Являясь частью Spring Framework Spring Security позволяет разработчику приложения обеспечить функции обратного вызова, которые будут вызваны в определённые моменты времени жизни приложения. Так, мы можем зарегистрировать наши методы, которые будут вызваны, когда случатся определённые аутентификационные события. В нашем случае мы создадим листенер, который получает AbstractAuthorizationEvent, то есть все события связанные с секьюрити-перехватами.

package ru.andrew.securegwt.server.auth;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.security.authentication.event.AbstractAuthenticationEvent;
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent;

public class CustomAuthListener implements ApplicationListener<AbstractAuthenticationEvent> {

private static final Log logger = LogFactory.getLog(CustomAuthListener.class);
@Override
public void onApplicationEvent(AbstractAuthenticationEvent event) {
final StringBuilder builder = new StringBuilder();
builder.append("Authentication event ");
builder.append(event.getClass().getSimpleName());
builder.append(": ");
builder.append(event.getAuthentication().getDetails());
builder.append("; details: ");

if(event instanceof AbstractAuthenticationFailureEvent){
builder.append("; exception: ");
builder.append(((AbstractAuthenticationFailureEvent) event).getException().getMessage());
}
logger.warn(builder.toString());
}
}
В этой реализации мы просто  логируем все удачные и неудачные события аутентификации (основываясь на классе LoggerListener), но совершенно понятно, как вы можете поместить здесь свою бизнес-логику.

8. Последним шагом создадим на стороне сервера асинхронный GWT-сервис, обещанный в декларациях web.xml, который обеспечит клиента информацией касательно юзера и usernaame, с помощью которого он логировался.
AuthService.java

package ru.andrew.securegwt.client;
import com.google.gwt.user.client.rpc.RemoteService;
public interface AuthService extends RemoteService {
String retrieveUsername();
}

AuthServiceAsync.java


package ru.andrew.securegwt.client;
import com.google.gwt.user.client.rpc.AsyncCallback;
public interface AuthServiceAsync {
void retrieveUsername(AsyncCallback<String> callback);
}

AuthServiceImpl.java
package ru.andrew.securegwt.server;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import ru.andrew.securegwt.client.AuthService;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
public class AuthServiceImpl extends RemoteServiceServlet implements
AuthService {
private static final long serialVersionUID = 8394711806095967642L;

@Override
public String retrieveUsername() {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
if (authentication == null) {
System.out.println("Not logged in");
return null;
} else {
return (String) authentication.getPrincipal();
}
}
}

9. Как это всё использовать?
Давайте вставим в самый конец метода onModuleLoad() класса Securegwt такой кодик:

authService.retrieveUsername(new AsyncCallback<String>() {
public void onFailure(Throwable caught) {
dialogBox.setText("Remote Procedure Call - Failure");
}

public void onSuccess(String result) {
nameField.setText(result);
}
});

а в начало этого вышеупомянутого класса определение:

private final AuthServiceAsync authService = GWT.create(AuthService.class);
И вообще-то этого должно быть достаточно - жмём правой кнопкой по проекту Run As > Web Application
В течение нескольких секунд проект запустится, во вкладке Development Mode будет выдана строка адреса, по которой можно обращаться в браузере:
http://127.0.0.1:8888/Securegwt.html?gwt.codesvr=127.0.0.1:9997
Собственно, вставляем её в хром и видим такую картинку:

Вводим туда заветную парочку fabrizio / javacodegeeks
И видим окно с приветствием, нажав на котором на кнопочку можно увидеть следующую картину:

Всем спасибо :)

5 комментариев:

  1. Супер!!! все понятно и с первого раза заработало! спасибо!

    ОтветитьУдалить
  2. Здравствуйте! Пытался запустить приложение на внешнем сервере (Apache Tomcat 6). После авторизации отображается просто страница html но ответ с сервера не приходит. Подскажите пожалуйста в чем проблема.

    ОтветитьУдалить
    Ответы
    1. Если я правильно понял, на локальном томкате приложение работало корректно, а на хостинговом сервере перестало?! Если это так,тогда проблема может быть связана с на стройками безопасности хост-провайдера. Надо им звонить, чтобы узнать их ограничения и смотреть логи. Я пользовался услугами brim.ru, мне пришлось повысить тариф, чтобы обойти их ограничения.

      Удалить
    2. На локальном Tomcat после авторизации появляется алерт - GWT module "securegwt" may need to bee (re)compiled.

      Удалить
    3. Ты компилируешь gwt-приложение, прежде чем выкладывать его на томкат?

      Удалить