[[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() { public Object run() { //Do something return null; } }); ctx.logout(); ===== Авторизация ===== ==== Краткое описание проверки пермишена ==== В 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).