"Нельзя делать свои велосипеды! Надо использовать проверенные и отлаженные библиотеки!"
Вроде бы верно. Но если посмотреть чуть глубже... Почему так? Адепты "проверенных библиотек" с горящими глазами скажут что-то вроде: "Свои велосипеды писать дольше, их надо отлаживать и их труднее сопровождать". Для "сферического проекта в вакууме" это верно. Но давайте рассмотрим задачу реальную. Я постараюсь поставить условия задачи, которые чаще всего встречались в моей, уже почти 10-летней практике. И покажу решение, которое писать быстрее, проще и легче сопровождать, хотя оно и является классическим "велосипедом" :)
Задача
Мы делаем web приложение. И нам нужно реализовать систему авторизации пользователей для доступа к отдельным страницам нашего приложения. Для определённости, пусть это будет панель администрирования сайта, реализованная на обычных jsp-страницах. Пользователям мы будем предоставлять доступ к отдельным разделам, при чём области доступа могут пересекаться. Например user1 имеет доступ только к старнице user1.jsp, user2 - только к user2.jsp, moderator - к страницам user1.jsp и user2.jsp а admin - ко всем вышеперечисленным а также к secure.jsp. И, напоследок, index.jsp - будет доступна вообще без авторизации.
Ограничения:
- никакого специфического кода не должно быть в наших jsp-страницах.
- никаких "тупиковых" страниц типа 403 access denied
- настройки держим отдельно от кода
И, самое главное: минимум посторонних библиотек. Если мы пишем админку на 10-50 классов (а именно такие они, админки, в большинстве своём), то подключать и конфигурировать 100500 классов "Spring Чего-То Там" будет долго, тяжело и нафиг не нужно.
Инструменты
javax.servlet.Filter. И всё. Нам нужно повесить предобработку запросов на определённые (или все) url? Именно для этого и предназначен этот интерфейс.
Правила доступа пользователей к страницам опишем в обычном txt-файле вот таким образом:
*~~/index.jsp,/
admin~adminSuperPassword~/*
user1~u1pass~/user1.jsp
user2~u2pass~/user2.jsp
moderator~moderpass~/user1.jsp,/user2.jsp
Тут всё просто: каждая строка описывает одного пользователя. Логин, пароль и набор url. Три части правила разделяем символом "~", url разделяем запятыми. *-означает любого пользователя, /*-любой url.
И, собственно, 100 строк реализации:
import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import java.io.*; import java.util.*; @WebFilter("/*") public class AuthFilter implements Filter { private static List<Rule> rules = new ArrayList<Rule>(); private static final String PARAM_TOKEN = "token"; private static final String PARAM_LOGIN = "login"; private static final String PARAM_PASS = "pass"; @Override public void init(FilterConfig filterConfig) throws ServletException { BufferedReader res = new BufferedReader(new InputStreamReader(filterConfig.getServletContext().getResourceAsStream("/WEB-INF/classes/users.txt"))); try { String tmp; while ((tmp=res.readLine())!=null) { String[] strings = tmp.split("~"); if (strings.length==3) rules.add(new Rule(strings[0], strings[1], strings[2].split(","))); } } catch (IOException ioe) { System.err.println("access rules not loaded!"); ioe.printStackTrace(); } } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { String uri = ((HttpServletRequest) req).getRequestURI(); boolean rez = false; for (Rule r : rules) { if (r.isApply(uri) && auth((HttpServletRequest)req, r)) { rez = true; break; } } if (rez) chain.doFilter(req, resp); else { PrintWriter out = resp.getWriter(); out.print("<html><body><form method=\"POST\" action=\"" + uri + "\">\n" + "<input type=\"text\" name=\""+PARAM_LOGIN+"\" placeholder=\"Login\"><br/>\n" + "<input type=\"password\" name=\""+PARAM_PASS+"\" placeholder=\"Password\"><br/>\n" + "<input type=\"submit\" value=\"Login\" />\n" + "</form></body></html>"); out.flush(); out.close(); } } @Override public void destroy() {} public boolean auth(HttpServletRequest req, Rule r) { if (Rule.USER_ANY.equals(r.user)) return true; String reqUser = getStoredUser(req); String reqLogin = req.getParameter(PARAM_LOGIN); String reqPass = req.getParameter(PARAM_PASS); if (reqUser!=null && reqUser.equals(r.user)) { return true; } if(reqLogin!=null && reqPass!=null && r.check(reqLogin, reqPass)) { storeUser(req, reqLogin); return true; } return false; } private String getStoredUser(HttpServletRequest req) { return (String) req.getSession().getAttribute(PARAM_TOKEN); } private void storeUser(HttpServletRequest req, String user) { req.getSession(true).setAttribute(PARAM_TOKEN, user); } private class Rule { public static final String USER_ANY = "*"; public static final String URL_ANY = "/*"; String user, password; String[] access; private Rule(String user, String password, String[] access) { this.user = user; this.password = password; this.access = access; } public boolean isApply(String uri) { for (String a : access) if (URL_ANY.equals(a) || a.equals(uri)) return true; return false; } public boolean check(String login, String pass) { return USER_ANY.equals(this.user) || (login.equals(this.user) && pass.equals(this.password)); } } }
Как это работает?
В реализации метода init() мы вычитываем файл users.txt c с нашими настройками доступа. Если планируется добавлять и удалять пользователей часто - перенесите эту логику в метод doFilter(). В методе doFilter() мы перебираем правила (экземпляры вложенного класса Rule) и если данное правило применимо к данному url выполняем метод auth() для этого правила. Если не одно из правил не сработало, выводим пользователю поля для авторизации. Форма авторизации будет отправлена сюда же и снова пройдёт через те же правила. Авторизация идёт в два захода: сначала ищем уже авторизованного пользователя в сессии и проверяем его полномочия, затем пытаемся его авторизовать и сохранить в сессию. Есть необходимость хранить сессии где-то в другом месте? Например, у вас много инстансов с общим Redis? Просто заменяем внутренности методов storeUser() и getStoredUser(). Страшно держать пароли в открытом виде? Sha1 вам в помощь. Класс всего один, маленький, простой, без зависимостей: разобраться сможет любой стажёр. И допилить под свои потребности, если нужно.
Как это подключить?
Просто положите фильтр в свой проект и заполните правила доступа. Никаких километровых xml-конфигов в стиле прошлого века. На дворе 2013 год. Все адекватные java web-сервера уже реализуют спецификацию Servlet 3.0 и нам пора заканчивать использовать языки разметки текстов вместо языков программирования :)
Как это подключить?
Просто положите фильтр в свой проект и заполните правила доступа. Никаких километровых xml-конфигов в стиле прошлого века. На дворе 2013 год. Все адекватные java web-сервера уже реализуют спецификацию Servlet 3.0 и нам пора заканчивать использовать языки разметки текстов вместо языков программирования :)
Этот комментарий был удален автором.
ОтветитьУдалитьЭтот комментарий был удален автором.
Удалить