Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - Все статьи »

Обсудить на форуме Обсудить на форуме

Система аутентификации на базе протокола HTTP Basic. Создание своего .NET HttpModule.

Большинство Web разработчиков используют обычные HTML-формы для обеспечения информации о правах доступа к страницам приложения, потому что эти формы всем знакомы, могут предоставить дополнительную информацию, запросить дополнительную информацию, кроме имени пользовател и пароля.

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

Но у этого подхода есть и недостатки:

  1. Большие затраты со стороны разработчика.
  2. Прозрачность приложения.
    • например, часто, посмотрев Source страницы с login/password легко понять, куда происходит post и с какими параметрами
  3. Открытость для атак:
    • кросс-сайтных атак через сценарий:
      • http://www.cert.org/advisories/CA-2000-02.html
      • http://www.microsoft.com/technet/security/crssite.asp
    • атак через различные Http Clients

HTTP "Basic"

В данной статье я приведу другой вариант предоставления права на доступ для анонимного юзера. Система основана на базе протокола HTTP, "basic". Сразу оговорюсь, что в "basic", login и пароль передаются через заголовок HTTP, но как clear text. Этот недостаток сведён на нет в системе HTTP "digest", но "digest" это тема для другой статьи.

Всмотритесь в это окно:

В форумах иногда можно увидеть вопросы типа: "А как сделать чтобы для анонимного пользователя вылетала стандартна форма в браузере: логин, пароль, домен ?"

Как правило такие вопросы остаются либо без ответа, либо ответ такой: "Для этого существуют специальные ISAPI фильтры."

К слову самый известный коммерческий фильтр этого типа это Authentix. Теоретически можно написать и самому, но это во-первых задача не из лёгких, а во-вторых если хостинг чужой, то далеко не каждый хостер согласится поставить ISAPI фильтр на HTTP Server.

Алгоритм аутентификации на базе HTTP "basic":

Он такой:

Этап 1

  1. Проверяем существует ли заголовок HTTP "Authorization", если отсутствует переходим к этапу 2.
  2. Если существует, то мы получаем то, что юзер заполнил в модальном окне, (заголовок "Authorization") строкой следующего вида: "basic abrakadabrasdfsdfsdabrakadabra="
  3. Пропускаем слово "basic", и декодируем оставшуюся часть, закодированную base64. Результатом будет строка username:password
  4. Сравниваем с тем, что хранится у нас в базе данных. Если всё нормально, возвращаем страницу. Если нет, переходим к этапу 2.

В мини проекте, который прилагается к данной статье, я построил базу данных сходно с тем, как описывается в статье на ASPNETMANIA "Создание системы авторизации, основанной на ролях в ASP.NET приложении." (небольшие отличия) Чтобы пример был полнокровный - юзер с ролями, страница с ролями(см. статью).

Этап 2

Возвращаем код состояния HTTP 401 (Unauthorized), и задаём заголовок следующего вида:

WWW-Authenticate: BASIC realm="some-name"

Этот ответ заставляет браузер вывести наше модальное окно, чтобы юзер ввёл логин и пароль для some-name, и повторно установить соединение с логином и паролем, собранным в одну строчку, закодированную base64 (здесь кодирование происходит автоматически).

Алгоритм base64 широко известен, поэтому безопасность не является целью кодирования. Кодируется скорее всего для того, чтобы сделать заголовок непрозрачным.

Реализация аутентификации HTTP "basic" на основе ролей в .NET:

Я имел в своё время нелёгкий опыт реализации на чистом ASP - ещё один способ помимо ISAPI:

Декодирование из base64 выглядело примерно так:

'*** Декодировка из base64 - VBScript
Function Base64Decode(ByVal base64String)

   Const Base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
   Dim dataLength, sOut, groupBegin

   'remove white spaces, If any
   base64String = Replace(base64String, vbCrLf, "")
   base64String = Replace(base64String, vbTab, "")
   base64String = Replace(base64String, " ", "")

   'The source must consists from groups with Len of 4 chars
   dataLength = Len(base64String)
   If dataLength Mod 4 <> 0 Then
      Err.Raise 1, "Base64Decode", "Bad Base64 string."
      Exit Function
   End If


   ' Now decode each group:
   For groupBegin = 1 To dataLength Step 4
      Dim numDataBytes, CharCounter, thisChar, thisData, nGroup, pOut
      ' Each data group encodes up To 3 actual bytes.
      numDataBytes = 3
      nGroup = 0

      For CharCounter = 0 To 3
         ' Convert each character into 6 bits of data, And add it To
         ' an integer For temporary storage.  If a character is a '=', there
         ' is one fewer data byte.  (There can only be a maximum of 2 '=' In
         ' the whole string.)

         thisChar = Mid(base64String, groupBegin + CharCounter, 1)

         If thisChar = "=" Then
            numDataBytes = numDataBytes - 1
            thisData = 0
         Else
            thisData = InStr(Base64, thisChar) - 1
         End If

         If thisData = -1 Then
            Err.Raise 2, "Base64Decode", "Bad character In Base64 string."
            Exit Function
         End If

         nGroup = 64 * nGroup + thisData
      Next

      'Hex splits the long To 6 groups with 4 bits
      nGroup = Hex(nGroup)

      'Add leading zeros
      nGroup = String(6 - Len(nGroup), "0") & nGroup

      'Convert the 3 byte hex integer (6 chars) To 3 characters
      pOut = Chr(CByte("&H" & Mid(nGroup, 1, 2))) + _
         Chr(CByte("&H" & Mid(nGroup, 3, 2))) + _
         Chr(CByte("&H" & Mid(nGroup, 5, 2)))

      'add numDataBytes characters To out string
      sOut = sOut & Left(pOut, numDataBytes)
   Next

   Base64Decode = sOut
End Function

К счастью в .NET это делается в 2 строчки:

   byte[] tempConverted = Convert.FromBase64String(strEncoded);
   string userInfo = new ASCIIEncoding().GetString(tempConverted);

Ещё в добром старом ASP приходилось тяжело манипулировать реквестами и респонсами (смотрите алгоритм).

Чтобы легко выполнять HTTP "basic" алгоритм необходимо нечто, некий компонент, через который будут происходить все реквесты для данной аппликации.

В ASP.NET специально для таких задач предусмотрен инструмент, HttpModule.

наш HttpModule

Мы напишем собственный HttpModule, в котором реализуем "basic" алгоритм. Чтобы класс был зарегистрирован как HttpModule, достаточно реализовать в нём интерфейс System.Web.IHttpModule, имеющий 2 метода: OnAuthenticateRequest и OnEndRequest, определяющие этап 1 и этап 2 "basic" алгоритма соответственно.

Ну что ж, к делу:

/*
################################################################################
#    Components/AuthBasic.cs                                                   #
#                                                                              #
#                                                                              #
################################################################################
*/

using System;
using System.Collections;
using System.Configuration;
using System.Security.Principal;
using System.Text;
using System.Web;


namespace HTTPAuth.Components
{
   // реализация интерфейса IHttpModule
   // класс будет называться AuthBasic

   public class AuthBasic : IHttpModule
   {
      public AuthBasic() { }
      public void Dispose() { }


      public void Init(HttpApplication application)
      {
         application.AuthenticateRequest += new EventHandler(this.OnAuthenticateRequest);
         application.EndRequest += new EventHandler(this.OnEndRequest);
      }



      // первый метод OnAuthenticateRequest, реализующий этап 1 алгоритма

      /*
      ##########################################################################
      # OnAuthenticateRequest
      # <summary>
      #
      # </summary>
      ##########################################################################
      */
      public void OnAuthenticateRequest(object source, EventArgs eventArgs)
      {
         HttpApplication app = (HttpApplication) source;

         // дальше идём по алгоритму
         // достаём заголовок Authorization
         // проверяем, существует ли он

         string authorization = app.Request.Headers["Authorization"];
         if ((authorization == null) || (authorization.Length == 0))
         {
            AccessDenied(app);
            return;
         }

         // проверяем, удостоверяемся,
         // что заголовок вида basic

         authorization = authorization.Trim();
         if (authorization.IndexOf("Basic", 0) != 0)
         {
            AccessDenied(app);
            return;
         }

         // отсекаем слово basic и декодируем из base64
         // получаем "username:password"

         byte[] tempConverted = Convert.FromBase64String(authorization.Substring(6));
         string userInfo = new ASCIIEncoding().GetString(tempConverted);

         // получаем "username"
         // получаем "password"

         string[] usernamePassword = userInfo.Split(new char[] {':'});
         string username = usernamePassword[0];
         string password = usernamePassword[1];

         // сравниваем username, password против базы данных
         // если всё нормально, получаем список групп юзера из базы данных
         // и создаём экземпляр GenericPrincipal

         string[] groups;
         if (AuthenticateAgent(app, username, password, out groups))
         {
            app.Context.User = new GenericPrincipal(new GenericIdentity(username, "HTTPAuth.Components.AuthBasic"), groups);
         }

         // если нет, AccessDenied

         else
         {
            AccessDenied(app);
            return;
         }
      }



      // второй метод OnEndRequest, реализующий этап 2 алгоритма

      /*
      ##########################################################################
      # OnEndRequest
      # <summary>
      #
      # </summary>
      ##########################################################################
      */
      public void OnEndRequest(object source, EventArgs eventArgs)
      {
         HttpApplication app = (HttpApplication) source;
         if (app.Response.StatusCode == 401)
         {

            // Поднимаем модальное окно, realm хранится в web.config

            string realm = String.Format("Basic Realm=\"{0}\"", 
               ConfigurationSettings.AppSettings["HTTPAuth.Components.AuthBasic_Realm"]);
            app.Response.AppendHeader("WWW-Authenticate", realm);
         }
      }


      // Вход воспрещён - Unauthorized

      /*
      ##########################################################################
      # AccessDenied
      # 401 - Access Denied
      # <summary>
      # app      in ;  HttpApplication
      # </summary>
      ##########################################################################
      */
      private void AccessDenied(HttpApplication app)
      {
         app.Response.StatusCode = 401;
         app.Response.StatusDescription = "Access Denied";

         // Пишем в браузер

         app.Response.Write("401 Access Denied");
         app.CompleteRequest();
      }



      // следующий метод реализует проверку против базы данных
      // на основе ролей
      // если всё нормально, возвращает true и список групп пользовател
      // который нужен для создания экземпляра GenericPrincipal

      /*
      ##########################################################################
      # AuthenticateAgent
      #
      # <summary>
      # Authenticates Agent, returns true/false
      # app      in ;  HttpApplication
      # User     in ;  username
      # Password in ;  password
      # groups   out;  agent groups to create GenericPrincipal
      # </summary>
      ##########################################################################
      */
      protected virtual bool AuthenticateAgent(HttpApplication app, string username, string password, out string[] groups)
      {
         groups = null;
         int lagentID = 0;
         string lpageURL = "";

         // экземпляр класса, который осуществляет работу с базой
         // код прилагаетс

         SqlDataProvider dataProvider = new SqlDataProvider();


         // проверим есть ли вообще такой юзер

         // get agent if exists
         lagentID = dataProvider.getAgentByUsernamePassword(username, password);
         if (lagentID == 0)
            return false;


         // проверим есть ли вообще у него группы

         // get agent groups
         ArrayList arrAgentsGroups = new ArrayList();
         arrAgentsGroups = dataProvider.getGroupsByAgentID(lagentID);
         if (arrAgentsGroups.Count == 0)
            return false;


         // проверим есть ли группы у запрашиваемой страницы

         // get pages groups
         lpageURL = app.Request.Path;
         ArrayList arrPagesGroups = new ArrayList();
         arrPagesGroups = dataProvider.getGroupsByPageURL(lpageURL);
         if (arrPagesGroups.Count == 0)
            return false;


         // проверим если хотя бы одна группа юзера
         // находится в списке групп запрашиваемой страницы
         // если да - возвращаем true

         // check if at least one agent group is in Page Groups List
         string[] pagegroups = (String[]) arrPagesGroups.ToArray(typeof(String));
         groups = (string[]) arrAgentsGroups.ToArray(typeof(string));

         foreach (String groupagentID in groups)
         {
            foreach (String grouppageID in pagegroups)
            {
               if (groupagentID == grouppageID)
                  return true;
            }
         }
         return false;
      }
   }
}

Всё. HttpModule готов. Подключим его к веб приложению: В файле web.config под sytem.web

      <httpModules>
         <add name="BasicAuthenticationModule" type="HTTPAuth.Components.AuthBasic, HTTPAuth" />
      </httpModules>

формат такой:

      <httpModules>
         <add name="имя" type="полное имя класса, имя сборки" />
      </httpModules>

отменяем встроенную аутентификацию

      <authentication mode="None" />

      <authorization>
         <deny users="?" />
      </authorization>

а в appSettings мы сохраним realm string

   <appSettings>
      <add key="HTTPAuth.Components.AuthBasic_Realm" value="Protected System" />
   </appSettings>

Вот и всё.

К коду прилагаются database scripts и упрощённый класс для работы с базой.


Текст примеров данной статьи можно выкачать здесь


Может пригодится:


Автор: Anatoly Lubarsky
Прочитано: 3492
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (0)

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру