Статьи

Защита приложений GWT с помощью Spring Security

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

Если вы являетесь постоянным читателем JavaCodeGeeks , вы, вероятно, уже должны знать, что мы действительно любим GWT . В прошлом Джастин написал несколько потрясающих статей о GWT : как интегрировать GWT с Spring и Hibernate (JPA) и как добавить Eclipse и Maven в смесь . Более того, я написал о том, как добавить возможности JSON в ваше приложение GWT , как добавить CAPTCHA для GWT и как начать работу со SmartGWT . Наконец, Пэт написал о создании собственного GWT Spring Maven Archetype и интеграции GWT, EJB3, Maven и JBoss .

Таким образом, неудивительно, что мы сейчас внедряем модуль безопасности Spring . Как говорится на официальном сайте, Spring Security является мощной и настраиваемой средой аутентификации и контроля доступа. Это де-факто стандарт для защиты приложений на базе Spring . Spring Security — это развитие инфраструктуры Acegi, в которой Spring использовался для обеспечения безопасности, главным образом, веб-приложений. Тем не менее, Spring Security в настоящее время является полноценной структурой безопасности, включающей функциональность не только для Интернета, но и для интеграции LDAP и создания ACL . Прежде чем приступить к работе с этим учебником, было бы неплохо взглянуть на справочную документацию Spring Security и иметь под рукой Javadocs API Spring Security .

Для этого урока я буду использовать GWT 2.1.0 и Spring Security 3.0.5. Вы можете скачать последнюю версию производства здесь . Как вы уже догадались, некоторые библиотеки из ядра Spring также будут необходимы. Вы можете скачать рамки здесь .

Давайте начнем с создания нового проекта веб-приложения в Eclipse (я полагаю, вы уже установили плагин Google для Eclipse и что у вас также развернут GWT). Я выбрал глубокое имя «GwtSpringSecurityProject» для названия проекта. Вот как будет выглядеть экран Eclipse:

Первым шагом для добавления безопасности Spring в наш проект является объявление фильтра в нашем файле «web.xml». Этот фильтр, который является экземпляром класса FilterChainProxy , будет перехватывать все входящие запросы и делегировать управление запросом соответствующему обработчику Spring. Соответствующий фрагмент файла веб-декларации следующий:

01
02
03
04
05
06
07
08
09
10
11
<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>
...

Мы также должны определить ContextLoaderListener в нашем «web.xml» для загрузки контекста Spring. Это делается с помощью следующего фрагмента:

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

Затем мы создаем файл с именем «applicationContext.xml» в папке «war / WEB-INF». Там мы декларируем информацию о безопасности весны. Наиболее важным элементом является « http », который может использоваться для определения того, к каким URL-адресам должна применяться безопасность, а также к ролям, которые должны выполнять пользователи для доступа к конкретным ресурсам. В нашем случае фрагмент выглядит следующим образом:

1
2
3
4
5
6
7
8
<http auto-config="true">
        <intercept-url pattern="/gwtspringsecurityproject/**" 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>
...

Вкратце, в приведенном выше заявлении говорится, что роль «ROLE_USER» необходима для получения доступа к файлам в папках «gwt» и «gwtspringsecurityproject» (где находятся ресурсы, связанные с GWT). Точно так же все файлы HTML (например, точка входа GWT) требуют одинаковой роли. «IS_AUTHENTICATED_ANONYMOUSLY» означает, что все пользователи могут получить доступ к конкретному ресурсу, не будучи частью определенной роли. При таком простом использовании элемента « http » Spring использует страницу входа по умолчанию и URL выхода из системы.

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

Прежде чем мы углубимся в код приложения, мы должны сначала позаботиться о зависимостях. Вот JAR-файлы, которые необходимо добавить в путь к классам проекта:

  • org.springframework.context-3.0.5.RELEASE.jar
  • весна-безопасности ядро-3.0.5.RELEASE.jar
  • весна-безопасности веб-3.0.5.RELEASE.jar

Хорошо, теперь мы готовы. Наш провайдер довольно прост и использует статическую карту для хранения пользователей и их соответствующих паролей. Вот код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.javacodegeeks.gwt.security.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 storedPass = users.get(username);
         
        if (!storedPass.equals(password))
            throw new BadCredentialsException("Invalid password");
         
        Authentication customAuthentication =
            new CustomUserAuthentication("ROLE_USER", authentication);
        customAuthentication.setAuthenticated(true);
         
        return customAuthentication;
         
    }
 
    @Override
    public boolean supports(Class<? extends Object> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
 
}

Давайте начнем разработку этого кода с конца. Метод support определяет тип аутентификации, предоставляемый этим провайдером. В нашем случае мы хотим обработать UsernamePasswordAuthenticationToken . Эта реализация предназначена для простого представления имени пользователя и пароля.

Метод authenticate реализован, и внутри него мы получаем имя пользователя, предоставленное в форме входа в систему (через метод getPrincipal ), а также сопутствующий пароль (через метод getCredentials ). Сначала мы проверяем, существует ли конкретное имя пользователя и, если нет, генерируется исключение UsernameNotFoundException . Точно так же, если имя пользователя существует, но пароль неверен, выдается исключение BadCredentialsException . Обратите внимание, что оба этих исключения расширяют родительский класс AuthenticationException .

Если имя пользователя и пароль верны, мы на месте для аутентификации пользователя. Для этого мы должны вернуть конкретный экземпляр интерфейса аутентификации . При этом мы должны инкапсулировать уже известную информацию пользователя (учетные данные и т. Д.), А также роли (полномочия), которыми обладает пользователь. Обратите внимание, что назначенная роль (ROLE_USER) соответствует роли, объявленной в файле applicationContext.xml. Кроме того, метод setAuthenticated должен быть вызван (с истиной в качестве аргумента), чтобы указать остальной части цепочки аутентификации, что конкретный пользователь был успешно аутентифицирован нашим модулем. Давайте посмотрим, как определяется пользовательский объект аутентификации в нашем случае:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.javacodegeeks.gwt.security.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 = -3091441742758356129L;
     
    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 Collection<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;
    }
 
    @Override
    public String getName() {
        return this.getClass().getSimpleName();
    }
 
}

В конструкторе мы передаем роль пользователя и исходный объект аутентификации . В реализованных методах наиболее важным является getAuthorities , который возвращает полномочия, предоставленные принципалу. Эта информация предоставляется внутри коллекции объектов GrantedAuthority .

Посмотрим теперь, как выглядит «applicationContext.xml»:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
 
 
    <beans:bean id="customAuthListener" class="com.javacodegeeks.gwt.security.server.auth.CustomAuthListener"/>
 
    <http auto-config="true">
        <intercept-url pattern="/gwtspringsecurityproject/**" 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="com.javacodegeeks.gwt.security.server.auth.CustomAuthenticationProvider" />   
     
    <authentication-manager alias="authenticationManager">
     <authentication-provider ref="customAuthenticationProvider"/>
 </authentication-manager>
         
</beans:beans>

Каждый элемент файла объявления был определен за исключением «CustomAuthListener». Будучи частью среды Spring, Spring Security позволяет разработчику приложения предоставлять обратные вызовы, которые будут вызываться в определенных частях жизненного цикла приложения. Таким образом, мы можем зарегистрировать наши методы для вызова при возникновении определенных событий аутентификации. В нашем случае мы создадим прослушиватель, который получает AbstractAuthorizationEvent s, то есть все события, связанные с перехватом безопасности. Давайте посмотрим, как это достигается:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.javacodegeeks.gwt.security.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().getName());
        builder.append("; details: ");
        builder.append(event.getAuthentication().getDetails());
 
        if (event instanceof AbstractAuthenticationFailureEvent) {
            builder.append("; exception: ");
            builder.append(((AbstractAuthenticationFailureEvent) event).getException().getMessage());
        }
 
        logger.warn(builder.toString());
 
 }
 
}

В нашей реализации мы просто регистрируем все успешные и неуспешные события аутентификации (на основе класса LoggerListener ), но, очевидно, довольно просто предоставить здесь свою бизнес-логику.

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

AuthService

01
02
03
04
05
06
07
08
09
10
11
12
package com.javacodegeeks.gwt.security.client;
 
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
 
/**
 * The client side stub for the RPC service.
 */
@RemoteServiceRelativePath("auth")
public interface AuthService extends RemoteService {
 String retrieveUsername();
}

AuthServiceAsync

01
02
03
04
05
06
07
08
09
10
package com.javacodegeeks.gwt.security.client;
 
import com.google.gwt.user.client.rpc.AsyncCallback;
 
/**
 * The async counterpart of <code>AuthService</code>.
 */
public interface AuthServiceAsync {
 void retrieveUsername(AsyncCallback<String> callback);
}

AuthServiceImpl

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.javacodegeeks.gwt.security.server;
 
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
 
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.javacodegeeks.gwt.security.client.AuthService;
 
@SuppressWarnings("serial")
public class AuthServiceImpl extends RemoteServiceServlet implements AuthService {
 
    @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();
        }
         
    }
     
}

Код очень прост. Мы используем класс SecurityContextHolder для извлечения текущего SecurityContext, а затем метода getAuthentication для получения ссылки на базовый объект Authentication . Из этого мы получаем имя пользователя, если оно есть, с помощью метода getPrincipal .

Конечно, мы должны объявить конкретный сервлет в файле нашего приложения «web.xml». Вот:

01
02
03
04
05
06
07
08
09
10
11
...
<servlet>
 <servlet-name>authServlet</servlet-name>
 <servlet-class>com.javacodegeeks.gwt.security.server.AuthServiceImpl</servlet-class>
</servlet>
 
<servlet-mapping>
 <servlet-name>authServlet</servlet-name>
 <url-pattern>/gwtspringsecurityproject/auth</url-pattern>
</servlet-mapping>
...

А вот и весь файл веб-декларации:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 
<web-app>
 
    <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>
 
    <!-- Servlets -->
    <servlet>
        <servlet-name>greetServlet</servlet-name>
        <servlet-class>
            com.javacodegeeks.gwt.security.server.GreetingServiceImpl
        </servlet-class>
    </servlet>
 
    <servlet-mapping>
        <servlet-name>greetServlet</servlet-name>
        <url-pattern>/gwtspringsecurityproject/greet</url-pattern>
    </servlet-mapping>
     
    <servlet>
        <servlet-name>authServlet</servlet-name>
        <servlet-class>com.javacodegeeks.gwt.security.server.AuthServiceImpl</servlet-class>
    </servlet>
 
    <servlet-mapping>
        <servlet-name>authServlet</servlet-name>
        <url-pattern>/gwtspringsecurityproject/auth</url-pattern>
    </servlet-mapping>
 
    <!-- Default page to serve -->
    <welcome-file-list>
        <welcome-file>GwtSpringSecurityProject.html</welcome-file>
    </welcome-file-list>
 
</web-app>

Давайте посмотрим, как этот сервис используется внутри точки входа приложения. Мы добавляем следующий фрагмент кода непосредственно перед концом метода onModuleLoad :

01
02
03
04
05
06
07
08
09
10
authService.retrieveUsername(
 new AsyncCallback<String>() {
  public void onFailure(Throwable caught) {
   dialogBox.setText("Remote Procedure Call - Failure");
  }
  public void onSuccess(String result) {
   nameField.setText(result);
  }
 }
);

Последний шаг перед запуском нашего приложения — позаботиться о зависимостях времени выполнения. Spring требует кучу библиотек, чтобы сделать магию DI, поэтому вот список JAR, которые должны присутствовать в вашей папке «war / WEB-INF / lib»:

  • org.springframework.aop-3.0.5.RELEASE.jar
  • org.springframework.asm-3.0.5.RELEASE.jar
  • org.springframework.beans-3.0.5.RELEASE.jar
  • org.springframework.context-3.0.5.RELEASE.jar
  • org.springframework.core-3.0.5.RELEASE.jar
  • org.springframework.expression-3.0.5.RELEASE.jar
  • org.springframework.web-3.0.5.RELEASE.jar
  • весна-безопасности конфиг-3.0.5.RELEASE.jar
  • весна-безопасности ядро-3.0.5.RELEASE.jar
  • весна-безопасности веб-3.0.5.RELEASE.jar

После копирования всего вышеперечисленного запустите конфигурацию проекта Eclipse и попробуйте получить доступ к URL-адресу по умолчанию:

http://127.0.0.1:8888/GwtSpringSecurityProject.html?gwt.codesvr=127.0.0.1:9997

Spring Security перехватит запрос, и вы увидите страницу входа по умолчанию. Укажите действительные учетные данные, как показано ниже:

Отправьте данные формы, и вы будете перенаправлены на исходный URL. Обратите внимание, что текстовое поле будет заполнено именем пользователя, используемым для входа в систему.

Вернитесь к представлению Eclipse Console и просмотрите различные журналы, напечатанные там. Вы должны увидеть что-то вроде следующего:


12 декабря 2010 20:45:49 com.javacodegeeks.gwt.security.server.auth.CustomAuthListener onApplicationEvent
ПРЕДУПРЕЖДЕНИЕ: событие аутентификации AuthenticationSuccessEvent: CustomUserAuthentication; подробности: org.springframework.security.web.authentication.WebAuthenticationDetails@fffdaa08: RemoteIpAddress: 127.0.0.1; SessionId: im1fdjvdu7yw
12 декабря 2010 20:45:49 com.javacodegeeks.gwt.security.server.auth.CustomAuthListener onApplicationEvent
ВНИМАНИЕ: Событие аутентификации InteractiveAuthenticationSuccessEvent: CustomUserAuthentication; подробности: org.springframework.security.web.authentication.WebAuthenticationDetails@fffdaa08: RemoteIpAddress: 127.0.0.1; SessionId: im1fdjvdu7yw

Это все, ребята. Вы можете найти здесь созданный проект Eclipse. Повеселись!

Статьи по Теме :