понедельник, 2 сентября 2013 г.

Добавление спрингового HttpRequestHandlerServlet сервлета в Tapestry приложение

Недавно возникла небольшая проблема: как добавить свой спринговый сервлет в Tapestry приложение. Если просто прописать его в web.xml, например так:
    <servlet>
        <servlet-name>PaymentService</servlet-name>
        <servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>PaymentService</servlet-name>
        <url-pattern>/PaymentService</url-pattern>
    </servlet-mapping>


то Tapestry filter перехватывает запрос к этому сервлету, из-за вот такой настройки:
    <filter>
        <filter-name>app</filter-name>
        <filter-class>org.apache.tapestry5.TapestryFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>app</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>


Как исключить что-то в url-pattern, я так и не понял, но, к счастью, тапестри позволяет настроить обработку HttpRequest и вручную определить, какие фильтры к нему применять. Нужно всего лишь добавить следующий код в AppModule
       @Contribute(HttpServletRequestHandler.class)
    public void сontributeFilter(OrderedConfiguration<HttpServletRequestFilter> config){
        config.add("httpInvoker", new HttpServletRequestFilter() {
            @Override
            public boolean service(HttpServletRequest request, HttpServletResponse response, HttpServletRequestHandler handler) throws IOException {
                if(request.getServletPath().equals("/PaymentService")){ // имя нашего сервлета
                    try{
                        AppContext.getHttpInvoker(StringUtils.removeStart(request.getServletPath(), "/")).handleRequest(request, response);
                    }catch (ServletException ex){
                        throw new IOException(ex);
                    }
                    return true; // мы обработали запрос сами, не нужно его обрабатывать другими фильтрами.
                }
                return handler.service(request, response); // передаем запрос тапестри
            }
        });
    }


AppContext.getHttpInvoker - это хелпер, который возвращает spring bean по его имени, приведя его к нужному типу.
    public static HttpInvokerServiceExporter getHttpInvoker(String name){
        if(context == null){
            throw new ApplicationNotInitializedException();
        }
        return (HttpInvokerServiceExporter) context.getBean(name);
    }

 
И, для полноты картины, кусочек конфига спринга:
    <bean name="PaymentService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
        <property name="service" ref="PaymentServiceImpl"/>
        <property name="serviceInterface" value="com.example.httpInvoker.PaymentService"/>
    </bean>


пятница, 7 июня 2013 г.

Изменение стилей Tapestry приложения

В данной статье описывается, как поменять стили у программы, написанной с использованием Tapestry 5.3.x

Добавляем нашу любимую библиотечку со стилями, например bootstrap.css. Это почти сразу придаст приложению определенный общий вид, как будто для него разрабатывали дизайн. Но к сожалению, некоторые вещи, в частности валидация, пейджинг, таблицы, начинают смотреться чужеродно. Например вот так выглядит страница логина после применения bootstrap.css
К сожалению, исправить там что-то уже довольно сложно.
Поэтому принимаем решение полностью отказаться от стилей tapestry в приложении. Делается это следующим образом: добавляем к AppModule.java вот такой код
    @Contribute(MarkupRenderer.class)
    public static void deactiveDefaultCSS(OrderedConfiguration<MarkupRendererFilter> configuration)
    {
        configuration.override("InjectDefaultStylesheet", null);
    }
  

После удаления таблицы стилей страницы выглядят пустовато; но ничего, скоро мы это исправим.
Добавляем style.css - нашу собственную таблицу стилей. В ней мы переопределим некоторые стили tapestry.

Для начала, добавим стили для корректного отображения ошибок:

#корректируем ошибку tapestry с заползанием текста ошибки за левый край
DIV.t-error LI{
  margin-left: 20px;
}

#задаем цвет попапа с ошибкой
DIV.t-error-popup{
   color: #b94a48;
}

#рисуем стрелку вниз
.t-error-popup > span:after {
    border-color: #fcf8e3 transparent;
    border-style: solid;
    border-width: 15px 15px 0;
    bottom: 0px;
    content: "";
    display: block;
    left: 20px;
    position: absolute;
    width: 0;
}

#рисуем попап
DIV.t-error-popup > span {
   padding: 4px;
   text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
   border-color: #eed3d7;
   background-color: #fcf8e3;
   border: 1px solid #eed3d7;
   -webkit-border-radius: 4px;
      -moz-border-radius: 4px;
           border-radius: 4px;
}

#Задаем цвет контура поля ввода с ошибкой
input.t-error{
      border-color: #eed3d7;
}


И стили для корректного отображения pager'а

#стили для grid pager, скопированы из bootstrap.css
DIV.t-data-grid-pager{
    padding-left: 15px;
    display: inline-block;
  *display: inline;
  margin-bottom: 4px;
  margin-left: 0;
  -webkit-border-radius: 4px;
     -moz-border-radius: 4px;
          border-radius: 4px;
  *zoom: 1;
}
.t-data-grid-pager > a,
.t-data-grid-pager > span {
  padding: 4px 12px;
  line-height: 20px;
  text-decoration: none;
  background-color: #ffffff;
  border: 1px solid #dddddd;
  /*border-left-width: 0;*/
}
.t-data-grid-pager > a:hover,
.t-data-grid-pager > a:focus,
.t-data-grid-pager > .current > a,
.t-data-grid-pager > .current > span {
  background-color: #f5f5f5;
}
.t-data-grid-pager > .current > a,
.t-data-grid-pager > .current > span {
  color: #999999;
  cursor: default;
}
.t-data-grid-pager a:first-child,
.t-data-grid-pager span:first-child {
  border-left-width: 1px;
  -webkit-border-bottom-left-radius: 4px;
          border-bottom-left-radius: 4px;
  -webkit-border-top-left-radius: 4px;
          border-top-left-radius: 4px;
  -moz-border-radius-bottomleft: 4px;
  -moz-border-radius-topleft: 4px;
}
.t-data-grid-pager a:last-child,
.t-data-grid-pager span:last-child  {
  -webkit-border-top-right-radius: 4px;
          border-top-right-radius: 4px;
  -webkit-border-bottom-right-radius: 4px;
          border-bottom-right-radius: 4px;
  -moz-border-radius-topright: 4px;
  -moz-border-radius-bottomright: 4px;
}

Результат:



Подправим beandisplay

DL.t-beandisplay {
    background: none repeat scroll 0 0 rgb(245, 245, 245);
    display: block;
    padding: 2px;
    width: auto;
}
DL.t-beandisplay DT:after {
    content: ":";
}
DL.t-beandisplay DT {
    clear: left;
    display: inline;
    float: left;
    padding-right: 3px;
    text-align: right;
    vertical-align: middle;
    width: 250px;
}


Поправим показ таблиц (выделим заголовок)
.table th {
  white-space: nowrap;
  line-height: 20px;
  background-color: rgb(245, 245, 245);
}


Осталось немного допилить тексты сообщений, выводимые Tapestry при срабатывании валидации. Чтобы их скастомизировать, необходимо создать следующие файлы:
org.apache.tapestry5.corelib.components.Errors.properties следующего содержания:
default-banner=Вам необходимо исправить следующее, чтобы продолжить:
И org.apache.tapestry5.internal.ValidationMessages.properties в котором можно переопределить сообщения, выдаваемые при различных видах валидации, например для required:
required=Укажите значение

Результат:



четверг, 6 июня 2013 г.

Организация security в приложениях, написанных с использованием Tapesrty5

Существует несколько различных способов обеспечения безопасности в приложениях на Tapestry 5. В данной статье описывается только один из них - security с использованием библиотеки shiro и прослойки, которая осуществляет интеграцию с этой библиотекой - tapestry-security

При использовании maven необходимо добавить в зависимости:

        <dependency>
            <groupId>org.tynamo</groupId>
            <artifactId>tapestry-security</artifactId>
            <version>0.4.6</version>
        </dependency>


Самый простой случай, когда пользователи и пароли хранятся в файле, рассматривать не будем, это сделано в документации, вместо этого создадим свой собственный Realm. Ограничимся разделением доступа по ролям, с предопределенным набором ролей.
Для начала создадим класс пользователя User, экземпляр этого класса будет использоваться как SecurityPrincipal в дальнейшем.

import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.util.ByteSource;
import javax.persistence.*;


@Entity
@Table(name = "TEST_USER")
public class User implements Serializable{
    public static final String ROLE_ADMIN = "admin", ROLE_OPERATOR = "operator";
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true, nullable = false, length = 100)
    private String login;
    @Column(nullable = false)
    private String name;
    @Column(nullable = true)
    private String passwordHash;
    @Column(nullable = false)
    private boolean enabled = true;
    @Column(length = 128)
    private byte[] salt;
    @Column(nullable = false)
    private boolean admin = false;
   @Override
    public String toString() {
        return name;
    }
    public void setPassword(String password){
        ByteSource saltSource = new SecureRandomNumberGenerator().nextBytes();
        this.setSalt(saltSource.getBytes());
        this.setPasswordHash(new
Sha256Hash(password, saltSource).toString());
    }
}


Тривиальные get/set методы опущены. Как можно заметить, пароль не хранится в открытом виде, хранится только SHA-256 хэш пароля и соль.

Создадим свой собственный Realm для авторизации и аутентификации пользователей.

import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.SimpleByteSource;
import org.example.test.AppContext;
import
org.example.test.domain.User;

import java.util.HashSet;
import java.util.Set;

public class UserRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        if(principalCollection.isEmpty()) return null;
        try{
            User user = (User)principalCollection.fromRealm(getName()).iterator().next();
            if (user == null) return null;
            Set<String> roles = new HashSet<String>();

            // здесь используются только две роли, но ничто не мешает пользователю иметь несколько ролей одновременно
            if(user.isAdmin()){
                roles.add(User.ROLE_ADMIN);
            }else{
                roles.add(User.ROLE_OPERATOR);
            }
            return new SimpleAuthorizationInfo(roles);
        }catch (Exception ex){
            ex.printStackTrace();
            return null;
        }
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        // Null username is invalid
        if (username == null) { throw new AuthenticationException("Void username are not allowed by this realm."); }

        // запрос на получение пользователя из базы данных
        User user = AppContext.getUserDao().findByLogin(username);
        if(user == null){
            throw new UnknownAccountException("user not found");
        }
        return new SimpleAuthenticationInfo(user, user.getPasswordHash(), new SimpleByteSource(user.getSalt()), getName());
    }

    public UserRealm() {
        super(new MemoryConstrainedCacheManager());
        setName("dbaccounts");
        setAuthenticationTokenClass(UsernamePasswordToken.class);
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("SHA-256");
        setCredentialsMatcher(matcher);
    }
}


После этого можно приступать к настройке доступа к страницам и методам. Предположим, что все, что доступно админу находится в директории admin, а все, что доступно оператору - в директории operator. Страницы логина, запроса на смену пароля и ошибок находятся в корне и доступ к ним возможен для неавторизованных пользователей.

Пропишем эти настройки в AppModule.java Очень похоже на черную магию, и собственно это так и есть. Как и почему это работает, я не понимаю. Возможно это работает из-за аннотаций, или потому что переданы определенные параметры. или указано определенное имя функции, кто знает...

import org.apache.shiro.realm.Realm;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.apache.tapestry5.ioc.Configuration;
import org.apache.tapestry5.ioc.MappedConfiguration;
import org.apache.tapestry5.ioc.annotations.Contribute;
import org.apache.tapestry5.ioc.annotations.Marker;
import org.apache.tapestry5.ioc.services.ApplicationDefaults;
import org.apache.tapestry5.ioc.services.SymbolProvider;
import org.apache.tapestry5.services.HttpServletRequestFilter;
import org.tynamo.security.Security;
import org.tynamo.security.SecuritySymbols;
import org.tynamo.security.services.SecurityFilterChainFactory;
import org.tynamo.security.services.impl.SecurityFilterChain;
import org.example.test.domain.User;


    @Contribute(WebSecurityManager.class)
    public static void addRealms(Configuration<Realm> configuration) {
        UserRealm ur = new UserRealm();
        configuration.add(ur);
    }

    @Contribute(SymbolProvider.class)
    @ApplicationDefaults
    public static void applicationDefaults(MappedConfiguration<String, Object> configuration){
        // Tynamo's tapestry-security (Shiro) module configuration
        configuration.add(SecuritySymbols.LOGIN_URL, "/login");
        configuration.add(SecuritySymbols.SUCCESS_URL, "/index");
        configuration.add(SecuritySymbols.UNAUTHORIZED_URL, "/accessDenied");
    }

    @Contribute(HttpServletRequestFilter.class)
    @Marker(Security.class)
    public static void contributeSecurityConfiguration(Configuration<SecurityFilterChain> configuration,
                                                       SecurityFilterChainFactory factory,
                                                       WebSecurityManager securityManager) {
        configuration.add(factory.createChain("/admin/**").add(factory.roles(), User.ROLE_ADMIN).build());
        configuration.add(factory.createChain("/operator/**").add(factory.roles(), User.ROLE_OPERATOR).build());
        configuration.add(factory.createChain("/css/**").add(factory.anon()).build());
        configuration.add(factory.createChain("/*").add(factory.anon()).build());
    }


После того, как все настройки прописаны, займемся непосредственно страничками.
Для начала настроим страницу логина, поскольку то, что идет по умолчанию с tapestry-security, не всегда вписывается в общий дизайн приложения.

import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.StringUtils;
import org.apache.shiro.web.util.SavedRequest;
import org.apache.shiro.web.util.WebUtils;
import org.apache.tapestry5.PersistenceConstants;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.RequestGlobals;
import org.apache.tapestry5.services.Response;
import org.tynamo.security.services.PageService;
import org.tynamo.security.services.SecurityService;
import java.io.IOException;

public class Login {

    @Property
    private String jsecLogin;
    @Property
    private String jsecPassword;
    @Property
    private boolean jsecRememberMe;
    @Persist(PersistenceConstants.FLASH)
    private String loginMessage;
    @Inject
    private Response response;
    @Inject
    private RequestGlobals requestGlobals;
    @Inject
    private SecurityService securityService;
    @Inject
    private PageService pageService;

    public Object onActionFromJsecLoginForm() {
        Subject currentUser = securityService.getSubject();
        if (currentUser == null) {
            throw new IllegalStateException("Subject cannot be null");
        }
        UsernamePasswordToken token = new UsernamePasswordToken(jsecLogin, jsecPassword);
        token.setRememberMe(jsecRememberMe);
        try {
            currentUser.login(token);
        } catch (UnknownAccountException e) {
            loginMessage = "Такого пользователя нет в базе";
            return null;
        } catch (IncorrectCredentialsException e) {
            loginMessage = "Неверный пароль";
            return null;
        } catch (LockedAccountException e) {
            loginMessage = "Пользователь заблокирован";
            return null;
        } catch (AuthenticationException e) {
            loginMessage = "Ошибка авторизации";
            return null;
        }
        SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(requestGlobals.getHTTPServletRequest());
        if (savedRequest != null) {
            try {
                WebUtils.redirectToSavedRequest(requestGlobals.getHTTPServletRequest(), requestGlobals.getHTTPServletResponse(), pageService.getSuccessPage());
                return savedRequest.getRequestURI();
            } catch (IOException e) {
                return pageService.getSuccessPage();
            }
        } else {
            return pageService.getSuccessPage();
        }
    }
    public String getLoginMessage() {
        if (hasLoginMessage()) {
            return loginMessage;
        } else {
            return " ";
        }
    }
    public boolean hasLoginMessage() {
        return StringUtils.hasText(loginMessage);
    }
}


Login.tml В файле layout'а прописаны ссылки на bootstrap.css, и не используется css от tapestry. Как сделать красивые странички, и сколько проблем при этом возникло - тема для отдельного поста.

<t:layout title="literal:Вход" bodyId="signin"
          xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
          xmlns:p="tapestry:parameter">
    <t:form class="form-horizontal" t:id="jsecLoginForm">
        <t:if test="hasLoginMessage()">
        <div class="control-group">
            <div class="controls">
            <label class="text-error">${loginMessage}</label>
            </div>
        </div>
        </t:if>
        <div class="control-group">
            <label class="control-label" for="jsecLogin">Логин</label>
            <div class="controls">
                <t:textfield t:id="jsecLogin" class="input-large" validate="required" size="40" tabindex="1"  placeholder="Login"/>
            </div>
        </div>
        <div class="control-group">
            <label class="control-label" for="jsecPassword">Пароль</label>
            <div class="controls">
                <t:passwordfield t:id="jsecPassword" class="input-large" validate="required" size="40" tabindex="2" placeholder="Password" />
            </div>
        </div>
        <div class="control-group">
            <div class="controls">
                <t:pageLink page="getPass">Забыли пароль?</t:pageLink>
                <label class="checkbox">
                    <t:checkbox t:id="jsecRememberMe" tabindex="3" /> Запомнить меня
                </label>
                <input type="submit" name="login" class="btn" value="Войти" tabindex="4" />
            </div>
        </div>
    </t:form>
</t:layout>


После успешного логина пользователя пребрасывает на index, где уже осуществляется переброс в засимости от роли.

public class Index {
    @InjectPage
    private AdminIndex adminIndex;
    @InjectPage
    private OperatorIndex operatorIndex;
    @OnEvent(value = EventConstants.ACTIVATE)
    public Object initPage() {
        if(SecurityUtils.getSubject().hasRole(User.ROLE_ADMIN)){
            return adminIndex;
        }else{
            return operatorIndex;
        }
    }
}


И остался небольшой момент - это logout. Функция logout реализована непосредственно в шаблоне

public class Layout {

    @InjectPage
    private Index index;
    @Inject
    @Property
    private HttpServletRequest request;
    public String getUsername() {
        try {

            return SecurityUtils.getSubject().getPrincipal().toString();
        } catch (NullPointerException ex) {
            return "guest";
        }
    }

    public Object onActionFromLogout() {
        SecurityUtils.getSecurityManager().logout(SecurityUtils.getSubject());
        try {
            final HttpSession session = this.request.getSession(false);
            if (session != null) {
                session.invalidate();
            }
        } catch (final IllegalStateException ex) { }
        return index;
    }
}


И Layout.tml

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
      xmlns:p="tapestry:parameter" >
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <link href="${request.contextPath}/css/bootstrap.css" rel="stylesheet" type="text/css"/>
    <link href="${request.contextPath}/css/mystyle.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<t:security.user>
    <p class="pad-left">Здравствуйте, ${username}

    <t:actionLink t:id="logout" class="pull-right pad-right"> Выйти</t:actionLink></p>
</t:security.user>
<div class="control-group">
    <div class="control">
        <t:body/>
    </div>
</div>
</body>
</html>


В общем-то этого должно быть достаточно. Если необходимо получить где-то в коде текущего залогиненного пользователя, достаточно выполнить (org.example.test.domain.User) SecurityUtils.getSubject().getPrincipal();

вторник, 30 апреля 2013 г.

Вызов WCF веб-сервиса, использующего WS-Security

Недавно потребовалось вызвать .net WCF веб-сервис из java программы. При этом исходный сервис использовал WS-Security с подписью и шифрованием. Раньше мне не приходилось использовать WS-Security в веб-сервисах и после некоторого периода исследований я остановился на двух библиотеках, которые декларировали поддержку данной технологии: Apache CXF и Glassfish Metro. Первым я попробовал CXF.
Документации не сказать чтобы совсем никакой, но по части WS_Security очень скудная, и не понятно, как же в итоге заставить клиента веб-сервиса работать. Забегая вперед скажу, что у меня так и не получилось создать запрос, который был бы принят сервером.

Вот пример попытки вызова метода веб-сервиса.

        Client client = ClientProxy.getClient(iface);
        client.getInInterceptors().add(new LoggingInInterceptor());
        client.getOutInterceptors().add(new LoggingOutInterceptor());
        Endpoint cxfEndpoint = client.getEndpoint();
        cxfEndpoint.put("ws-security.encryption.username", "server");
        cxfEndpoint.put("ws-security.encryption.properties", "enc.properties");
        cxfEndpoint.put("ws-security.signature.username", "mykey");
        cxfEndpoint.put("ws-security.signature.properties", "sig.properties");

        cxfEndpoint.put("ws-security.callback-handler", "ClientKeystorePasswordCallback");


sig.properties и enc.properties идентичны и содержат следующее:

org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=changeit
org.apache.ws.security.crypto.merlin.keystore.file=/home/grigory/out.jks       


Оказалось, что в формируемом клиентом SOAP запросе в заголовоке wsse:Security нет тега wsse:BinarySecurityToken. Я понял это только после того, как с помощью Glassfish Metro создал работающего клиента и сравнил отправляемые запросы.

Следующей на очереди была Glassfish Metro. Мне она изначально не нравилась из-за того, что заточена под использование в NetBeans, и из-за этого вся документация подается в виде "пойдите туда, нажмите эту кнопочку и все волшебным образом заработает". Но в итоге все-таки удалось создать решение, не зависящее от конкретной IDE, но, к сожалению, заставляющее программиста вручную редактировать xml, если в wsdl сервиса появятся изменения.

Итак, вот решение:
  • копируем wsdl и все зависимые wsdl и xsd файлы локально. В нашем случае это service.wsdl и wsdl0.wsdl. В wsdl0.wsdl описываются полиси для каждого метода, впоследствии мы их заменим.
  • дописываем к названию wsdl файлов .xml (и меняем внутри, если есть импорты)
  • создаем файл wsit-client.xml (он должен называться именно так) следующего содержания, в который просто включаем наш переименованный service.wsdl.xml
<?xml version="1.0" encoding="UTF-8"?>
<definitions
        xmlns="http://schemas.xmlsoap.org/wsdl/"
        xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
        name="mainclientconfig">
    <import location="service.wsdl.xml"
            namespace="http://testuri.com/wcflib/"/>
</definitions>

  • заменяем в файле wsdl0.wsdl.xml все wsp:Policy для всех методов на одну-единственную, следующего вида:
<wsdl:definitions 
...
                  xmlns:wsp1="http://www.w3.org/ns/ws-policy" 
...
/> 

<wsp1:Policy wsu:Id="WS2007HttpBinding_IWebService_policy">
        <wsp1:ExactlyOne>
            <wsp1:All>
                <sc:KeyStore wspp:visibility="private" alias="${keystore.private}"

                             storepass="${keystore.password}" type="JKS" 
                             location="${keystore.path}" keypass="${keystore.password}"/>
                <sc:TrustStore wspp:visibility="private" peeralias="${keystore.server}"

                             storepass="${keystore.password}" type="JKS"
                             location="${keystore.path}"/>
            </wsp1:All>
        </wsp1:ExactlyOne>
    </wsp1:Policy>


${keystore.path}, ${keystore.password} и т.п проставятся на этапе сборки.
WS2007HttpBinding_IWebService_policy нужно заменить на значение, прописанное в теге wsp:PolicyReference в wsdl:binding для вашего сервиса
  • Удаляем все ссылки на несуществующие полиси. Сами полиси мы удалили на предущем шаге, но ссылки в wsdl:input и wsdl:output на них остались.
  • Генерируем код клиента. Этот шаг мог быть первым, он не зависит от предыдущих. Для этого используем плагин для maven.
              <plugin>
                <groupId>org.jvnet.jax-ws-commons</groupId>
                <artifactId>jaxws-maven-plugin</artifactId>
                <version>2.1</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>wsimport</goal>
                        </goals>
                        <configuration>
                            <extension>true</extension>
                            <bindingDirectory>
                                src/main/resources
                            </bindingDirectory>
                            <bindingFiles>
                                <bindingFile>jaxb-java-datatypes.xml</bindingFile>
                                <bindingFile>jaxb-binding-xsd4.xml</bindingFile>
                                <bindingFile>jaxb-binding-xsd9.xml</bindingFile>
                            </bindingFiles>
                            <wsdlDirectory>
                                src/main/resources
                            </wsdlDirectory>
                            <wsdlFiles>
                                <wsdlFile>service.wsdl</wsdlFile>
                            </wsdlFiles>
                            <wsdlLocation>http://testuri.com/wcflib-tc/service.svc</wsdlLocation>
                            <sourceDestDir>
                                ${basedir}/target/generated-sources
                            </sourceDestDir>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

Также очень полезным оказалось автоматическая генерация метода toString(). Этот метод генерируется другим плагином, поскольку я не нашел способа передать нужные мне параметры в XJC в jaxws-maven-plugin.
Ниже приведен сконфигурированный maven-jaxb2-plugin; указание forceRegenerate=true обязательно, иначе плагин ничего не будет делать
         <plugin>
                <groupId>org.jvnet.jaxb2.maven2</groupId>
                <artifactId>maven-jaxb2-plugin</artifactId>
                <version>0.7.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <generateDirectory>
                        ${basedir}/target/generated-sources
                    </generateDirectory>
                    <args>
                        <arg>-XtoString</arg>
                        <arg>-b</arg>
                        <arg> src/main/resources/jaxb-binding-xsd4.xml</arg>
                        <arg>-b</arg>
                        <arg> src/main/resources/jaxb-binding-xsd9.xml</arg>
                        <arg>-b</arg>
                        <arg> src/main/resources/jaxb-java-datatypes.xml</arg>
                    </args>
                    <plugins>
                        <plugin>
                            <groupId>org.jvnet.jaxb2_commons</groupId>
                            <artifactId>jaxb2-basics</artifactId>
                            <version>0.5.3</version>
                        </plugin>
                    </plugins>
                    <forceRegenerate>true</forceRegenerate>
                </configuration>
            </plugin>

  •  После того, как клиентские классы были сгенерированы, можно вызывать методы веб-сервиса. В нижеприведенном примере проставляются таймауты и указывается расположение wsdl.
     try{
            System.setProperty("sun.net.client.defaultConnectTimeout", "5000");
            System.setProperty("sun.net.client.defaultReadTimeout", "50000");
            URL baseUrl = TimeoutTest.class.getResource(".");
            String WEBSERVICE_WSDL_LOCATION =  "service.wsdl";
            GetCountriesRequestMessage r = new GetCountriesRequestMessage();
            WebService service = new WebService(new URL(baseUrl, WEBSERVICE_WSDL_LOCATION), new QName("http://testuri.com/wcflib/", "WebService"));
            IWebService iface = service.getWS2007HttpBindingIWebService();
            iface.getCountries(r);

        }catch (WebServiceException ex){
            if(ex.getCause() instanceof SocketTimeoutException){
                if(ex.getCause().getMessage().contains("connect timed out")){
                    System.out.println("Connection timeout");
                }else{
                    System.out.println("READ timeout! " + ex.getCause().getMessage());
                }
            }else if(ex.getCause() instanceof UnknownHostException) {
                System.out.println("No route to host! " + ex.getCause().getMessage());
            }else{
                ex.printStackTrace();
            }
        }catch (Exception ex){
            ex.printStackTrace();
        } 


В случае правильной конфигурации при запуске тестов в логе появится следующее:
Apr 30, 2013 2:53:59 PM [com.sun.xml.ws.policy.parser.PolicyConfigParser]  parse
INFO: WSP5018: Loaded WSIT configuration from file: file:/home/grigory/dev/metro/target/test-classes/wsit-client.xml.
Apr 30, 2013 2:54:01 PM com.sun.xml.ws.security.opt.impl.util.CertificateRetriever setServerCertInTheContext
INFO: WSS0824: The certificate found in the server wsdl or by server cert property is valid, so using it

четверг, 11 апреля 2013 г.

wsdl2java: Two declarations cause a collision in the ObjectFactory class.

1. Получил странную ошибку при генерации: Two declarations cause a collision in the ObjectFactory class.
2. Непонятно где ошибка, поскольку xsd без перевода строки
3. Скачал локально, отформатировал, переписал все импорты в xsd и wsdl
4. Проблемных мест было два, в первой xsd:

первое объявление
<xs:element name="PersonAddress" nillable="true" type="tns:PersonAddress"/>

второе объявление
<xs:complexType name="Person">
        <xs:sequence>
            <xs:element minOccurs="0" name="Address" nillable="true" type="tns:PersonAddress"/>
   skip

во второй:

<xs:element name="BankFlags" nillable="true" type="tns:BankFlags"/>
<xs:complexType name="Bank">
        <xs:sequence>
            <xs:element minOccurs="0" name="Flags" nillable="true" type="tns:BankFlags"/>


skip

т.е ObjectFactory генерируется не совсем корректно для подобных случаев.

5. Решение нашлось тут: http://stackoverflow.com/questions/13422253/xjc-two-declarations-cause-a-collision-in-the-objectfactory-class

6. Создал фалы биндингов для этих случаев:
jaxb-binding-xsd4.xml

<jaxb:bindings xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
               xmlns:xs="http://www.w3.org/2001/XMLSchema"
               version="1.0">
    <jaxb:bindings schemaLocation="xsd4.xsd">
        <jaxb:bindings node="//xs:element[@name='PersonAddress']">
            <jaxb:factoryMethod name="TypePersonAddress"/>
        </jaxb:bindings>
    </jaxb:bindings>
</jaxb:bindings>

и
jaxb-binding-xsd9.xml

<jaxb:bindings xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
               xmlns:xs="http://www.w3.org/2001/XMLSchema"
               version="1.0">
    <jaxb:bindings schemaLocation="xsd9.xsd">
        <jaxb:bindings node="//xs:element[@name='BankFlags']">
            <jaxb:factoryMethod name="TypeBankFlags"/>
        </jaxb:bindings>
    </jaxb:bindings>
</jaxb:bindings>

7. Прописываем параметры в pom.xml

<plugin>
                <groupId>org.apache.cxf</groupId>
                <artifactId>cxf-codegen-plugin</artifactId>
                <version>2.7.3</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>wsdl2java</goal>
                        </goals>
                        <configuration>
                            <sourceRoot>${basedir}/target/generated-sources</sourceRoot>
                            <wsdlOptions>
                                <wsdlOption>
                                    <extraargs>
                                        <extraarg>-verbose</extraarg>                                        <extraarg>-b</extraarg>
                                        <extraarg>${basedir}/src/main/resources/jaxb-binding-xsd4.xml</extraarg>
                                        <extraarg>-b</extraarg>
                                        <extraarg>${basedir}/src/main/resources/jaxb-binding-xsd9.xml</extraarg>
                                    </extraargs>
                                    <wsdl>
                                        ${basedir}/src/main/resources/service.wsdl
                                    </wsdl>
                                    <wsdlLocation>classpath:service.wsdl</wsdlLocation>
                                </wsdlOption>
                            </wsdlOptions>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
8. Классы сгенерировались, ура!

четверг, 7 марта 2013 г.

HTTPS в Java

В этой заметке расскажу о, на первый взгляд, простой вещи - https соединении.


HTTPS - это по сути обычный HTTP, но данные передаются в зашифрованном виде с помощью SSL/TLS.


Установка соединения происходит в два этапа, соединение всегда инициирует клиент:
  1. Handshake
  2. Data 
На этапе handshake сторонам необходимо договориться о том, какая версия протокола и набор шифров будет использоваться (клиент предлагает список и сервер выбирает из него), а также произвести аутентификацию сервера (обязательно) и клиента (опционально).  После этого стороны договариваются о сессионном ключе (ключах). Поскольку асимметричные алгоритмы шифрования довольно затратны по ресурсам, для передачи данных используются обычно симметричные алгоритмы, которые менее ресурсоемки.

После этого осуществляется собственно обмен данными, зашифрованными с помощью сессионного ключа.

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

В java существует специальное хранилище cacerts, в котором уже находятся многие удостоверяющие центры. Это так называемые root CA, которым безусловно доверяют.

При получении серверного сертификата (цепочки сертификатов) клиент проверяет, приводит ли цепочка сертификатов к известному ему CA, которому он доверяет, и если он не находит такую цепочку, выбрасывается эксепшн вида
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target.

Так что пути решения проблемы два:
  • Добавить серверный сертификат в cacerts
Для этого необходимо скачать сертификат с сайта с помощью браузера или любого из описанного здесь методов: http://serverfault.com/questions/139728/how-to-download-ssl-certificate-from-a-website например
$openssl s_client -connect ${REMHOST}:${REMPORT} и скопировать в файл все что между строк -----BEGIN CERTIFICATE----- и -----END CERTIFICATE----- (включая их)
Полученный файл сертификата нужно добавить к cacerts командой
$keytool -import -keystore cacerts -alias [myalias] -file server_cert.pem
  • Установить trustManager при создании sslContext.

          SSLSocketFactory socketFactory = createSSLContext().getSocketFactory();

            HttpsURLConnection connection = (HttpsURLConnection) (url).openConnection();
            connection.setSSLSocketFactory(socketFactory);


      private final SSLContext createSSLContext()
            throws NoSuchAlgorithmException, KeyStoreException,
            CertificateException, IOException,
            UnrecoverableKeyException, KeyManagementException {


        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        FileInputStream in = new FileInputStream("server.pem");
        KeyStore trustStore = KeyStore.getInstance("JKS");
        trustStore.load(null);
        try {
            X509Certificate cacert = (X509Certificate) cf.generateCertificate(in);
            trustStore.setCertificateEntry("server_alias", cacert);
        } finally {
            IOUtils.closeQuietly(in);
        }

        TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
        tmf.init(trustStore);

        SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());
        return sslContext;
    }


О том, как предоставить клиентский сертификат, в следующий раз.

Полезные ссылки:
http://www.zytrax.com/tech/survival/ssl.html