SharePoint Alfresco PHP MySQL
О сайте Контакты
воскресенье, 28 августа 2011 г.

Прозрачная авторизация в Alfresco Share 4.0

Как уже говорилось ранее, Alfresco имеет множество встроенных механизмов авторизации. Однако, встроенной поддержки прозрачной аутентификации (SSO, single-sign-on) Alfresco не имеет. Поэтому задача стоит так: создать подсистему авторизации, позволяющей пользователям Alfresco входить без ввода пароля, используя доменную NTLM-авторизацию. Данная подсистема должна работать в Windows 7, Vista, XP, быть надежной и безопасной. Как эта задача была решена, читайте далее.

Предлагаемая схема прозрачной SSO авторизации в Alfresco приведена на рисунке:

В качестве рабочего механизма авторизации Alfresco выбираем External. Для этого в файле C:\Alfresco\tomcat\shared\classes\alfresco-global.properties устанавливаем следующие настройки:

authentication.chain=external1:external,alfrescoNtlm1:alfrescoNtlm 
external.authentication.enabled=true 
external.authentication.proxyHeader=X-Alfresco-Remote-User
external.authentication.proxyUserName=

Чтобы взаимодействие между приложением Share и приложением Alfresco проходило посредством cookies, в файле необходимо прописать:

<config evaluator="string-compare" condition="Remote">
  <remote>  
     <endpoint>
        <id>alfresco-noauth</id>
        <name>Alfresco - unauthenticated access</name>
        <description>Access to Alfresco Repository WebScripts that do 
        not require authentication</description>
        <connector-id>alfresco</connector-id>
        <endpoint-url>http://localhost:8080/alfresco/s</endpoint-url>
        <identity>none</identity>
     </endpoint>         
     <connector>
        <id>alfrescoCookie</id>
        <name>Alfresco Connector</name>
        <description>Connects to an Alfresco instance using cookie-
        based authentication</description>
        <class>org.springframework.extensions.webscripts.connector.AlfrescoConnector</class>
     </connector>         
     <endpoint>
        <id>alfresco</id>
        <name>Alfresco - user access</name>
        <description>Access to Alfresco Repository WebScripts that 
        require user authentication</description>
        <connector-id>alfrescoCookie</connector-id>
        <endpoint-url>http://localhost:8080/alfresco/wcs</endpoint-url>
        <identity>user</identity>
        <external-auth>true</external-auth>
     </endpoint>
  </remote>
</config>

Если этого не сделать, Share не сможет связаться с репозиторием Alfresco.

Теперь Alfresco будет доверять переданной ей серверной переменной RemoteUser. Чтобы сформировать эту переменную, создадим подсистему из модулей Apache. В качестве модуля, отвечающего за доменную прозрачную авторизацию пользователя, выберем sspi_auth_module.

Чтобы его подключить, пропишем в настройках Apache в файле httpd.conf:

LoadModule sspi_auth_module modules/mod_auth_sspi.so

Заодно, пропишем в httpd.conf настройки, которые пригодятся в дальнейшем:

LoadFile "C:/Perl/bin/perl512.dll"
LoadModule perl_module modules/mod_perl.so
Listen 10.0.20.245:80
ServerName vm-lysenko

<VirtualHost 10.0.20.245:80>
  ServerName vm-lysenko
  DocumentRoot "C:\alf"
  UseCanonicalName Off

  PerlModule Apache2::RequestRec

  RedirectMatch ^/$ /share/
  RedirectMatch ^/alfresco$ /alfresco/
  RedirectMatch ^/share$ /share/
  RedirectMatch ^/login$ /login/
  
  <Proxy ajp://127.0.0.1:8009/login/>
    Order deny,allow
    Deny from none
    Allow from all
  </Proxy>

  <Proxy ajp://127.0.0.1:8009/alfresco/>
    Order deny,allow
    Deny from none
    Allow from all
  </Proxy>

  <Proxy ajp://127.0.0.1:8009/share/>
    Order deny,allow
    Deny from none
    Allow from all
    PerlFixupHandler Apache2::SetRemoteUser
  </Proxy>
  
  ProxyPass /login/ ajp://127.0.0.1:8009/login/
  ProxyPassReverse /login/ ajp://127.0.0.1:8009/login/
  ProxyPass /alfresco/ ajp://127.0.0.1:8009/alfresco/
  ProxyPassReverse /alfresco/ ajp://127.0.0.1:8009/alfresco/
  ProxyPass /share/ ajp://127.0.0.1:8009/share/
  ProxyPassReverse /share/ ajp://127.0.0.1:8009/share/  

  <Location "/login/login.jsp">
    AuthName "Tomcat"
    AuthType SSPI
    SSPIAuth On
    SSPIPackage NTLM
    SSPIAuthoritative On
    SSPIOfferBasic Off
    SSPIOmitDomain On
    SSPIUsernameCase lower
    # SSPIPerRequestAuth Off
    # SSPIBasicPreferred
    # SSPIUsernameCase lower
    require valid-user
  </Location>  
</VirtualHost>

Из вышеуказанных настроек видно, что Apache играет роль прокси-сервера, и пропускает через себя все запросы от пользователя к Alfresco Share. Каждый раз, когда мы обращаемся к Share, мы применяем PerlFixupHandler Apache2::SetRemoteUser, чтобы установить переменную RemoteUser. Для того, чтобы устанавливать эту переменную, используется следующий perl-скрипт: (в папке c:\Perl\site\lib\Apache2\ файл SetRemoteUser.pm)

package Apache2::SetRemoteUser;
 
use strict;
use warnings;

use CGI::Cookie;
use Apache2::Const -compile => qw(OK);
use Apache2::Connection;
use Apache::DBI;
 
sub handler {
    my $r = shift;
            
    my $session_value = "";
    
    my $raw_cookie = $r->headers_in->{'Cookie'} || '';
    if ($raw_cookie ne '') {
      my %cookie = CGI::Cookie->parse($raw_cookie);
      if (exists $cookie{'JSESSIONID'}) {
        $session_value = $cookie{'JSESSIONID'}->value || "unknown-session";
      }
    }
    
    my $ip             = $r->connection->remote_ip() || "unknown-ip";
    my $browser        = $r->headers_in->{ 'User-Agent' } || "unknown-agent";
    my $authorization  = $r->headers_in->{ 'Authorization' } || "not-auth";    
    
    my $url      = $r->uri || "unknown-url";
    
    my $dbh = DBI->connect("DBI:mysql:sessions:localhost:3306", "session_user", "session_password", {
        PrintError => 1, # warn( ) on errors
        RaiseError => 0, # don't die on error
        AutoCommit => 1, # commit executes immediately
      }
    ) or die "Cannot connect to database: $DBI::errstr";   
    
    my $do_sql = "SELECT username FROM session WHERE unique_value = MD5('" . $session_value . $ip . $browser . "')";
    my $sth = $dbh->prepare($do_sql);
    $sth->execute( );
    
    my $result = $sth->fetchrow_hashref();
    my $u_name = $result->{'username'} || "";
    $dbh->disconnect( ); # NOOP under Apache::DBI  

    if ($u_name ne "") {
      $r->user($u_name);
    }
    return Apache2::Const::OK; 
} 
1;

Данный скрипт проверяет, есть ли зарегистрированная сессия с указанным именем пользователя, если такая сессия есть – то remoteUser устанавливается. В качестве места хранения сессий выбрана СУБД MySQL. Структура таблицы сессий:

CREATE TABLE `session` (
  `session_ID` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(30) DEFAULT NULL,
  `session_value` varchar(50) DEFAULT NULL,
  `unique_value` varchar(255) DEFAULT NULL,
  `session_date` datetime DEFAULT NULL,
  PRIMARY KEY (`session_ID`)
) ENGINE=InnoDB

Perl-скрипт только считывает данные из этой таблицы. Запись туда ведет jsp-приложение login, которое уже упоминалось в настройках httpd.conf. Данное приложение должно быть развернуто на том же сервере tomcat, что и приложения Share и Alfresco. Основной его частью является файл login.jsp, который и отвечает за «узнавание» пользователей и запись сессии в базу:

<%@ page session="true" %>
<%@ page import="java.io.*"  %>
<%@ page import="java.sql.*;" %>
<% 

String remoteUser = request.getRemoteUser();
String returnPath = request.getParameter("r");

Boolean login = false;
Integer error_status = 0;

if (remoteUser != null) {
    
  String driver = "org.gjt.mm.mysql.Driver";
  Class.forName(driver).newInstance();
  
  Connection        con = null;
  ResultSet         rst = null;
  Statement         stmt = null;
  PreparedStatement pstmt = null;
  
  String found_name="";
  String sessionValue="";
  String sql="";
  
  String ip      = request.getRemoteAddr();
  String browser = request.getHeader("User-Agent");  
  
  Cookie cookies[] = request.getCookies();
  
  if (cookies != null) {
    for (int i = 0; i < cookies.length; i++) {
      if (((String)cookies[i].getName()).equals("JSESSIONID")) {
        sessionValue = cookies[i].getValue();
      }
    }
  } 
 
  try {
    String session_url  = "jdbc:mysql://localhost:3306/sessions";
    String alfresco_url = "jdbc:mysql://localhost:3307/alfresco";
    
    con = DriverManager.getConnection(alfresco_url, "root", "root");
    sql = "SELECT authority FROM alf_authority WHERE authority = '" +  remoteUser + "'"; 
    stmt = con.createStatement();
    rst = stmt.executeQuery(sql);
    while(rst.next()){
      found_name = rst.getString(1);
    }
    con.close();
    stmt.close();
    
    if (sessionValue.equals("")) {
      error_status = 1;
    }
    
    if (!(found_name.equals(remoteUser) && !found_name.equals(""))) {
      error_status = 2;
    }

    if (error_status.equals(0)) {
      con=DriverManager.getConnection(session_url, "session_user", "session_password");
      sql = "DELETE FROM session WHERE (session_value = '" + sessionValue + "') OR (username = '" + remoteUser + "')";    
      pstmt = con.prepareStatement(sql);
      pstmt.executeUpdate();    

      sql = "INSERT INTO session SET session_value = '" + sessionValue + "', username = '" + remoteUser + "', session_date = NOW(), unique_value = MD5('" + sessionValue + ip + browser + "')";    
      pstmt = con.prepareStatement(sql);
      pstmt.executeUpdate();
      
      login = true; 
    } else {      
      login = false;
    }    
    
    rst.close();
    pstmt.close();
    con.close();  
  } catch(Exception e){
    System.out.println(e.getMessage());
  }
  
  if (login) {
    if (returnPath != null) {
      response.sendRedirect(returnPath);
    } else {
      response.sendRedirect("/share/page/user/" + remoteUser + "/dashboard");
    }
  } else {
    if (error_status.equals(1)) {
      response.sendRedirect("login.jsp");
    } else {
      response.sendRedirect("could-not-login.jsp");
    }
  }
}
%>

Login.jsp проверяет, есть ли определенный пользователь в базе Alfresco. Это необходимо для того, чтобы пускать только тех доменных пользователей, которые зарегистрированы в Alfresco. Также этот скрипт записывает в таблицу сессий уникальные данные пользователя, необходимые для его безошибочной идентификации: браузер, IP и т.д.

Каждый раз, когда приложение Share не узнало пользователя, оно выдает страницу с вводом логина и пароля. В данную страницу необходимо внедрить обработчик, который вместо выдачи формы ввода логина и пароля будет перенаправлять на наш скрипт доменной авторизации. Для этого в файл slingshot-login.ftl нужно добавить строку:

<script>
 window.location.href = "/login/login.jsp?r=" + window.location.href;
</script>

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

Теперь, когда установлены все дополнительные подсистемы, необходимо правильно настроить Tomcat. В файле server.xml должны быть прописаны два коннектора со следующими параметрами:

<Connector port="8080" URIEncoding="UTF-8" protocol="HTTP/1.1" 
           connectionTimeout="20000" 
           redirectPort="8443" tomcatAuthentication="false"
           emptySessionPath="true" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" 
           tomcatAuthentication="false"
           emptySessionPath="true" />

Коннектор с портом 8009 необходим для проксирования Апачем. В целях безопасности необходимо закрыть порты 8080 и 8009 от доступа извне, к ним должен иметь доступ только локальный сервер. Все внешние запросы принимает на себя Apache по порту 80. Если не закрыть упомянутые порты, то злоумышленник сможет обмануть систему и войти под любым нужным ему пользователем.

Параметр tomcatAuthentication="false" нужен для того, чтобы выключить встроенные в Tomcat механизмы авторизации, так как они нам не нужны и будут мешать. Параметр emptySessionPath="true" нужен для того, чтобы сессия, сформированная в одном приложении, действовала и в другом. Если этот параметр не установить, то сессия приложения share не будет действовать в приложении alfresco, не говоря уже о нашем приложении login. Для описываемой процедуры авторизации требуется, чтобы приложение login могло свободно передавать сессию в приложение share.

Sergey Lysenko, воскресенье, 28 августа 2011 г.

Комментарии: