Хочется, чтобы нашим приложением пользовались только те, кто имеет в нём аккаунт. И ещё хочется знать, какой именно пользователь сейчас пользуется нашим приложением, чтобы предоставить ему всю относящуюся к нему информацию, и никакую другую.
Хорошая новость в том, что для 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.
Вот список требуемых библиотек:
- org.springframework.aop-3.1.1.RELEASE
- org.springframework.asm-3.1.1.RELEASE
- org.springframework.beans-3.1.1.RELEASE
- org.springframework.context-3.1.1.RELEASE
- org.springframework.core-3.1.1.RELEASE
- org.springframework.expression-3.1.1.RELEASE
- org.springframework.web-3.1.1.RELEASE
- spring-security-config-3.1.3.RELEASE
- spring-security-core-3.1.3.RELEASE
- 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>
<?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);
}
}
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;
}
}
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();
}
}
}
Давайте вставим в самый конец метода 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В течение нескольких секунд проект запустится, во вкладке Development Mode будет выдана строка адреса, по которой можно обращаться в браузере:
http://127.0.0.1:8888/Securegwt.html?gwt.codesvr=127.0.0.1:9997
Собственно, вставляем её в хром и видим такую картинку:
И видим окно с приветствием, нажав на котором на кнопочку можно увидеть следующую картину:
Всем спасибо :)



Супер!!! все понятно и с первого раза заработало! спасибо!
ОтветитьУдалитьЗдравствуйте! Пытался запустить приложение на внешнем сервере (Apache Tomcat 6). После авторизации отображается просто страница html но ответ с сервера не приходит. Подскажите пожалуйста в чем проблема.
ОтветитьУдалитьЕсли я правильно понял, на локальном томкате приложение работало корректно, а на хостинговом сервере перестало?! Если это так,тогда проблема может быть связана с на стройками безопасности хост-провайдера. Надо им звонить, чтобы узнать их ограничения и смотреть логи. Я пользовался услугами brim.ru, мне пришлось повысить тариф, чтобы обойти их ограничения.
УдалитьНа локальном Tomcat после авторизации появляется алерт - GWT module "securegwt" may need to bee (re)compiled.
УдалитьТы компилируешь gwt-приложение, прежде чем выкладывать его на томкат?
Удалить