[[start|На главную]]
====== Реализация системы безопасности в Java ======
Всё нижеописанное исходит из предположения что в Sun хорошо подумали когда
проектировали пермишены, секьюрити-менеджер и всё с ними связанное.
===== О чём это вообще =====
В типичном веб-приложении далеко не каждый пользователь может менять любые данные. У кого-то есть доступ только на чтение, кто-то может менять только свои данные, кто-то только данные пользователей из определённой группы или находящиеся в определённом состоянии. Возникает задача разграничивания прав. Есть несколько вариантов её решения. Вариант первый - логика разрешения/запрещения жёстко задаётся в коде. Существует жёстко заданный набор групп с наборами правам. Этот подход работает только до тех пор пока не возникает необходимости изменить эти наборы. Другой подход связан с динамическим созданием групп и динамическим же назначением им прав. И логика по проверке просто проверяет, есть ли у текущего пользователя право на то действие, которое он пытается совершить.
==== Что защищать ====
Любые данные.
==== Зачем защищать ====
Чтобы разделять, кому что можно, а кому что нельзя делать.
===== Аутентификация =====
==== Несортированный трэш ====
//Authorization init
Policy.setPolicy(new Policy()
{
public boolean implies(final ProtectionDomain domain, final Permission permission)
{
for (final Principal principal : domain.getPrincipals()) {
if (principal instanceof MyPrincipal) {
//получить откуда-то коллекцию прав принципала и вызвать для них implies, передав в качестве аргумента проверяемый пермишен.
}
}
return false;
}
});
//Authentication init
final Configuration orig = Configuration.getConfiguration();
Configuration.setConfiguration(new Configuration()
{
public AppConfigurationEntry[] getAppConfigurationEntry(final String name)
{
return (name.equals("MY_APP")) ?
new AppConfigurationEntry[]{
new AppConfigurationEntry(
"com.myApp.security.MyLoginModule", //наша реализация LoginModule.
AppConfigurationEntry.LoginModuleControlFlag.REQUISITE,
Collections.emptyMap()
)
} : orig.getAppConfigurationEntry(name);
}
public void refresh()
{
orig.refresh();
}
});
System.setSecurityManager(new SecurityManager());
//Аутентификация
final String name = "login";
final String password = "password";
final LoginContext ctx = new LoginContext("MY_APP", new CallbackHandler()
{
public void handle(final Callback[] callbacks) throws IOException, UnsupportedCallbackException
{
for (final Callback cb : callbacks)
{
if (cb instanceof NameCallback)
{
((NameCallback) cb).setName(name);
}
else if (cb instanceof PasswordCallback)
{
((PasswordCallback) cb).setPassword(password.toCharArray());
}
else
{
throw new UnsupportedCallbackException(cb);
}
}
}
});
ctx.login();
//выполнение кода в контексте аутентифицированного пользователя
Subject.doAs(ctx.getSubject(), new PrivilegedAction
===== Авторизация =====
==== Краткое описание проверки пермишена ====
В Java существует [[http://java.sun.com/j2se/1.5.0/docs/guide/security/spec/security-specTOC.fm.html|встроенный механизм прав]].
* Проверяется, не равен ли ''null'''у ''System#getSecurityManager()''.
* Если равен, то считается что действие разрешено делать.
* Вызывается [[http://java.sun.com/j2se/1.5.0/docs/api/java/lang/SecurityManager.html#checkPermission(java.security.Permission)|SecurityManager#checkPermission(java.security.Permission)]]
* Берётся текущий ''AccessControlContext'' и вызывается у него метод [[http://java.sun.com/j2se/1.5.0/docs/api/java/security/AccessControlContext.html#checkPermission(java.security.Permission)|AccessControlContext#checkPermission(java.security.Permission)]]
* ''AccessControlContext'' итерейтится по содержащимся в нём ''ProtectionDomain'''ам и у них вызывается метод [[http://java.sun.com/j2se/1.5.0/docs/api/java/security/ProtectionDomain.html#implies(java.security.Permission)|ProtectionDomain#implies(java.security.Permission)]]
* Берётся текущая ''Policy'', у которой вызывается метод [[http://java.sun.com/j2se/1.5.0/docs/api/java/security/Policy.html#implies(java.security.ProtectionDomain, java.security.Permission)|Policy#implies(java.security.ProtectionDomain, java.security.Permission)]] с данным ''ProtectionDomain''ом.
==== Компоненты системы авторизации ====
* [[http://java.sun.com/j2se/1.5.0/docs/api/java/lang/SecurityManager.html|SecurityManager]] - просто фасад над всей системой прав.
* [[http://java.sun.com/j2se/1.5.0/docs/api/java/security/AccessControlContext.html|AccessControlContext]] - контекст с информацией о том, в каких ''ProtectionDomain'''ах выполняется сейчас код.
* [[http://java.sun.com/j2se/1.5.0/docs/api/java/security/ProtectionDomain.html|ProtectionDomain]] - содержит информацию об URL'ах (возможно с сертификатами), из которых получен выполняющийся код, ''ClassLoader'', через который этот код был получен и (**внимание**) коллекцию [[http://java.sun.com/j2se/1.5.0/docs/api/java/security/Principal.html|Principal]]'ов, от имени которых код выполняется.
* [[http://java.sun.com/j2se/1.5.0/docs/api/java/security/Policy.html|Policy]] - хранилище пермишенов, которое определяет какие права у кого есть.
==== Внедрение своих правил авторизации ====
=== Principal ===
Скорее всего у вас в приложении есть класс ''User''. Нужно будет написать реализацию ''Principal'''а для него (Внимание! Не надо делать так чтобы ваш ''User'' реализовывал ''Principal'''а, т.к. отсутствие юзера не означает отсутствие ''Principal'''а).
== Примерная реализация ==
public class UserPrincipal implements java.security.Principal {
// ------------------------------ FIELDS ------------------------------
/**
* Константа для незарегистрированного пользователя.
*/
private static final UserPrincipal nullUserPrincipal = new UserPrincipal (null);
private final User user;
// -------------------------- STATIC METHODS --------------------------
public static UserPrincipal getPrincipal (final User user) {
return user == null ? nullUserPrincipal : new UserPrincipal (user);
}
// --------------------------- CONSTRUCTORS ---------------------------
protected UserPrincipal (final User user) {
super ();
this.user = user;
}
// ------------------------ CANONICAL METHODS ------------------------
public boolean equals (final Object obj) {
if (obj == null || !(obj instanceof UserPrincipal)) {
return false;
}
final UserPrincipal o = (UserPrincipal) obj;
final User myUser = this.getUser ();
final User oUser = o.getUser ();
if (myUser == null && oUser == null) {
return true;
} else if (myUser != null && oUser != null) {
return myUser.equals (oUser);
} else {
return false;
}
}
public int hashCode () {
return this.getClass ().hashCode () + (this.user == null ? 13 : this.user.hashCode ());
}
// ------------------------ INTERFACE METHODS ------------------------
// --------------------- Interface Nullable ---------------------
public boolean isNull () {
return this.user == null;
}
// --------------------- Interface Principal ---------------------
public String getName () {
return this.getClass ().getName () + " " + (this.user == null ? "unregistered" : this.getUser ().getUserId ());
}
// -------------------------- OTHER METHODS --------------------------
public User getUser () {
return this.user;
}
}
=== Policy ===
''Policy'' - это как раз то, куда надо всовывать свой код, чтобы делать проверку прав по своей базе, а не по стандартной реализации в policy-файле. Делается это просто - пишется свой класс, наследующийся от ''Policy'' (внимание, если в вашем приложении права могут динамически изменяться, то надо переопределять метод ''implies'', т.к. в стандартной реализации он активно занимается кэшированием пермишенов.
> **ВНИМАНИЕ** Считается, что право разрешено, если оно разрешено для **ВСЕХ** ProtectionDomain'ов, находящихся в ''AccessControlContext'''е вызывающего кода.
=== Собираем вместе ===
== Установка Policy ==
Policy.setPolicy (new MyPolicy ());
== Включение SecurityManager'а ==
if (System.getSecurityManager () != null) {
System.setSecurityManager (new SecurityManager ());
}
== Вызов защищённого кода ==
try {
final Subject subject = ...;
final T result = Subject.doAs (subject, new PrivilegedExceptionAction() {
public Object run () throws IOException, ServletException {
//выполнить какой-то код, защищённый правами.
}
});
} catch (PrivilegedActionException e) {
...
}
== SecurityUtils ==
Пара методов для удобства.
public final class SecurityUtils {
public static boolean hasPermission (final Permission permission) {
try {
checkPermission (permission);
return true;
} catch (final SecurityException e) {
return false;
}
}
public static void checkPermission (final Permission permission) throws SecurityException {
if (System.getSecurityManager () != null) {
System.getSecurityManager ().checkPermission (permission);
}
}
}
===== Альтернативы =====
* [[http://acegisecurity.org/|Acegi Security]]
FIXME
===== Permissions Evangelizm =====
FIXME
====== JAAS vs Acegi Security ======
===== Java Security (JAAS) =====
{{jsf-ru:plus.gif}} Является стандартом, поддерживается большим количеством технологий (EJB, Servlets, etc)
{{jsf-ru:plus.gif}} Простой API
{{jsf-ru:plus.gif}} Хорошо документирован
{{jsf-ru:minus.gif}} Довольно скудная базовая функциональность
{{jsf-ru:minus.gif}} Ограниченное встроенное кэширование (не предполагает изменения набора прав в процессе работы приложения), в случае отключения которого необходимо реализовывать кэширование на уровне приложения
{{jsf-ru:minus.gif}} Отсутствуют теги для проверки прав к различным элементам интерфейса (однако возможно их написание)
{{jsf-ru:minus.gif}} Политики принятия решений необходимо реализовывать вручную
{{jsf-ru:minus.gif}} В некоторых случаях контейнеро-зависим (однако возможно написание контейнеро-независимого кода)
===== Acegi Security =====
{{jsf-ru:plus.gif}} Хорошо документирован
{{jsf-ru:plus.gif}} Простая интеграция со Spring
{{jsf-ru:plus.gif}} Встроенные аудиты
{{jsf-ru:plus.gif}} Встроенные интерцепторы (например, для удаления из коллекций недоступных объектов)
{{jsf-ru:plus.gif}} Различные политики принятия решений (если все полиси разрешают, если хоть одна полиси разрешает, на основе голосования и очков)
{{jsf-ru:plus.gif}} Встроенные теги для проверки прав к различным элементам интерфейса (правда примитивные)
{{jsf-ru:plus.gif}} Встроенное кэширование
{{jsf-ru:plus.gif}} Контейнеро-независим
{{jsf-ru:plus.gif}} Встроенная поддержка опции «запомнить меня»
{{jsf-ru:plus.gif}} Множество адаптеров для различных механизмов аутентификации
{{jsf-ru:minus.gif}} Многократно более сложный API
{{jsf-ru:minus.gif}} Не специфицирован
При этом следует учитывать, что имеется возможность оборачивать использование сторонним кодом JAAS’а в обращения к Acegi и наоборот. Так же необходимо понимать, что права виртуальной машины (доступ к файлам, сокетам, работа с класслоадерами и т.д. проверяются через JAAS, поэтому при использовании Acegi в любом случае будет необходимо направлять в него обращения к JAAS).