Някои тънкости при работата с Java Server Pages (JSP)

Светлин Наков

Стандартни обекти в JSP

Съгласно стандарта за Java Server Pages във всички JSP-страници автоматично се създават следните обекти:

request – за достъп до HTTP заявката и параметрите, които клиентът е изпратил към нея

response – за управление на отговора на HTTP заявката

out – изходен текстов поток за отговора на HTTP заявката

session – за управление на потребителските сесии

application – за достъп до данните, съхранявани  в контекста на Web-приложението

За удобство на програмиста тези обекти са достъпни от всички скриптлети в JSP-страницата. В нашия пример използвахме обекта out, чрез който отпечатахме текущата дата в изходния поток на JSP-страницата. В други случаи ще използваме и другите обекти. Целта е на автоматично създадените обекти е да се намали обемът на кода, който програмистът механично пише при работа със сървлети.

Технологията Java Server Pages предоставя на Web-разработчика освен скриптлети и други тагове. Ще разгледаме най-важните от тях.

JSP атрибути

В примера използването на класа java.util.Random става чрез пълното име на класа, предшествано от името на пакета, в който стои този клас. При нормалното програмиране на Java в програмата могат да се включват пакети чрез ключовата дума import, следвана от име на пакет. След това могат да се използват класовете от включените пакети като се изписват само имената им без пълните имена на пакетите, към които те принадлежат. В JSP също има начин за import-ване на пакети. Това става с атрибутът <%@ page import="име_на_пакет" %>, който се слага обикновено в началото на JSP-страницата. Например следният атрибут в JSP документ:

<%@ page import="java.util.*" %>

е еквивалентен на реда

import java.util.*;

написан в началото на сървлета преди декларацията на класа, който се получава при трансформацията на JSP страницата в сървлет.

Чрез подобен атрибут на JSP документа може да се зададе и content-type-а и encoding на върнатия HTTP отговор. Например ако искаме да върнем документ, който да се интерпретира от Web-браузъра на клиента като чист текст, а не като HTML, можем да напишем следното на един от началните редове на JSP документа:

<%@ page contentType="text/plain" %>

Ако искаме да укажем на клиентския Web-браузър, че върнатият от документ трябва да се изобрази на кирилица с българската кодова таблица, трябва да зададем атрибута:

<%@ page contentType="text/html;charset=windows-1251" %>

Забележете, че след символа “;” не трябва да има интервал.

Ако използваме език, който не използва латинската азбука, е полезен и атрибутът pageEncoding. С него можем да зададем кодирането, използвано в текущия JSP документ. Например, ако използваме кодиране „UTF-8”, трябва да зададем атрибута:

<%@ page pageEncoding="UTF-8" %>

С подобни атрибути могат да се задават и други настройки на JSP-страницата. Например атрибутът

<%@ page session="false" %>

указва, че JSP страницата няма да използва сесия, с което се ускорява достъпът до нея и същевременно сървърът се натоварва по-малко. Тази настройка трябва да се слага във всички JSP страници, които не използват потребителската сесия (обекта session).

Друг полезен атрибут на JSP страниците, който може да се задава по подобен начин, е страницата за обработка на грешки (error page). Ако в началото на една JSP страница се сложи ред, който съдържа

<%@ page errorPage="някое_релативно _URL" %>

всяко изключение (exception), възникнало по време на изпълнение на JSP-то, което не е обработено от това JSP, се предава на зададената страница за обработка на грешки. Задачата на тази страница за обработка на грешки е да покаже грешката във формат, разбираем за потребителя и евентуално да се погрижи да уведоми администратора за възникналия проблем. Целта е в случай на проблем потребителят да не получи 250 реда exception dump, а да му се покаже културно съобщение за грешка и обяснения как да продължи работата си. Всяка страница за обработка на грешки (error page) трябва да съдържа тага

<%@ page isErrorPage="true" %>

който указва, че това е страница за обработка на грешки. В такива страници е достъпен още един допълнителен обект exception, който съдържа последното възникнало необработено изключение.

Включване на фрагменти код към JSP страница

Друг полезен таг в JSP страниците е тагът

<%@ include file="relative_url" %>

Той позволява включването на съдържанието файл на текущата позиция в дадена JSP страница. Включването става по време на трансформирането на JSP страницата в сървлет. Този таг е особено подходящ когато Web-приложението съдържа много JSP страници, съдържащи общи фрагменти. Например ако трябва в началото на всяка страница от нашето Web-приложение да има меню, бихме могли да отделим кода, който създава това меню в отделен файл и да го включим във всеки JSP файл с тага <%@ include … %>. Тази възможност позволява повторното използване на вече написани фрагменти код (code reuse), което при големи проекти е много често използвана техника. Например в началото на JSP документа може да се включи следния ред:

<%@ include file="menu.jsp" %>

Той включва съдържанието на файла menu.jsp в текущата JSP страница по време на компилацията й. Включеният код може да не е статичен HTML и може да съдържа JSP тагове.

За включване на фрагмент код в текущата JSP страница има и още един подобен таг: <jsp:include page="relative_url" />, но той работи малко по-различно. При включване чрез <%@ include … %> включеният файл се прочита веднъж при първото изпълнение на JSP-то и след това дори да бъде променен, промените не се отразяват на JSP-то (static include). При включване на файл чрез <jsp:include … /> включеният файл се изпълнява при всяка заявка към JSP страницата и резултатът от него се вмъква в страницата (dynamic include). Така, ако включеният файл бъде променен, промяната се отразява и на всички JSP-та, които го включват.

Има и още една разлика между двата тага. Чрез <jsp:include … /> могат да се включват сървлети, CGI скриптове и други ресурси, достъпни чрез зададеното URL, а не само фрагменти от JSP документи. Ето и пример за включване на заглавен фрагмент в началото на JSP страница:

<jsp:include page="header.jsp" flush="true"/>

Атрибутът flush="true" е задължителен и трябва винаги да се включва при използване на <jsp:include … /> тага. Стойност false не е допустима.

Пренасочване към друга страница

Още един полезен таг в JSP стандарта е тагът за пренасочване към друга страница

<jsp:forward page="relative_URL"/>

При изпълнение на този таг, като резултат от заявката на клиента се връща резултатът от изпълнението на посоченото URL. Има голяма разлика между пренасочване чрез response.sendRedirect(…) (browser redirection) и <jsp:forward … /> (server redirection). Методът response.sendRedirect(…) просто казва на браузъра да зареди посоченото URL вместо това URL, което е поискал. Това става като сървърът върне отговор с код 302 на HTTP заявката (document temporary moved). Такова пренасочване е еквивалентно на това потребителят да напише посоченото URL в address bar-а на браузъра и да го зареди. Пренасочването с <jsp:forward … /> работи по съвсем друг начин. При него браузърът не разбира, че на сървъра се е извършило пренасочване, а просто получава резултата от изпълнението на URL-то, към което е направено пренасочване с <jsp:forward … />. В такъв случай в address bar-а на браузъра URL-то не се променя. Сървърът връща като отговор на клиентската заявка не страницата, която Web-браузърът е поискал, а страницата, която се връща при извличане на URL ресурса, към който е извършено пренасочването.

Забраняване на кеширането на Web-браузъра

Понеже Web-приложенията работят най-вече с динамично-генерирано съдържание, кешът на браузърите често пъти може да се окаже досаден проблем. Например ако имаме динамична страница за показване на информация, която се променя на всяка секунда, вероятно няма да искаме потребителят да вижда остарели данни заради кеша на браузъра. За да се забрани кеша на браузъра за текущата страница се задават няколко специални полета в хедъра на HTTP отговора. Ето един фрагмент от JSP страница, който указва на браузъра да не кешира документа, който получи:

<% 
    response.setHeader("Pragma", "No-cache"); 
    response.setDateHeader("Expires", 0); 
    response.setHeader("Cache-Control", "no-cache"); 
%>

Препоръчва се трите посочени реда да се използват заедно заради съвместимост с всички браузъри.

Проблеми със специалните символи в HTML

Да разгледаме следния фрагмент от JSP страница:

<% String name = request.getParameter("name"); %> 
Welcome, <%= name %>!

В кода има един сериозен проблем. Ако параметърът name има стойност <font color="red">, вместо да се отпечата поздрав с името на потребителя, ще се отпечата HTML таг, който задава червен цвят за остатъка от HTML документа. Ефектът може да бъде дори много по-страшен ако потребителят въведе за име на потребител следното:

<script language="JavaScript">while (1) alert("Bug!");</script>

Ако не се досещате какво ще се случи, пробвайте. При повечето Web-браузъри ефектът ще е неприятен: постоянно ще излиза съобщение „Bug!” и браузърът дори няма да може да бъде затворен.

При една сложна Web-базирана система е възможно потребителят да въвежда нещо и то да отива директно при някои оператор, който го обработва. Тогава неприятният ефект няма да се стовари върху потребителя, който го е предизвикал, а върху оператора. Ако нападателят е достатъчно хитър и достатъчно злонамерен, могат да се случат дори още по-лоши неща. Например на машината на оператора може да се появи съобщение, че сесията му е изтекла и HTML форма, в която да си въведе паролата, за да му бъде възобновена сесията. След това, естествено, въведената парола може свободно да бъде изпратена при нападателя. Такъв род проблеми със сигурността са известни като „cross-site scripting ” уязвимости и потенциално съществуват при всички езици и технологии за динамично генериране на HTML.

Справяне с проблема със специалните символи в HTML

Очевидно проблемът е доста сериозен и застрашава нормалната работа на системата. Да помислим как можем да го решим. Единият начин да се справим е като филтрираме някои непозволени символи, винаги, когато приемаме данни, идващи от потребителя. Това не винаги е възможно, потребителят може да иска да изпрати някакъв HTML документ като нормална част от работата си. Трябва ни друго решение.

Правилният начин за справяне с проблема със специалните символи в HTML, е чрез заместването им с еквивалентни последователности от символи, които не съдържат специални за HTML символи. Такова преобразование се нарича escaping (ескейпване). Има различни видове ескейпване.

Ако искаме да ескейпнем текст, който да поставим като параметър в даден URL адрес, трябва да използваме ескейпването „URL encode”, което замества символа интервал със символа „+” или с последователността от символи „%20”, символа въпросителен знак – с последователността „%3F” и т.н. За такова ескейпване в Java може да се използва класа метода encode на класа java.net.URLEncoder.

Ако искаме да поставим безопасно текст в HTML тага <textarea> трябва да избегнем единствено символите „<” и „&” като ги заменим с последователностите „&lt;” и „&amp;”.

Ако искаме да поставим безопасно стойност на текстово поле или стойност на атрибут на HTML таг, трябва да избегнем символите кавичкаапостроф и „&” като ги заместим съответно с „&#34;”, „&#39;” и „&amp;”.

Ако искаме да поставим безопасно текст в тялото на HTML документ, трябва да осигурим избягването на символите „<”, „>”, нов ред, интервал, табулация и „&” съответно с последователностите „&lt;”, „&gt;”, „<br>”, „&nbsp;”, „&nbsp;&nbsp;&nbsp;&nbsp;” и „&amp;”. Считаме, че табулацията се разглежда като 4 последователни интервала.

За последните три случая в Java няма стандартен клас или метод, който да извършва ескейпването. Колкото и странно да изглежда, колкото и този проблем да присъства във всички Java-базирани Web-приложения, нито в Servlet API спецификацията, нито в друг стандартен за J2EE или J2SE клас няма метод за ескейпване на HTML текст.

Единствената възможност, която ни остава, е да използваме собствен метод за HTML ескейпване. Трябва много да внимаваме да не пропуснем някой символ или да не ескейпнем някой символ грешно, защото това ще наруши сигурността на всички приложения, които използват нашия код. Ето една коректна реализация:

/** 
 * Escapes given text for placing it in the HTML body. If 
 * you need escaping for placing text in an attribut value,
 * you should remove the escaping for the "\n" character. 
 * 
 * (c) Svetlin Nakov, 2004 - http://www.nakov.com 
 */ 
public static String htmlEscape(String aText) { 
    if (aText == null) { 
        return ""; 
    } 
    StringBuffer escapedText = new StringBuffer(); 
    for (int i=0; i<aText.length(); i++) { 
        char ch = aText.charAt(i); 
        if (ch == '\'') 
            escapedText.append("&#39;"); 
        else if (ch == '\"') 
            escapedText.append("&#34;"); 
        else if (ch == '<') 
            escapedText.append("&lt;"); 
        else if (ch == '>') 
            escapedText.append("&gt;"); 
        else if (ch == '&') 
            escapedText.append("&amp;"); 
        else if (ch == '\n') 
            escapedText.append("<br>\n"); 
        else if (ch == ' ') 
            escapedText.append("&nbsp;"); 
        else if (ch == '\t') 
            escapedText.append("&nbsp;&nbsp;&nbsp;&nbsp;"); 
        else 
            escapedText.append(ch); 
    } 
    String result = escapedText.toString(); 
    return result; 
}

Този сорс код може да се използва за ескейпване на текст, който ще се поставя директно в тялото на HTML документ. При ескейпване на текст за поставяне като стойност на атрибут от HTML таг или в тялото на <textarea>, трябва да се премахне ескейпването на символа нов ред, защото ще причини проблеми. Всичко останало може да се запази.

Липсата на ескейпване или неправилното ескейпване може да причини проблеми със сигурността и стабилността на нашето Web-приложение. За да се предпазим от такива проблеми, винаги, когато генерираме динамичен HTML, трябва да спазваме следните правила:

-       Винаги трябва да ескейпваме преди директно отпечатване на текст в динамични HTML документи!

-       Винаги трябва да използваме правилния тип ескейпване – URL encode, HTML escaping или HTML escaping за стойност на атрибут или <textarea>.

-       Текстът трябва да се ескейпва непосредствено преди отпечатването му в документа! Не трябва да ескейпваме текста още при получаването му, защото рискуваме да се получи проблема „двойно ескейпване”.

Използвайки метода htmlEscape(…), можем да поправим проблемния JSP фрагмент от примера по-горе по следния начин:

<% String name = request.getParameter("name"); %> 
Welcome, <%= htmlEscape(name) %>!

Сега вече каквото и да изпрати потребителя като стойност на параметъра name, няма да наруши правилната работа на JSP страницата.

Кирилицата в сървлети и JSP страници

Основният проблем с кирилицата в сървлетите и JSP страниците идва от това, че при протокола HTTP заявката и отговорът обикновено са текстове, при които един символ се представя с един байт. Понеже в Java един символ се представя с Unicode (2 байта), възниква проблемът как да се преобразува малкото множество на еднобайтовите символи в голямото множество на Unicode символите. Очевидно съответствието не е еднозначно. За задаване на такова съответствие се използват т. нар. схеми за кодиране на символите (character encodings).

Повечето схеми за кодиране задават съответствия между част от еднобайтовите символи и част от Unicode символите. При преобразуването от Unicode към някоя кодираща схема или обратното всички символи, за които в схемата няма дефинирано съответствие, се заменят със символа „?” (байт със стойност 63).

Често пъти при използване на кирилица в сървлети и JSP страници вместо кирилица излизат въпросителни знаци. Това се дължи на неправилната кодираща схема, която се използва. За представяне на кирилица най-често се използва стандартната кодираща схема „windows-1251”. Ако не бъде указано да бъде използвано точно тя, често пъти възникват проблеми.

Проблемите с кирилицата при работа със сървлети и JSP страници са два – проблем с кодирането на HTTP заявката и проблем с кодирането на отговора на HTTP заявката.

Задаване на кодирането на HTTP заявката

В сървлети и JSP страници, които приемат параметри от HTTP заявката (например чрез метода getParameter(…) на HttpServletRequest класа) може да възникне проблем с кирилицата и в резултат на това в стойностите на параметрите да има въпросителни знаци на мястото на всички букви от кирилицата. Този проблем се решава чрез задаване на кодирането на HTTP заявката посредством израза:

<%
    request.setCharacterEncoding("cp1251");
%>

Задаването на схемата за декодиране на HTTP заявката трябва да е първото нещо, което прави един сървлет или JSP страница. След първото извикване на метода getParameter() на request обекта задаването на схема за декодиране няма никакъв ефект.

Въпреки всичко в някои ситуации може горният израз да не реши проблема с кирилицата. Методът setCharacterEncoding(…) по принцип се отнася до тялото на HTTP заявката и затова действа за всички параметри, изпратени по HTTP POST метод, но при заявки по метод HTTP GET е възможно да няма ефект. В такъв случай трябва да или да се премине към използване на POST заявки или да се променят стандартните настройките на Web-контейнера, за да се установи кодиране по подразбиране windows-1251.

Задаване на кодирането на HTTP отговори на заявки

Проблемът с неправилната схема на кодиране на HTTP отговора може да бъде решен по подобен начин – като бъде зададена подходяща схема за кодиране. В JSP страница за да укажем, че искаме отговорът на HTTP заявката да бъде разглеждан като текст на кирилица с кодиране windows-1251, трябва да зададем следния JSP атрибут:

<%@ page contentType="text/html;charset=windows-1251" %>

Ако искаме да укажем същото от сървлет, можем да го направим така:

response.setContentType("text/html;charset=windows-1251");

Задаването на content-type трябва да е първото нещо, което сървлетът прави, защото след като започне писането в потока, свързан с отговора на заявката, това вече е невъзможно.

Не винаги е възможно да зададем content-type на една JSP страница, защото ако една страница включва друга, се допуска само едната от двете да използва директивата <%@page contentType="…" %>. В такива случаи за страницата, която се включва в другата трябва да използваме директивата:

<%@ page pageEncoding="windows-1251" %>

С нея указваме на сървлет-контейнера какво е кодирането на страницата без да задаваме content-type.