Существует несколько различных способов обеспечения безопасности в приложениях на 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();
Комментариев нет:
Отправить комментарий