Как стать автором
Обновить

Применение JAAS в Web-приложениях на glassfish v2

Время на прочтение13 мин
Количество просмотров18K
На этот раз хочется написать про применение JAAS (Java Authentification and Authorization Service) для веб-приложений. Для начала рассмотрим простой контроль доступа к веб-ресурсам и авторизацию. Я попытаюсь раскрыть основную идею, а также дам подсказку по способу развёртывания (позже из текста станет понятно в чём проблема).



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

Когда веб-контейнер «тречит» сессии всех подключающихся к нему машин, то с каждой такой сессией могут быть ассоциированы роли, а также user principal. Каждая роль связана с возможностью или невозможностью доступа к некоторому набору веб-ресурсов, а также осуществления некоторых действий (privileged action).

Изначально сессия пользователя не связана ни с каким user principal'ом и ни с одной ролью. Это означает, что если на доступ к некоторому набору веб-ресурсов наложено ограничениче (constraint), то он не будет иметь доступ к этим ресурсам.

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

Авторизация осущестляется с помощью JAAS. Во время авторизации пользователь посылает свои креденциалы (credentials, в простом случае это могут быть логин: пароль или пользовательский сертификат). На сервере с процессом контроля связаны две сущности, а именно Realm и Login Module. Login Module осуществляет проверку связи пользователя с каким-то набором групп пользователей (не путать с ролями). Кроме того, авторизация может быть пройдена успешно, однако, пользователь может быть не связан ни с одной из групп.

Веб-приложение может определять соотношение между группами и ролями. Приложение задаёт это соответствие с помощью sun-web.xml.

Login Module и Realm не являются частью веб-приложения, а являются разделяемыми ресурсами сервера приложений, так что они должны быть в classpath сервера и должны быть соответствующим образом зарегистрировны в сервере (в login.conf и domain.xml). Приложение выбирает realm по имени через web.xml.

Приложение определяет набор ограничений на доступ к своим ресурсам через web.xml и указывает: какие веб-реурсы (по шаблону URL (url pattern)) могут быть доступны для каких видов запросов (get, post, head, etc) и для каких ролей. Кроме того, приложение определяет способ авторизации. Например, можно указать страницу логина или задать использование стандартной HTTP-авторизции (браузер показывает окно авторизации).

За выполнением ограничений средит веб-контейнер, так что приложению не требуется выполнять дополнительные проверки. Поскольку ограничения накладываются на шаблон URL, то не имеет значения что подпадает под этот шаблон, будь то сервлет, JSP или же даже ничего (404). Даже если ничего (404), а ограничение требует наличия у пользователя некоторой роли, то ему придётся пройти авторизацию для того чтобы увидеть, что там ничего нет.

Каждый realm может быть ассоциирован с каким-нибудь Login Module. Для этого, в свойствах (properties) для него указывает специальная пара «jaas-context» -> «login-module-name». Иногда Realm и Login Module работают только в паре. Например, если мы захотим написать свой Login Module и Realm, то вполне возможно захочется чтобы наш Realm мог работать только с нашим же Login Module и ни с каким другим.

Для примера создадим простую пару для авторизации по паролю.
Начнём с написания Realm'а:
public class TestRealm extends AppservRealm {

    @Override
    public String getAuthType() {
        return "magic";
    }

    @Override
    public Enumeration getGroupNames( String string ) throws InvalidOperationException, NoSuchUserException {
        return Collections.enumeration( Arrays.asList( "users", "guests" ) );
    }

    @Override
    protected void init( Properties props ) throws BadRealmException, NoSuchRealmException {
        super.init( props );

        System.err.println( "Realm:: Hello!!" );

        if( props.containsKey( JAAS_CONTEXT_PARAM ) )
            setProperty( JAAS_CONTEXT_PARAM, props.getProperty( JAAS_CONTEXT_PARAM ) );
    }

    @Override
    public AuthenticationHandler getAuthenticationHandler() {
        return null;
    }

}



Метод getGroupNames возвращает enum с возможными группами. Метод init вызывается при инициализации (по факту — при старте домена). В нём мы можем выполнить требуемую инициализацию (например, достать необходимые паретры из props, а затем куда-то сохранить их для последующего использования). Очень важно не забыть передать свойство JAAS_CONTEXT_PARAM («jaas-context»), иначе реальм не будет работать.

Далее, опишем наш Login Module. Будем использовать авторизацию по паролю.
public class TestLoginModule extends AppservPasswordLoginModule {

    @Override
    protected void authenticateUser() throws LoginException {
        if( _username == null || _password == null )
            throw new LoginException( "Username of password is null" );

        if( "user".equals( _username ) && "user-pass".equals( _password ) )
            commitUserAuthentication( new String[]{ "users" } );
        else if ( "guest".equals( _username ) && "guest".equals( _password ) )
            commitUserAuthentication( new String[]{ "guests" } );
        else
            throw new LoginException( "bad login/password" );
    }

}


В этом примере мы просто «забили» варианты паролей. Вместо этого можно было бы «спросить» у БД или у LDAP-сервера, а может, даже, и то, и другое.

После сборки получим простой jar с двумя классами (для успешной сборки в classpath надо иметь javaee.jar, appserv-rt.jar и appserv-ext.jar и директории glassfish-v2/lib).

После сборки, наш jar можно положить, например, в директорию lib домена.

Далее, Realm и Login Module следует зарегистрировать в домене.
Для начала, надо позаботиться о том, чтобы наш .jar был в classpath сервера. Вероятно, существует неколько способов это сделать надлежащим образом, однако, в документации по теме описывается один конкретный, вот его-то мы и будем использовать.

В файле domain.xml среди всего прочего описываются параметры jvm. Там можно найти тэг java-config. У него есть аттрибут classpath-suffix. Обычно он пуст. Вот туда-то нам и надо «вписаться».
Например:
...
<java-config classpath-suffix="/home/cy6ergn0m/.domains/domain1/lib/MyTestRealm.jar"  ...
....


После этого, надо добавить наш Login Module. Это делается в текстовом файле login.conf в директории домена (domain_dir/conf/login.conf). Можно просто дописать в конец, например, так:

testRealmLM {
        cy6ergn0m.auth.TestLoginModule required;
};



Когда метод логина описан, можно зарегистрировать наш именованный Realm. Его можно добавить в domain.xml или через web admin console. Мы сделаем это через domain.xml.

Среди тэгов верхнего уровня можно найти тэг security-service. Обычно в нём уже есть несколько тэгов auth-realm. Мы добавим туда же и свой. Однако, важно, чтобы наш тэг был после auth-realm'ов, которые уже есть, т.к. схема domain.xml требует, чтобы auth-realm'ы были в начале.
<auth-realm classname="cy6ergn0m.auth.TestRealm" name="testRealm">
          <property name="jaas-context" value="testRealmLM"/>
          <property name="auth-type" value="magic"/>
</auth-realm>


Среди параметров (properties) мы можем передать и другие. Эти свойства потом попадают в метод init нашего Realm'а. Сюда мы можем положить, например, адрес\порт сервера или ещё какие-то настройки.

Следует также заметить, что можно создать несколько реалмов, но с разными параметрами. Login Module в свою очередь всегда может узнать, который из реалмов его использует с помощью protected-поля _currentRealm. Также, таким способов он может получить у реалма какие-то специфичные настройки (хост, порт, и т.п.).

Теперь, следует перезапутить домен. При старте домена в логе мы сможем увидеть нашу запись: «Realm:: Hello!!». Если этого не произошло, то скорее всего вы что-то сделали неверно.

Итак, наш модуль авторизации готов к употреблению. Попробуем воспользоваться им.

Для этого напишем крошечное web приложение, которое будет использовать наш модуль.

Создадим несколько web-страниц, для начала сделаем три: index.html, secret.html и for-guests.html.
Сделаем ссылки с index.html на остальные две.

После этого, создадим ограничения на доступ к нашим «страшно секретным» страницам. Для этого придётся редактировать файлы web.xml и sun-web.xml.

Наш модуль авторизации умеет авторизовать пользователей двух групп: «user» и «guest. Давайте создадим две роли соответствующие этим группам.

Откроем файл sun-web.xml и добавим эти соотношения. Для этого, добавим в него две секции <security-role-mapping>:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sun-web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 Servlet 2.5//EN" "http://www.sun.com/software/appserver/dtds/sun-web-app_2_5-0.dtd">
<sun-web-app error-url="">
  <context-root>/TestRealmClient</context-root>
  <security-role-mapping>
    <role-name>user</role-name>
    <group-name>users</group-name>
  </security-role-mapping>
  <security-role-mapping>
    <role-name>guest</role-name>
    <group-name>guests</group-name>
  </security-role-mapping>
  <class-loader delegate="true"/>
  <jsp-config>
    <property name="keepgenerated" value="true">
      <description>Keep a copy of the generated servlet class' java code.</description>
    </property>
  </jsp-config>
</sun-web-app>


Теперь опишем те роли, которые хотим использовать в web.xml:
    <security-role>
        <description/>
        <role-name>user</role-name>
    </security-role>
    <security-role>
        <description/>
        <role-name>guest</role-name>
    </security-role>


Теперь можно создавать ограничение (constraint). Для этого исправим web.xml и добавим в него ограничения:
    <security-constraint>
        <display-name>Constraint1</display-name>
        <web-resource-collection>
            <web-resource-name>secrets</web-resource-name>
            <description/>
            <url-pattern>/secret*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <description/>
            <role-name>user</role-name>
        </auth-constraint>
    </security-constraint>
    <security-constraint>
        <display-name>Constraint2</display-name>
        <web-resource-collection>
            <web-resource-name>guests</web-resource-name>
            <description/>
            <url-pattern>/for-guests*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <description/>
            <role-name>guest</role-name>
            <role-name>user</role-name>
        </auth-constraint>
    </security-constraint>


Таким образом мы определили, что доступ к страницам /secret* будет только для пользователей с ролью user, а адреса /for-guests* для пользователей с ролями user и guest. Это означает, что пользователь с ролью user может посещать все страницы, пользователь с ролью guest только index.html и for-guests.html, а „никто“ — только index.html.

Теперь мы должны указать realm и способ авторизации. Для начала выберем простой способ (HTTP-авторизация) — BASIC. Для этого в web.xml укажем:
    <login-config>
        <auth-method>BASIC</auth-method>
        <realm-name>testRealm</realm-name>
    </login-config>


Таким образом, в конце у нас получится следующий web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>
    <security-constraint>
        <display-name>Constraint1</display-name>
        <web-resource-collection>
            <web-resource-name>secrets</web-resource-name>
            <description/>
            <url-pattern>/secret*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <description/>
            <role-name>user</role-name>
        </auth-constraint>
    </security-constraint>
    <security-constraint>
        <display-name>Constraint2</display-name>
        <web-resource-collection>
            <web-resource-name>guests</web-resource-name>
            <description/>
            <url-pattern>/for-guests*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <description/>
            <role-name>guest</role-name>
            <role-name>user</role-name>
        </auth-constraint>
    </security-constraint>
    <login-config>
        <auth-method>BASIC</auth-method>
        <realm-name>testRealm</realm-name>
    </login-config>
    <security-role>
        <description/>
        <role-name>user</role-name>
    </security-role>
    <security-role>
        <description/>
        <role-name>guest</role-name>
    </security-role>
</web-app>


В редакторе web.xml в netbeans это будет выглядеть следующим образом:

web.xml in netbeans

Готово. Теперь приложение можно запустить и проверить. При попытке перейти по одной из ссылок возникнет окно авторизации.

Давайте усложним задачу. Допустим, мы хотим сами сделать свою собственную страницу логина.

Достичь этого мы можем двумя способами. Расмотрим оба.

Для начала мы должны изменить способ авторизации в web.xml. Ранее вы указывать метод авторизции BASIC. Теперь мы выберем логин-форму.
    <login-config>
        <auth-method>FORM</auth-method>
        <realm-name>testRealm</realm-name>
        <form-login-config>
            <form-login-page>/login.jsp</form-login-page>
            <form-error-page>/login.jsp?fail</form-error-page>
        </form-login-config>
    </login-config>


Теперь создадим форму логина login.jsp согласно первому способу, более простому:
<%@page contentType="text/html" pageEncoding="windows-1251"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
        <title>JSP Page</title>
    </head>
    <body>
        <h1>Hello World!</h1>

        <h2>Web container's login method</h2>
        <form action="j_security_check" method="post">
            <input type="text" name="j_username" />
            <input type="password" name="j_password" />
            <input type="submit" />
        </form>
    </body>
</html>


Готово. Теперь, когда неавторизованный пользователь будет пытаться пройти на „защищённые“ страницы, веб-контейнер будет „редиректить“ его на нашу форму логина. После авторизции пользователь получит необходимые роли и сможет посещать „защищённые“ страницы.

В некоторых случаях нам нужем больший контроль за процессом авторизации и какой-то неконтролируемый нами j_security_check нас не устраивает. В таком случае, мы всегда можем воспользоваться вторым способом. Мы можем вручную выполнить логин. Поскольку Glassfish использует нестандартный логин-модуль (не случайно мы реализовывали AppservPasswordLoginModule вместо стандартного JAAS интерфейса), то мы не можем воспользоваться обычным путём авторизации через LoginContext, а должны использовать нестандартное API сервера (точнее можем, но это сложнее и всё равно непереносимо).

Мы создадим LoginServlet, который будет использовать ProgrammaticLogin из API Glassfish'а [7]. Следует обратить внимание, что для того, чтобы класс бы виден, необходимо добавить файлы javaee.jar, appserv-rt.jar и appserv-ext.jar в classpath при сборке (и в вашей IDE, что возможно одно и то же).

public class LoginServlet extends HttpServlet {


    @Override
    protected void doGet( HttpServletRequest request, HttpServletResponse response )
            throws ServletException, IOException {
        response.sendError( HttpServletResponse.SC_FORBIDDEN );
    }

    @Override
    protected void doPost( HttpServletRequest request, HttpServletResponse response )
            throws ServletException, IOException {
        ProgrammaticLogin pl = new ProgrammaticLogin();
        try {
            Boolean rc = pl.login( request.getParameter( "name"), request.getParameter( "pass"), "testRealm", request, response, true );
            if( rc != null && rc.booleanValue() ) {
                response.sendRedirect( "index.jsp" );
                return;
            }
        } catch( Exception ex ) {
            Logger.getLogger( LoginServlet.class.getName() ).log( Level.SEVERE, null, ex );
        }

        response.sendRedirect( "login.jsp?fail" );
    }

    @Override
    public String getServletInfo() {
        return "Login servlet";
    }

}


А страница login.jsp изменим соответственно так, чтобы параметры формы направляли в LoginSerlvet

<%@page contentType="text/html" pageEncoding="windows-1251"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
        <title>JSP Page</title>
    </head>
    <body>
        <h1>Hello World!</h1>

        <h2>Web container's login method</h2>
        <form action="login" method="post">
            <input type="text" name="name" />
            <input type="password" name="pass" />
            <input type="submit" />
        </form>
    </body>
</html>


Таким образом, получим сервлет логина, который можем использовать любым удобным для нас способом, например, мы можем ещё сделать „каптчу“, или проверять логин аяксом и многое другое.

В заключение хочется осветить ещё один момент. Дело в том, что регистрация Realm и Login Module не слишком простая, то хотелось бы иметь возможность автоматически регистрировать их в произвольном домене.

Для реализации задумки можно сделать свою собственную „таску“ (Custom Task) для Ant. Её можно написать на Java и выполнить все необходимые действия.

По служебному долгу пришлось написать подобную вещь, но, по понятным причинам не могу привести тут этот код. Однако, дам несколько намёков о её содержании. К файлу login.conf можно легко дописать пару строчек через FileWriter, созданный с парметром append=true. С манипуляциями над domain.xml сложнее, однако, это ведь XML, к тому же обозримого размера… стало быть, можно с помощью DocumentBuilder'а прочеть весь XML в DOM-дерево, потом внести необходимые поправки в дерево, добавить classpath-suffix и auth-realm, а потом серилизовать DOM-дерево в domain.xml обратно. У меня получилось уложиться в 200 строк вместе с проверками входных параметов. Такую таску можно включить в ant-скрипты создания всего домена (если домен создаётся ant'ом).

Несмотря на то, что тестировал я всё в glassfish v2, однако, судя по разного рода источникам, можно надеяться, что для v3 это также актуально, хотя возможно потребует корректировок.

На этом непростая статья подошла к концу. Конечно, многое осталось за кадром (privileged actions, user principals), но я надеюсь, что мне удалось пролить свет на эту непростую нишу веб-разработки.

Успехов.

CG.

Список литературы ака ссылки:

1. Authentication Using Custom Realms in Sun Java System Application Server
developers.sun.com/appserver/reference/techart/as8_authentication

2. Using JAAS with Tomcat
www.kopz.org/public/documents/tomcat/jaasintomcat.html

3. JAAS Tomcat Login Module
www.owasp.org/index.php/JAAS_Tomcat_Login_Module

4. JavaTM Authentication and Authorization Service (JAAS). LoginModule Developer's Guide.
java.sun.com/javase/6/docs/technotes/guides/security/jaas/JAASLMDevGuide.html

5. Sun GlassFish Enterprise Server v3 Application Development Guide
Chapter 5 Securing Applications
docs.sun.com/app/docs/doc/820-7695/beabg?l=ru&q=glassfish+JAAS&a=view

6. Ветка на форуме про веб-логин через j_security_check
www.sql.ru/Forum/actualthread.aspx?tid=624508

7. Javadoc Class ProgrammaticLogin
glassfish.dev.java.net/nonav/docs/v3/api

8. Sun GlassFish Enterprise Server v3 Application Development Guide
Programmatic Login
docs.sun.com/app/docs/doc/820-7695/beacm?l=ru&a=view

9. Implement JAAS based Authentication and Authorization for ADF Faces applications on OC4J 10.1.3
technology.amis.nl/blog/1426/implement-jaas-based-authentication-and-authorization-for-adf-faces-applications-on-oc4j-1013

10. Securing a Web Application on Glassfish using JAAS — Part 1 and 2
www.developinjava.com/features/47-enterprise-java/105-securing-a-web-application-on-glassfish-using-jaas.html
www.developinjava.com/features/47-enterprise-java/106-securing-a-web-application-on-glassfish-using-jaas-pt-2.html

11. Glassfish javadoc: Class AppservRealm
glassfish.java.net/nonav/javaee5/api/com/sun/appserv/security/AppservRealm.html
Теги:
Хабы:
+4
Комментарии11

Публикации

Истории

Работа

Java разработчик
355 вакансий

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн