В этом уроке мы увидим, как интегрировать 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" ?> xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd"> < 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. Повеселись!
- GWT 2 Spring 3 JPA 2 Hibernate 3.5 Учебник
- Начало работы с SmartGWT для потрясающих интерфейсов GWT
- Создание собственного GWT Spring Maven Archetype
- Учебник по GWT 2 Spring 3 JPA 2 Hibernate 3.5 — демонстрация Eclipse и Maven 2
- Отправка электронной почты в Java с помощью Spring — пример SMTP-сервера GMail