Статьи

Безопасность веб-приложений на Java. Часть IV. Программные API входа

За последний месяц я опубликовал ряд статей о реализации аутентификации с помощью Java EE 6 , Spring Security и Apache Shiro . Одной из вещей, которые я продемонстрировал в своих живых демонстрациях (на собраниях JUG в Юте), была программная аутентификация. Я исключил это из своих скринкастов и предыдущих уроков, потому что я думал, что это будет лучше соответствовать статье сравнения.

В этой статье я хотел бы показать вам, как вы можете программно войти в приложение, используя вышеупомянутые платформы безопасности. Для этого я буду использовать свое приложение ajax-login, которое я написал для реализации Ajax-аутентификации с использованием jQuery, Spring Security и HTTPS .

Для начала я реализовал LoginController как Spring MVC Controller, который возвращает JSON.

package org.appfuse.examples.webapp.security;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.ResponseBody;@Controller@RequestMapping("/api/login.json")public class LoginController {    @Autowired    LoginService loginService;    @RequestMapping(method = RequestMethod.GET)    @ResponseBody    public LoginStatus getStatus() {        return loginService.getStatus();    }    @RequestMapping(method = RequestMethod.POST)    @ResponseBody    public LoginStatus login(@RequestParam("j_username") String username,                             @RequestParam("j_password") String password) {        return loginService.login(username, password);    }}

Этот контроллер делегирует свою логику интерфейсу LoginService.

package org.appfuse.examples.webapp.security;public interface LoginService {  LoginStatus getStatus();  LoginStatus login(String username, String password);}

Клиент
Клиент для этого контроллера такой же, как упомянуто в моей предыдущей статье , но я опубликую его снова для вашего удобства. Я использовал jQuery и jQuery UI для реализации диалога, который открывает страницу входа на той же странице, а не перенаправляет на страницу входа. Локатор «#demo» ссылается на кнопку на странице. 

var dialog = $('<div></div>');$(document).ready(function() {    $.get('/login?ajax=true', function(data) {        dialog.html(data);        dialog.dialog({            autoOpen: false,       title: 'Authentication Required'        });    });    $('#demo').click(function() {      dialog.dialog('open');      // prevent the default action, e.g., following a link      return false;    });});

Страница входа затем имеет следующий JavaScript-код для добавления обработчика кликов к кнопке «вход», которая безопасно отправляет запрос в LoginController.

var getHost = function() {    var port = (window.location.port == "8080") ? ":8443" : "";    return ((secure) ? 'https://' : 'http://') + window.location.hostname + port;};var loginFailed = function(data, status) {    $(".error").remove();    $('#username-label').before('<div class="error">Login failed, please try again.</div>');};$("#login").live('click', function(e) {    e.preventDefault();    $.ajax({url: getHost() + "${ctx}/api/login.json",        type: "POST",        beforeSend: function(xhr) {            xhr.withCredentials = true;        },        data: $("#loginForm").serialize(),        success: function(data, status) {            if (data.loggedIn) {                // success                dialog.dialog('close');                location.href = getHost() + '${ctx}/users';            } else {                loginFailed(data);            }        },        error: loginFailed    });});

Самый большой секрет, чтобы заставить все это работать (HTTP -> HTTPS-связь, которая считается междоменной), — это window.name Transport и плагин jQuery, который его реализует. Чтобы этот плагин работал с Firefox 3.6, мне пришлось реализовать фильтр, который добавляет заголовки Access-Control.

public class OptionsHeadersFilter implements Filter {    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)            throws IOException, ServletException {        HttpServletResponse response = (HttpServletResponse) res;        response.setHeader("Access-Control-Allow-Origin", "http://" + req.getServerName());        response.setHeader("Access-Control-Allow-Methods", "GET,POST");        response.setHeader("Access-Control-Max-Age", "360");        response.setHeader("Access-Control-Allow-Headers", "x-requested-with");        response.setHeader("Access-Control-Allow-Credentials", "true");        chain.doFilter(req, res);    }    public void init(FilterConfig filterConfig) {    }    public void destroy() {    }}

Java EE 6 LoginService
Java EE 6 имеет несколько новых методов в HttpServletRequest :

  • проверку подлинности (ответ)
  • логин (пользователь, пароль)
  • выйти()

В этом примере я буду использовать новый метод входа в систему (имя пользователя, пароль) . Самым сложным в этой работе было найти правильную зависимость от Maven. Сначала я попробовал тот, который, казалось, имел больше смысла:

<dependency>    <groupId>javax</groupId>    <artifactId>javaee-web-api</artifactId>    <version>6.0</version></dependency>

К сожалению, это привело к странной ошибке, которая означает, что зависимость имеет интерфейсы, но не классы реализации. Вместо этого я использовал зависимость GlassFish (спасибо за переполнение стека за подсказку).

<dependency>    <groupId>org.glassfish</groupId>    <artifactId>javax.servlet</artifactId>    <version>3.0</version>    <scope>provided</scope></dependency>

Поскольку Servlet 3.0, по-видимому, отсутствует в Maven Central, мне пришлось добавить репозиторий GlassFish в элемент <repositories> моего pom.xml.

<repository>    <id>glassfish-repo</id>    <url>http://download.java.net/maven/glassfish</url></repository>

После этого было легко реализовать интерфейс LoginService с помощью класса JavaEELoginService:

package org.appfuse.examples.webapp.security;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;@Service("javaeeLoginService")public class JavaEELoginService implements LoginService {    private Log log = LogFactory.getLog(JavaEELoginService.class);    @Autowired    HttpServletRequest request;    public LoginStatus getStatus() {        if (request.getRemoteUser() != null) {            return new LoginStatus(true, request.getRemoteUser());        } else {            return new LoginStatus(false, null);        }    }    @Override    public LoginStatus login(String username, String password) {        try {            if (request.getRemoteUser() == null) {                request.login(username, password);                log.debug("Login succeeded!");            }            return new LoginStatus(true, request.getRemoteUser());        } catch (ServletException e) {            e.printStackTrace();            return new LoginStatus(false, null);        }    }}

Я пытался использовать это с «mvn jetty: run» (с версией 8.0.0.M2 из jetty-maven-plugin), но я получил следующую ошибку:

javax.servlet.ServletException        at org.eclipse.jetty.server.Request.login(Request.java:1927)        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)        at java.lang.reflect.Method.invoke(Method.java:597)        at org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler.invoke(AutowireUtils.java:178)        at $Proxy52.login(Unknown Source)        at org.appfuse.examples.webapp.security.JavaEELoginService.login(JavaEELoginService.java:30)

Это привело меня к мысли, что Servlet 3 не совсем реализован, поэтому я попробовал его с Tomcat 7.0.8. Для поддержки SSL и аутентификации, управляемой контейнером, мне пришлось создать хранилище ключей сертификатов и раскомментировать коннектор SSL в $ CATALINA_HOME / conf / server.xml . Мне также пришлось добавить пользователя «admin» с role = «ROLE_ADMIN» в $ CATALINA_HOME / conf / tomcat-users.xml.

<user username="admin" password="admin" roles="ROLE_ADMIN"/>

С Tomcat 7 я смог успешно войти в систему, что подтверждается следующей регистрацией.

DEBUG - JavaEELoginService.login(31) | Login succeeded!

Тем не менее, в пользовательском интерфейсе я все еще получил «Ошибка входа в систему, попробуйте еще раз». сообщение. Вспоминая, что у меня были некоторые проблемы с предыдущими портами, я настроил Apache для прокси по умолчанию для портов http / https по умолчанию 8080/8443 и повторил попытку. На этот раз это сработало!

Spring Security LoginService
Spring Security предлагает программный API, и я смог реализовать его LoginService следующим образом:

package org.appfuse.examples.webapp.security;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import org.appfuse.model.User;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.authentication.BadCredentialsException;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.Authentication;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.stereotype.Service;@Service("springLoginService")public class SpringSecurityLoginService implements LoginService {    private Log log = LogFactory.getLog(SpringSecurityLoginService.class);    @Autowired(required = false)    @Qualifier("authenticationManager")    AuthenticationManager authenticationManager;    public LoginStatus getStatus() {        Authentication auth = SecurityContextHolder.getContext().getAuthentication();        if (auth != null && !auth.getName().equals("anonymousUser") && auth.isAuthenticated()) {            return new LoginStatus(true, auth.getName());        } else {            return new LoginStatus(false, null);        }    }    public LoginStatus login(String username, String password) {        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);        User details = new User(username);        token.setDetails(details);        try {            Authentication auth = authenticationManager.authenticate(token);            log.debug("Login succeeded!");            SecurityContextHolder.getContext().setAuthentication(auth);            return new LoginStatus(auth.isAuthenticated(), auth.getName());        } catch (BadCredentialsException e) {            return new LoginStatus(false, null);        }    }}

Затем я изменил зависимость LoginService в LoginController, чтобы использовать эту реализацию.

@Autowired@Qualifier("springLoginService")LoginService loginService;

Поскольку Spring API не зависит от Servlet 3, я попробовал его в Jetty, используя «mvn jetty: run». Конечно, перед этим я изменил свой web.xml для Spring Security. Интересно, что я обнаружил, что мой SpringSecurityLoginService, похоже, работает:

DEBUG - SpringSecurityLoginService.login(39) | Login succeeded!

Но в пользовательском интерфейсе произошла ошибка при входе в систему: «Ошибка входа в систему, повторите попытку». сообщение. Использование стандартных портов с Apache перед Jetty решило эту проблему.

Apache Shiro LoginService
Apache Shiro достаточно хорош, чтобы также предлагать программный API. Я смог реализовать ShiroLoginService следующим образом:

package org.appfuse.examples.webapp.security;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.AuthenticationException;import org.apache.shiro.authc.UsernamePasswordToken;import org.apache.shiro.subject.Subject;import org.springframework.stereotype.Service;@Service("shiroLoginService")public class ShiroLoginService implements LoginService {    private Log log = LogFactory.getLog(ShiroLoginService.class);    public LoginStatus getStatus() {        Subject currentUser = SecurityUtils.getSubject();        if (currentUser.isAuthenticated()) {            return new LoginStatus(true, currentUser.getPrincipal().toString());        } else {            return new LoginStatus(false, null);        }    }    public LoginStatus login(String username, String password) {        if (!getStatus().isLoggedIn()) {            UsernamePasswordToken token = new UsernamePasswordToken(username, password);            Subject currentUser = SecurityUtils.getSubject();            try {                currentUser.login(token);                log.debug("Login succeeded!");                return new LoginStatus(currentUser.isAuthenticated(),                        currentUser.getPrincipal().toString());            } catch (AuthenticationException e) {                return new LoginStatus(false, null);            }        } else {            return getStatus();        }    }}

Затем я изменил зависимость LoginService в LoginController, чтобы использовать эту реализацию.

@Autowired@Qualifier("shiroLoginService")LoginService loginService;

Затем я изменил свой web.xml для Apache Shiro и попробовал «mvn jetty: run». Опять же, вход в систему, как представляется, успешно (на основе сообщений журнала) на сервере, но не удалось в пользовательском интерфейсе. При использовании http: // localhost вместо http: // localhost: 8080 все работало нормально.

Резюме В
этой статье показано, как можно программно войти в систему с помощью Java EE 6, Spring Security и Apache Shiro. До Java EE 6 (и Servlet 3) не было API для программного входа, поэтому это долгожданное дополнение. Тот факт, что мой пример входа в Ajax не работал, когда порты отличались, объясняется тем, что браузеры используют одну и ту же политику происхождения , которая указывает, что порты должны быть одинаковыми. Указание без портов (по умолчанию), кажется, лазейка.

В связи с этим я недавно обнаружил несколько интересных статей из блога AppSec .

Вторая статья имеет интересный абзац:

… есть Apache Shiro (FKA JSecurity, а затем Apache Ki ), еще одна безопасная среда для приложений Java. Хотя он выглядит проще в использовании и понимании, чем ESAPI, и охватывает большинство основных основ безопасности (аутентификация, авторизация, управление сеансами и шифрование), он не помогает позаботиться о важных функциях, таких как проверка ввода и кодирование вывода. А пользователи Spring имеют Spring Security (Acegi) всеобъемлющую, но мощную среду авторизации и аутентификации.

Итак, согласно этому блогу, обсуждаемые здесь рамки безопасности не самые лучшие.

Наиболее полным и современным выбором для разработчиков на Java является API-интерфейс ESAPI Enterprise Security от OWASP, особенно сейчас, когда только что вышла версия 2.0.

Я не слышал о многих организациях, использующих ESAPI вместо Java EE 6, Spring Security или Apache Shiro, но, возможно, я ошибаюсь. ESAPI — это то, что используется компаниями?

От http://raibledesigns.com/rd/entry/java_web_application_security_part3