Недавно возникла небольшая проблема: как добавить свой спринговый сервлет в 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>
понедельник, 2 сентября 2013 г.
пятница, 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=Укажите значение
Результат:
Добавляем нашу любимую библиотечку со стилями, например 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();
При использовании 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();
Ярлыки:
java,
security,
shiro,
tapestry-security,
tapestry5
вторник, 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 сервиса появятся изменения.
Итак, вот решение:
<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>
...
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 для вашего сервиса
<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>
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
Документации не сказать чтобы совсем никакой, но по части 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
<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 для всех методов на одну-единственную, следующего вида:
...
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.
<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.
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. Классы сгенерировались, ура!
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.
Установка соединения происходит в два этапа, соединение всегда инициирует клиент:
После этого осуществляется собственно обмен данными, зашифрованными с помощью сессионного ключа.
С практической точки зрения клиенту необходимо только решить, доверяет ли он серверу или нет, и предоставить свой сертификат, если нужно.
В java существует специальное хранилище cacerts, в котором уже находятся многие удостоверяющие центры. Это так называемые root CA, которым безусловно доверяют.
При получении серверного сертификата (цепочки сертификатов) клиент проверяет, приводит ли цепочка сертификатов к известному ему CA, которому он доверяет, и если он не находит такую цепочку, выбрасывается эксепшн вида
Так что пути решения проблемы два:
$openssl s_client -connect ${REMHOST}:${REMPORT} и скопировать в файл все что между строк -----BEGIN CERTIFICATE----- и -----END CERTIFICATE----- (включая их)
Полученный файл сертификата нужно добавить к cacerts командой
$keytool -import -keystore cacerts -alias [myalias] -file server_cert.pem
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
HTTPS - это по сути обычный HTTP, но данные передаются в зашифрованном виде с помощью SSL/TLS.
Установка соединения происходит в два этапа, соединение всегда инициирует клиент:
- Handshake
- Data
После этого осуществляется собственно обмен данными, зашифрованными с помощью сессионного ключа.
С практической точки зрения клиенту необходимо только решить, доверяет ли он серверу или нет, и предоставить свой сертификат, если нужно.
В java существует специальное хранилище cacerts, в котором уже находятся многие удостоверяющие центры. Это так называемые root CA, которым безусловно доверяют.
При получении серверного сертификата (цепочки сертификатов) клиент проверяет, приводит ли цепочка сертификатов к известному ему CA, которому он доверяет, и если он не находит такую цепочку, выбрасывается эксепшн вида
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target.
Так что пути решения проблемы два:
- Добавить серверный сертификат в cacerts
$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
Подписаться на:
Сообщения (Atom)