За последний месяц я опубликовал ряд статей о реализации аутентификации с помощью 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 .
- Семь конфигураций безопасности (ошибок) в файлах Java web.xml
- Более безопасное программное обеспечение через безопасные рамки
Вторая статья имеет интересный абзац:
… есть 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