OpenID. Обзор библиотеки joid

February 5, 2008

Сегодня я устрою тест еще одному серверу и consumer-у openid. Этот кандидат прибыл с экзотического острова Явы, так что ставить его в один ряд с описанными в прошлой статье библиотеками нельзя. Требования к хостингу у него будут повыше, да и процедура настройки и установки требует знания основных идей из мира java. Библиотека называется joid (полагаю, сокращение от java openid). Технически кандидат обещает, что сможет понять и версию 1.0 и 2.0 протокола openid. В рекламных проспектах говорится о простоте использования (читай, создания собственного сервера или consumer-а использующего функции joid). Домашняя страничка библиотеки http://code.google.com/p/joid/. На ней вы можете почитать куцую документацию, на странице downloads вы можете скачать саму библиотеку в виде архива joid-1.0.2.jar. Толковых описаний в сети "step by step" по использованию библиотеки я не нашел: пара страничек тупо копировали приведенный на странице проекта пример кода и расхваливали ее простоту и малый размер. Так что придется исправлять этот недостаток.

Скачанный архив библиотеки joid-1.0.2.jar я исследовал вдоль и поперек, и получил стойкое ощущение, что чего-то не хватает: в README шли ссылки на отсутствующие в архиве файлы и каталоги. Попытка решить проблему в лоб, создав веб-приложение (для теста я использовал apache-tomcat-6.0.14) поместив в папку lib библиотеки по следующей инструкции:
  1. Copy joid.jar, log4j-*.jar, and tsik.jar to your lib directory (so they end up in WEB-INF/lib).
  2. Add OpenIdFilter to your web.xml (see below for how to add it)
  3. бла-бла-бла

Так вот эта попытка закончилась неудачей: java.lang.ClassNotFoundException: org.verisign.joid.consumer.OpenIdFilter. Так что я выкачал весь проект из SVN. В его состав вошли следующие папки:
  • examples
  • lib
  • resources
  • src
  • stores
  • test

В папке lib находились нужные для работы приложения библиотеки: ant-junit.jar, commons-codec-1.3.jar, commons-httpclient-3.1-rc1.jar, commons-lang-2.3.jar, commons-logging-1.1.jar, javax.servlet.jar, junit-3.8.1.jar, log4j-1.2.13.jar, tsik.jar.

Из них только последняя библиотека является редкой (я так и не смог выяснить в составе какой другой системы или продукта она идет). Так что выкачивать целиком проект из репозитария не обязательно. Затем я написал сценарий ant выполняющий компиляцию проекта и упаковку его в виде war-архива.
  1. <?xml version="1.0" encoding="windows-1251"?>
  2.  
  3. <project name="joidapp" default="all" basedir=".">
  4.   <target name="init">
  5.  
  6.    <property name="name" value="joidserver" />
  7.    <property name="version" value="1.0" />
  8.  
  9.    <property name="out" value="out" />
  10.    <property name="src" value="src" />
  11.    <property name="lib" value="lib" />
  12.    <property name="configs" value="configs" />
  13.    <property name="examples" value="examples/server" />
  14.  
  15.    <property name="tomcat" location="E:\Program_Files_2\apache-tomcat-6.0.14" />
  16.  
  17.    <path id="clp">
  18.     <fileset dir="${lib}">
  19.        <include name="*.jar" />
  20.     </fileset>
  21.    </path>
  22.  
  23.   </target>
  24.  
  25.   <target name="clean" depends="init">
  26.     <delete dir="${out}" quiet="true" />
  27.  
  28.     <mkdir dir="${out}" />
  29.     <mkdir dir="${out}/WEB-INF" /> 
  30.     <mkdir dir="${out}/WEB-INF/lib" /> 
  31.     <mkdir dir="${out}/WEB-INF/classes" /> 
  32.  
  33.     <copy todir="${out}/WEB-INF/lib">
  34.       <fileset dir="${lib}">
  35.         <include name="**/*.jar" />
  36.         <exclude name="**/javax.servlet.jar" />
  37.       </fileset>
  38.     </copy>
  39.     <copy todir="${out}">
  40.       <fileset dir="${examples}">
  41.         <include name="**/*.*" />
  42.       </fileset>
  43.     </copy>
  44.  
  45.     <copy file="${configs}/web.xml" todir="${out}/WEB-INF" />
  46.     <copy file="${src}/log4j.properties" todir="${out}/WEB-INF/lib" />
  47.  
  48.   </target>
  49.  
  50.   <target name="compile" depends="clean">
  51.     <javac srcdir="${src}" destdir="${out}/WEB-INF/classes" fork="true" classpathref="clp">
  52.  
  53.     </javac>
  54.   </target>
  55.  
  56.  <target name="makewar" depends="compile">
  57.     <jar jarfile="${out}/${name}-${version}.war">
  58.       <fileset dir="${out}" />
  59.     </jar>
  60.     <copy todir="${tomcat}/webapps" file="${out}/${name}-${version}.war" />
  61.  </target>
  62.  
  63.  <target name="all" depends="makewar" />
  64. </project>
Ничего сложного в файле ant нет, обратите внимание только на пути к каталогам, в том числе каталог для веб-приложений tomcat.

Файл web.xml я разместил в новой папке configs и выглядит он так:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <web-app xmlns="http://java.sun.com/xml/ns/javaee"
  3.          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4.          xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
  5. 		  http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
  6.          version="2.5">
  7. <filter> 
  8.  <filter-name>OpenIdFilter</filter-name> 
  9.  <filter-class>org.verisign.joid.consumer.OpenIdFilter</filter-class> 
  10.  <init-param> 
  11.    <description>Optional. Will store the identity url in a cookie under "openid.identity" if set to true.</description> 
  12.    <param-name>saveInCookie</param-name> 
  13.    <param-value>true</param-value> 
  14.  </init-param> 
  15. </filter> 
  16. <filter-mapping> 
  17.  <filter-name>OpenIdFilter</filter-name> 
  18.  <url-pattern>/*</url-pattern> 
  19. </filter-mapping>
  20. </web-app>
Как видите, в этом файле просто подключается сервлет-фильтр. В качестве параметра ему, кроме показанного в примере параметра saveInCookie (сохранять ли подтвержденное значение openid-идентификатора в cookie или нет), можно указать список путей, которые не будут обслуживаться этим фильтром.

Устройство сервлета очень простое: в метод doFilter выполняется проверка двух вещей: того, что обслуживаемый адрес не попал в список "блокированных ресурсов" (настраивается в web.xml) и то, что в списке входных переменных есть одна с именем openid.identity - это признак обратного вызова от сервера аутентификации. Если оба эти условия выполняется, то процедура аутентификации завершается и помещаются сведения о ней внутрь cookie и внутрь сессии:
  1. public static final String OPENID_ATTRIBUTE = "openid.identity"; 
  2. // бла-бла-бла
  3. req.getSession(true).setAttribute(OpenIdFilter.OPENID_ATTRIBUTE, identity);
Ну а в противном случае ничего не происходит.

Все приложение заработало. Пока только в режиме consumer-а. Теперь надо провести тесты на "профпригодность". Для этого я использовал тех самых openid-провайдеров, о которых писал в прошлой статье.

1. Тест с mediawiki пройден успешно.

2. Тест с wordpress провален: переход на страницу провайдера выполнен не был, вместо этого получено сообщение об ошибке " An error occurred! Please press back and try again. ". В лог tomcat-а попало сообщение, что возникла ошибка NullPointerException, но это произошло где-то внутри файла JoidConsumer.java, но вникать в суть дела и пользоваться отладичком желания у меня не было. Спасибо, следующий.

3. Тест с phpMyID (доступен по адресу http://siege.org/projects/phpMyID/). Результаты изрядно удивили. После ввода в форму адреса openid-страницы передо мной появилось окно ввода имени и пароля. Но после того как я ввел там эти данные (правильные данные), обратный переход на страницу joid выполнен не был, а вместо этого меня снова попросили ввести имя и пароль. В ходе этого на заднем фоне firefox мигало какое-то всплывающее окошко, но оно исчезало так быстро, что рассмотреть толком не успевал. После того как мне надоело вводить имя и пароль. Я отказался и : при повторной попытке (с самого начала запустить процесс openid-аунтентификации) я успешно получил сообщение, что эта процедура была пройдена успешно (сам переход на страницу провайдера уже не выполнялся). Напомнило анекдот про двух алкашей и огурец в трехлитровой банке.

4. Тест с "my openIDOO" (загружен из SVN репозитария "svn checkout http://openidoo.googlecode.com/svn/trunk/ openidoo"). Тест пройден не был: получено очень понятное сообщение об ошибке: "An error occurred! Please press back and try again".

5. Тест с "PHP OpenID Server by JanRain, Inc". Тест также не был пройден. На этот раз в отличие от предыдущих тестов поведение было другим. После ввода openid-идентификатора я попал на страницу для ввода имени и пароля, ввел их, но обратный переход на страницу joid выполнен не был.

Выводы: все ужасно, только mediawiki не завалила тест.

Теперь попробую рассмотреть код клиентской части joid (в глубине души теплится надежда, что возможно я "просто не умею готовить сервера openid").
  1. String returnTo = UrlUtils.getBaseUrl(request);
  2. if (request.getParameter("signin") != null) {
  3. 	String id = request.getParameter("openid_url");
  4. 	if (!id.startsWith("http:")) {
  5. 		id = "http://" + id;
  6. 	}
  7. 	String trustRoot = returnTo;
  8. 	String s = OpenIdFilter.joid().getAuthUrl(id, returnTo, trustRoot);
  9. 	response.sendRedirect(s);
  10. }
Это фрагмент кода из файла index.jsp. В нем размещена html-форма для ввода openid-идентификатора в текстовое поле с именем openid_url. Как видите, первым шагом в примере выполняется проверка того, был ли выполнен вызов данного файла из формы или нет. Во втором случае условие не выполняется, а далее идет html-код страницы, формирующей ту самую форму для ввода openid-идентификатора. В противном случае, полученное из формы значение openid приводится в стандартную форму с добавлением (если это необходимо) префикса протокола, а затем оно поступает на вход к сервлету-фильтру OpenIdFilter. Он был подключен в файле web.xml, который я привел выше. В качестве входных параметров передается только адрес openid-идентификатора, а также адрес страницы, на которую необходимо выполнить возврат. Этот адрес они получают с помощью вызова UrlUtils.getBaseUrl(request), который возвращает строку с адресом текущего исполняющегося jsp-файла.

Следующий шаг - проверка, вдруг процедура аутентификации уже была пройдена и тогда извлекается имя пользователя и печатается на странице:
  1. String loggedInAs = OpenIdFilter.getCurrentUser(session);
В случае если пользователь еще не было залогинен, то значение переменной loggedInAs будет равно null.

В целом впечатление от клиентской части библиотеки очень положительное: код действительно компактен и прост. Хотя от необходимости "поработать напильником" я не свободен. В библиотеке реализована возможность выполнять запросы с помощью SRE (Simple Registration Extension) с целью получения таких сведений как nickname, fio, emai, : Так вот, используя только методы сервлета OpenIdFilter сделать это не возможно, нужно вносить правки в код класса JoidConsumer (именно это класс выполняет черновую работу) и изменить методы создания запроса на аутентификацию. С другой стороны библиотека находится в состоянии развития так что ...

Про сервер



Пример сервера joid построен с помощью трех файлов: login.jsp, me.jsp, logout.jsp. Их назначение, соответственно, логин совмещенный с созданием пользователей, затем, персональная страница аутентифицированного пользователя и страница выхода. Скажу сразу использовать этот пример как openid-сервер не возможно: продукт крайне сырой, нет достаточной документации, идущий в примере код сервлета-сервера openid (работает только с хранящимися в памяти данными), попытки привязать источник данных взятый из базы данных натолкнулся на отстуствие документации по использованию класса dbStore, а из названий и параметров методов нельзя было определить как указать ту БД, таблицу (и как она должна выглядеть). Вывод: фтоппку.