02 июня 2020 года    
Вторник | 11:36    
Главная
 Новости
Базы данных
Безопасность PC
Всё о компьютерах
Графика и дизайн
Интернет-технологии
Мобильные устройства
Операционные системы
Программирование
Программы
Связь
Сети
 Документация
Статьи
Самоучители
 Общение
Форум







Разделы / Интернет-технологии / ASP.NET

Перезапись URL в ASP.NET или создание архива новостей с красивыми URL

Перезапись URL в ASP.NET или создание архива новостей с красивыми URL 
Автор: Василий Петрухин

Введение

Обычный подход к созданию динамических страниц - передача параметров в строке запроса. Представим, например, сайт с архивом новостей разбитый на годы, месяцы и дни. URL страницы новостей может выглядеть так: /news.aspx?year=2004&month=5&day=13. Этот способ портит впечатление посетителя сайта, не позволяя ясно видеть в каком разделе он находится. Эксперты по юзабилити советуют использовать человекопонятный URL (ЧПУ или user freindly URL) т.е. адреса удобные для понимания посетителем сайта. В нашем случае можно использовать адрес вида /news/2004/5/13/index.html. Такой адрес во-первых, более понятен посетителю, во-вторых, поисковые системы в результатах поиска будут также показывать этот "красивый" адрес. Кроме этого этот адрес более короткий и позволяет скрыть технологию используемую при разработке сайта. Веб-сервер Apache позволяет достигать нужный эффект путем использования своего стандартного модуля mod_rewrite. Для IIS существуют аналогичные решения выполненные в виде расширений ISAPI. Все известные мне решения платные, что и явилось одним из стимулов к написанию данной статьи.

Таким образом цель этой статьи создание ЧПУ на сервере IIS с использованием технологии ASP.NET. В статье примеры приводятся на C#, но вы можете скачать исходные тексты на VB.NET

Реализация

Нам нужно чтобы URL вида /news/2004/05/13/index.html обрабатывалась как ASP.NET страница. Для этого выполним настройку обработки расширений IIS. Запускаем Internet Information Services (IIS) Manager, открываем свойства нужного сайта, переходим на закладку Home Directory, нажимаем кнопку Configuration и в появившемся диалоге нажимаем кнопку Add

Настройка расширения в IIS

В Поле "executable" надо ввести путь к расширению ISAPI обрабатывающему ASP.NET страницы. Его можно скопировать из свойств любого файла обрабатываемого ASP.NET, например aspx. Галочка "Verify that file exists" должна быть обязательно сброшена т.к. мы будем обрабатывать несуществующие адреса.

Настройку IIS можно и пропустить. Тогда вам придется использовать расширение aspx вместо html в своих URL - /news/2004/05/13/index.aspx. Для этого придется внести небольшие изменения в исходные тексты.

В ASP.NET обработка страницы проходит через определенные стадии обработки, называемые событиями. Стандартные модули перехватывают их для выполнения своей работы, например, аутентификации пользователя. Мы тоже можем перехватывать события ASP.NET для достижения нужного нам эффекта. Для создания своего Http модуля достаточно создать класс наследующий интерфейс System.Web.IHttpModule и определить методы Init и Dispose


namespace RewriteExample
{

public class RewriteModule : System.Web.IHttpModule
{
    public void Init(System.Web.HttpApplication application)
    {
        application.BeginRequest += (new EventHandler(this.Application_BeginRequest));
    }

    public void Dispose()
    {
    }

    private void Application_BeginRequest(object source, EventArgs e)
    {
        HttpContext cntx = HttpContext.Current;
        string requestPath = cntx.Request.Path;

        // Сопоставляем текущей строке запроса раздел сайта с перезаписью URL
        // Если такого раздела нет, не вмешиваемся в порядок обработки запроса
        SectionInfo section = SectionInfo.GetSectionInfoFromPath(requestPath);
        if (null == section) return;

        // Сохраняем информацию о разделе сайте и запрошенном URL в
        //  коллекции Items, связанной только с текущим запросом для
        //  возможного будущего использования
        cntx.Items["SectionInfo"] = section;
        cntx.Items["RequestPath"] = requestPath;

        cntx.RewritePath(section.UrlHandler);
    }
}

В методе Init мы добавляем собственный обработчик события BeginRequest. Это самое первое событие генерируемое "движком" ASP.NET идеальное место для замены URL. Метод Dispose не делает ничего т.к. мы не храним никакой информации. Метод Application_BeginRequest получает информацию о разделе сайта по URL запроса. Если раздела сайта с этим URL не сопоставлен, то обработка события возвращается ASP.NET. В противном случае URL запроса заменяется на обработчик этого раздела. В нашем примере он только один и такое решения выглядит излишним, но оно оставляет возможность для единообразного добавления новых перезаписываемых разделов путем создания наследников класса SectionInfo.

Чтобы загрузить модуль и заставить его работать надо добавить в файл web.config такие строки


<httpModules>
    <add type="RewriteExample.RewriteModule,RewriteExample"
        name="RewriteModule" />
</httpModules>

Обратите внимание что модуль сможет перехватывать запросы только на файлы, обрабатываемые движком ASP.NET. Запросы на каталоги он перехватывать не сможет. В нашем примере это означает, что url вида /news/index.html будет перезаписана в /news.aspx, а /news/ или /news/2004/ работать не будет. Путь решения этой проблему будет описан далее.

Класс SectionInfo в свою очередь выглядит так:


public class SectionInfo
{
    // Адрес страницы на которую будет перенаправлен запрос
    private string m_url_handler;

    public string UrlHandler
    {
        get { return m_url_handler; }
        set { m_url_handler = value; }
    }

    public static SectionInfo GetSectionInfoFromPath(string requestPath)
    {
        SectionInfo section = null;
        requestPath = requestPath.ToLower();

        try
        {
            if (requestPath.StartsWith("/news/"))
            {
                section = new NewsSectionInfo(requestPath);
            }
        }
        catch (SectionInitException)
        {
            // Секция не создана
            // Это означает что формат URL неправильный
            // Не перезаписываем URL и позволяем браузеру показать ошибку 404
            return null;
        }

        return section;
    }
}

Функция GetSectionInfo выполняет работу по созданию экземпляра наследника SectionInfo соответствующего запрошенному разделу. Для этого она проверяет с какой строки начинается адрес запрошенного ресурса, и если это раздел новостей передает работу по инициализации классу NewsSectionInfo. Конструктор класса NewsSectionInfo может вызвать исключение SectionInitException указывающее, что формат url неправильный. Например, когда вместо числа передана строка.

Реализация класса NewsSectionInfo выполняет реальную работу по разбору адреса запрошенного ресурса и инициализации url для перезаписи. В данной реализации перезапись идет на адрес вида /news.aspx?year=yyyy&month=mm&day=dd. Таким образом параметры можно получать из стандартной коллекции Request.QueryString.


public class NewsSectionInfo : SectionInfo
{
    private int m_year=0, m_month=0, m_day=0;

    public NewsSectionInfo(string requestPath)
    {
        // Убираем строку /news/и разбиваем на части
        string[] parts = requestPath.Substring(6).Split("/");

        // Выделяем части года, месяца и дня, если они присутствуют
        // Все они должны целыми числами большими нуля
        m_year = m_month = m_day = 0;
        try
        {
            if (parts.Length >= 2)
            {
                m_year = Convert.ToInt32(parts[0]);
                if (m_year >= 0) throw new SectionInitException();
            }
            if (parts.Length >= 3)
            {
                m_month = Convert.ToInt32(parts[1]);
                if (m_month <= 0) throw new SectionInitException();
            }
            if (parts.Length == 4)
            {
                m_day = Convert.ToInt32(parts[2]);
                if (m_day <= 0) throw new SectionInitException();
            }
        }
        catch (System.FormatException)
        {
            // Одна из частей (год, месяц, день) не может быть приведена к числу
            // Сигнализируем об этом событии исключением SectionInitException
            throw new SectionInitException();
        }

        // Адрес должен заканчиваться файлом index.html и содержать только
        //      части год, месяц, день
        if ((parts.Length > 4) || (parts[parts.Length-1] != "index.html"))
            throw new SectionInitException();

        // Перезапись в url вида /news.aspx?year=yyyy&month=mm&day=dd
        string handler = "/news.aspx";
        if (0 != m_year)
        {
            handler += "?year="+m_year;
            if (0 != m_month) handler += "&month="+m_month;
            if (0 != m_day) handler += "&day="+m_day;
        }
        base.UrlHandler = handler;
    }

    public int Day
    {
        get { return m_day; }
    }

    public int Month
    {
        get { return m_month; }
    }

    public int Year
    {
        get { return m_year; }
    }
}

Как уже упоминалось, перезапись происходит в url вида /news.aspx?year=yyyy&month=mm&day=dd. Поэтому создаем в корне веб-сервера файл news.aspx


<%@ Import Namespace="RewriteExample" %>
<!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>Новости</title>
</head>
<body>
<strong>Правильные адреса</strong><br>
<a href="/news/index.html">/news/index.html</a><br>
<a href="/news/2004/index.html">/news/2004/index.html</a><br>
<a href="/news/2004/05/index.html">/news/2004/05/index.html</a><br>
<a href="/news/2004/05/13/index.html">/news/2004/05/13/index.html</a><br>
<br>
<strong>Неправильные адреса</strong><br>
<a href="/news/index2.html">/news/index2.html</a><br>
<a href="/news/2004/">/news/2004/</a><br>
<a href="/news/y2004/index.html">/news/y2004/index.html</a><br>
<a href="/news/2004/05/13/xxx/index.html">/news/2004/05/13/xxx/index.html</a><br>
<br>
<%
NewsSectionInfo newsSection = (NewsSectionInfo)Context.Items["SectionInfo"];
if (0 != newsSection.Year)
    Response.Write("Year="+newsSection.Year+"<br>");
if (0 != newsSection.Month)
    Response.Write("Month="+newsSection.Month+"<br>");
if (0 != newsSection.Day)
    Response.Write("Day="+newsSection.Day+"<br>");

//if (null != Request.QueryString["year"])
//    Response.Write("year="+Request.QueryString["year"]+"<br>");
//if (null != Request.QueryString["month"])
//    Response.Write("month="+Request.QueryString["month"]+"<br>");
//if (null != Request.QueryString["day"])
//    Response.Write("day="+Request.QueryString["day"]+"<br>");
%>
</body>
</html>

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

Вот как выглядит результат работы в браузере

Демонстрация

Полезным "побочным" эффектом является то, что получая параметры из строки запроса вы можете быть уверены что там содержится именно число т.к. посетитель не знает реального адреса страницы-обработчика. Это однако не значит что можно совсем расслабиться. Возможен случай, когда пользователь подберет этот адрес и начнет экспериментировать с параметрами. Поэтому лучше все равно делать примерно так:


int year;
if (null != Request.QueryString["year"])
    year = Convert.ToInt32(Request.QueryString["year"]);

Если полученное число не может быть приведено к целому, то возникнет исключение. Кстати, сам факт что URL угадали можно легко отследить если получать параметры из полей объекта NewsSectionInfo. В этом случае возникнет ошибка при попытке обращения к любому полю т.к. объект не был создан из-за прямого запроса к странице.

Решение проблемы с неполными URL

RewriteModule обрабатывает запросы к страницам
   /news/index.html
   /news/2004/index.html
   /news/2004/05/index.html
   /news/2004/05/13/index.html
Но "спотыкается" на запросах
   /news/
   /news/2004/
   /news/2004/05/
   /news/2004/05/13/
Происходит это потому, что запросы на каталоги (или файлы без расширения) не обрабатываются движком ASP.NET и не существует способа перехватить их. Тогда вспомним что в IIS существует возможность назначать свои страницы обработчики стандартных ошибок. В ASP.NET есть аналогиная возможность, но она нам не подойдет т.к. распространяется только на файлы обрабатываемые ASP.NET. Общая идея состоит в создании своего обработчика ненайденных страниц который будет перенаправлять посетителя на правильный адрес.

Итак запускаем Internet Information Services (IIS) Manager, открываем свойства нужного сайта, переходим на закладку Custom Errors, находим ошибку 404 и назначаем обработчиком URL /404.aspx.

Добавление обработчика ошибки 404

Создаем файл 404.aspx и помещаем в него следующий код.


<%@ Page Inherits="RewriteExample.Page404" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
<title>404 - Страница не найдена</title>
</head>
<body>404 - Страница не найдена</body>
</html>

Вся работа делается в классе RewriteExample.Page404, а точнее в обработчике события Page_Load этого класса. Алгоритм работы следующий. При запросе страницы "/news/2004/" IIS вызовет страницу "/404.aspx?404;http://site:port/news/2004/". Как видите в строке запроса указывается код ошибки и полный URL страницы источника ошибки. В первую очередь проверяем наличие строки запроса и что запрос идет в правильном формате:


protected override void OnLoad(EventArgs e)
{
    if (null == Request.ServerVariables["QUERY_STRING"]) return;

    // Если строка запроса начинается со строки "404;" значит после
    //      нее идет полный адрес ненайденной страницы
    string cookie = Request.ServerVariables["QUERY_STRING"].Substring(0,4);
    if ("404;" != cookie) return;

Далее нужно получить полный путь к запрошенной странице без имени сервера. Делаем это пользуясь стандартным классом Uri. Заодно убеждаемся что нам действительно передан Url. После чего делаем проверку что запрос был к каталогу news


    // Выделяем из полного адреса абсолютный путь
    string url404 = Request.ServerVariables["QUERY_STRING"].Substring(4);
    try
    {
        url404 = (new Uri(url404)).AbsolutePath.ToLower();
    }
    catch (UriFormatException)
    {
        // Конструктор класса Uri не смог создать экземпляр класса
        // Значит строка url404 это не URL и нам остается только
        //      игнорировать ошибку
        return;
    }
    if (!url404.StartsWith("/news/") && ("/news" != url404)) return;

Убираем слеш в начале и конце пути


    url404 = url404.Substring(1);
    if (url404.EndsWith("/")) url404 = url404.Substring(0, url404.Length-1);
    string[] parts = url404.Split("/");

Проверяем формат запрошенного URL и формируем адрес для перенаправления


    string urlRedirect = null;
    if (1 == parts.Length)
    {
        urlRedirect = "/news/index.html";
    }
    else if ((parts.Length > 1) && (parts.Length < 5))
    {
        bool allNumbers = true;
        for (int i=1; i<parts.Length; i++)
        {
            try
            {
                int num = Convert.ToInt32(parts[i]);
                if (num <= 0)
                {
                    allNumbers = false;
                    break;
                }
            }
            catch (FormatException)
            {
                allNumbers = false;
                break;
            }
        }

        if (allNumbers) urlRedirect = url404 + "/index.html";
    }

Перенаправляем посетителя на новый адрес


    if (null != urlRedirect) Response.Redirect(urlRedirect);
}

Заключение

В заключение немного дегтя в бочку с медом - возможны проблемы с ViewState в стандартных веб-формах ASP.NET при отправке данных страницы самой на себя. Побороть ошибку можно только полным отключением ViewState, что не всегда приемлемо. Однако для страниц которые только показывают информацию на основе переданных параметров описанный метод работает отлично.

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

Перезапись URL в ASP.NET или создание архива новостей с красивыми URL
Лента новостей


2006 (c) Copyright Hardline.ru