Одной из новых функций в AppFuse 2.1 является архетип appfuse-ws . Этот архетип использует Enunciate и CXF для создания проекта с REST API и сгенерированной HTML-документацией. Enunciate — это очень полезный инструмент, позволяющий разрабатывать веб-сервисы с аннотациями JAX-RS и JAX-WS и создавать все типы клиентских библиотек. Для меня это кажется очень полезным для разработки серверной части приложений SOFEA (современных).
Еще в марте Райан Хитон опубликовал прекрасную статью о защите веб-сервисов в приложении Enunciate. Я решил продвинуть его руководство на шаг вперед и не только защитить свои веб-службы, но и интегрировать его с OAuth 2. В этом руководстве я покажу вам, как создать новое приложение с AppFuse WS, защитить его, добавить поддержку OAuth , а затем использовать клиентское приложение для проверки подлинности и получения данных.
- Создайте новый проект AppFuse WS
- Интеграция Spring Security и OAuth
- Аутентификация и получение данных с клиентом
Создание нового проекта AppFuse WS
Для начала я посетил страницу « Создание архетипов AppFuse» и создал новое приложение, используя опцию «Только веб-сервисы» в раскрывающемся меню Web Framework . Ниже приведена команда, которую я использовал для создания проекта appfuse-oauth.
mvn archetype:generate -B -DarchetypeGroupId=org.appfuse.archetypes \ -DarchetypeArtifactId=appfuse-ws-archetype -DarchetypeVersion=2.1.0 \ -DgroupId=org.appfuse.example -DartifactId=appfuse-oauth
После этого я запустил приложение, используя mvn jetty: run, и подтвердил, что все в порядке. На этом этапе я смог просмотреть сгенерированную документацию для приложения по адресу http: // localhost: 8080 . На скриншоте ниже показано, как выглядит приложение на данный момент.
ПРИМЕЧАНИЕ. Вы можете заметить конечную точку REST / {username}. Эта ошибка в AppFuse 2.1.0 была исправлена в SVN. Это не влияет на этот урок.
Интеграция Spring Security и OAuth
Первоначально я пытался интегрировать Spring Security с руководством Enunciate по защите веб-сервисов . Тем не менее, он только защищает конечные точки и не выполняет достаточную фильтрацию для поддержки OAuth, поэтому я использовал собственный web.xml. Я поместил этот файл в src / main / resources и загрузил его в свой файл enunciate.xml . Я также обновил Spring Security и импортировал свой файл security.xml.
<?xml version="1.0"?> <enunciate xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://enunciate.codehaus.org/schemas/enunciate-1.22.xsd"> ... <webapp mergeWebXML="src/main/resources/web.xml"/> <modules> ... <spring-app disabled="false" springVersion="3.0.5.RELEASE"> <springImport uri="classpath:/applicationContext-resources.xml"/> <springImport uri="classpath:/applicationContext-dao.xml"/> <springImport uri="classpath:/applicationContext-service.xml"/> <springImport uri="classpath:/applicationContext.xml"/> <springImport uri="classpath:/security.xml"/> </spring-app> </modules> </enunciate>
Затем я создал src / main / resources / web.xml с фильтром для Spring Security и DispatcherServlet для поддержки OAuth.
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <filter> <filter-name>securityFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetBeanName</param-name> <param-value>springSecurityFilterChain</param-value> </init-param> </filter> <filter-mapping> <filter-name>securityFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>appfuse-oauth</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>appfuse-oauth</servlet-name> <url-pattern>/oauth/*</url-pattern> </servlet-mapping> </web-app>
Затем я создал файл src / main / resources / security.xml и использовал его для защиты своего API, указания страницы входа, предоставления пользователей и интеграции OAuth (см. Последние 4 компонента ниже).
<?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:oauth="http://www.springframework.org/schema/security/oauth2" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2.xsd"> <http auto-config="true"> <intercept-url pattern="/api/**" access="ROLE_USER"/> <intercept-url pattern="/oauth/**" access="ROLE_USER"/> <intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/> <form-login login-page="/login.jsp" authentication-failure-url="/login.jsp?error=true" login-processing-url="/j_security_check"/> </http> <authentication-manager> <authentication-provider> <user-service> <user name="admin" password="admin" authorities="ROLE_USER,ROLE_ADMIN"/> <user name="user" password="user" authorities="ROLE_USER"/> </user-service> </authentication-provider> </authentication-manager> <!--hook up the spring security filter chain--> <beans:alias name="springSecurityFilterChain" alias="securityFilter"/> <beans:bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.InMemoryOAuth2ProviderTokenServices"> <beans:property name="supportRefreshToken" value="true"/> </beans:bean> <oauth:provider client-details-service-ref="clientDetails" token-services-ref="tokenServices"> <oauth:verification-code user-approval-page="/oauth/confirm_access"/> </oauth:provider> <oauth:client-details-service id="clientDetails"> <!--<oauth:client clientId="my-trusted-client" authorizedGrantTypes="password,authorization_code,refresh_token"/> <oauth:client clientId="my-trusted-client-with-secret" authorizedGrantTypes="password,authorization_code,refresh_token" secret="somesecret"/> <oauth:client clientId="my-less-trusted-client" authorizedGrantTypes="authorization_code"/>--> <oauth:client clientId="ajax-login" authorizedGrantTypes="authorization_code"/> </oauth:client-details-service> </beans:beans>
Я использовал примеры приложений OAuth для Spring Security, чтобы понять это. В этом примере я использовал authorGrantTypes = «authorization_code», но из приведенных выше элементов <oauth: client> видно, что есть несколько разных вариантов. Вы также должны заметить, что clientId жестко запрограммирован как «ajax-login», что означает, что я хочу разрешить аутентификацию только одному приложению.
На данный момент, я хотел бы дать Shoutout Райан Хитон для создания как излагают и поддержка OAuth Spring Security. Отличная работа, Райан!
На этом этапе мне нужно было выполнить ряд дополнительных задач, чтобы завершить интеграцию oauth. Первым было изменить конфигурацию подключаемого модуля Jetty: 1) запустить на порту 9000, 2) загрузить мои пользовательские файлы и 3) разрешить jetty: запустить для распознавания сгенерированных файлов Enunciate. Ниже приведена окончательная конфигурация в моем pom.xml.
<plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>maven-jetty-plugin</artifactId> <version>6.1.26</version> <configuration> <connectors> <connector implementation="org.mortbay.jetty.nio.SelectChannelConnector"> <port>9000</port> <maxIdleTime>60000</maxIdleTime> </connector> </connectors> <webAppConfig> <baseResource implementation="org.mortbay.resource.ResourceCollection"> <resourcesAsCSV> ${basedir}/src/main/webapp, ${project.build.directory}/${project.build.finalName} </resourcesAsCSV> </baseResource> <contextPath>/appfuse-oauth</contextPath> </webAppConfig> <webXml>${project.build.directory}/${project.build.finalName}/WEB-INF/web.xml</webXml> </configuration> </plugin>
Затем я добавил необходимые OAuth-зависимости для Spring Security в свой pom.xml. Поскольку последний выпуск является вехой, мне также пришлось добавить репозиторий Spring.
<repository> <id>spring-milestone</id> <url>http://s3.amazonaws.com/maven.springframework.org/milestone</url> </repository> ... <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>${spring.version}</version> <exclusions> <exclusion> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> </exclusion> <exclusion> <groupId>org.springframework</groupId> <artifactId>spring-support</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth</artifactId> <version>1.0.0.M3</version> <exclusions> <exclusion> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> </exclusion> <exclusion> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> </exclusion> <exclusion> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.1.2</version> </dependency> <dependency> <groupId>taglibs</groupId> <artifactId>standard</artifactId> <version>1.1.2</version> </dependency>
Поскольку я назвал свой DispatcherServlet «appfuse-oauth» в web.xml, я создал src / main / webapp / WEB-INF / appfuse-oauth-servlet.xml для настройки Spring MVC. Мне пришлось создать каталог src / main / webapp / WEB-INF .
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd"> <!-- Scans the classpath of this application for @Components to deploy as beans --> <context:component-scan base-package="org.appfuse.examples.webapp"/> <!-- Configures the @Controller programming model --> <mvc:annotation-driven/> <!-- Resolves view names to protected .jsp resources within the /WEB-INF/views directory --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> <property name="prefix" value="/"/> <property name="suffix" value=".jsp"/> </bean> </beans>
Чтобы показать страницу подтверждения OAuth, мне нужно было создать файл src / main / java / org / appfuse / examples / webapp / AccessConfirmationController.java и сопоставить его с / oauth / verify_access. Я скопировал это из одного из примеров проектов и изменил, чтобы использовать аннотации Spring.
package org.appfuse.examples.webapp; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.oauth2.provider.ClientAuthenticationToken; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.verification.ClientAuthenticationCache; import org.springframework.security.oauth2.provider.verification.DefaultClientAuthenticationCache; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.TreeMap; /** * Controller for retrieving the model for and displaying the confirmation page * for access to a protected resource. * * @author Ryan Heaton */ @Controller @RequestMapping("/confirm_access") public class AccessConfirmationController { private ClientAuthenticationCache authenticationCache = new DefaultClientAuthenticationCache(); @Autowired private ClientDetailsService clientDetailsService; @RequestMapping(method = RequestMethod.GET) protected ModelAndView confirm(HttpServletRequest request, HttpServletResponse response) throws Exception { ClientAuthenticationToken clientAuth = authenticationCache.getAuthentication(request, response); if (clientAuth == null) { throw new IllegalStateException("No client authentication request to authorize."); } TreeMap<String, Object> model = new TreeMap<String, Object>(); ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId()); model.put("auth_request", clientAuth); model.put("client", client); return new ModelAndView("access_confirmation", model); } }
Этот контроллер делегирует src / main / webapp / access_confirmation.jsp . Я создал этот файл и наполнил его кодом для отображения кнопок «Принять» и «Запретить».
<%@ page import="org.springframework.security.core.AuthenticationException" %> <%@ page import="org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException" %> <%@ page import="org.springframework.security.oauth2.provider.verification.BasicUserApprovalFilter" %> <%@ page import="org.springframework.security.oauth2.provider.verification.VerificationCodeFilter" %> <%@ page import="org.springframework.security.web.WebAttributes" %> <%@ taglib prefix="authz" uri="http://www.springframework.org/security/tags" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <html> <head> <title>Confirm Access</title> <link rel="stylesheet" type="text/css" media="all" href="http://demo.appfuse.org/appfuse-struts/styles/simplicity/theme.css"/> <style type="text/css"> h1 { margin-left: -300px; margin-top: 50px } </style> </head> <body> <h1>Confirm Access</h1> <div id="content"> <% if (session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION) != null && !(session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION) instanceof UnapprovedClientAuthenticationException)) { %> <div class="error"> <h2>Woops!</h2> <p>Access could not be granted. (<%= ((AuthenticationException) session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION)).getMessage() %>)</p> </div> <% } %> <c:remove scope="session" var="SPRING_SECURITY_LAST_EXCEPTION"/> <authz:authorize ifAnyGranted="ROLE_USER,ROLE_ADMIN"> <h2>Please Confirm</h2> <p>You hereby authorize "<c:out value="${client.clientId}" escapeXml="true"/>" to access your protected resources.</p> <form id="confirmationForm" name="confirmationForm" action="<%=request.getContextPath() + VerificationCodeFilter.DEFAULT_PROCESSING_URL%>" method="POST"> <input name="<%=BasicUserApprovalFilter.DEFAULT_APPROVAL_REQUEST_PARAMETER%>" value="<%=BasicUserApprovalFilter.DEFAULT_APPROVAL_PARAMETER_VALUE%>" type="hidden"/> <label><input name="authorize" value="Authorize" type="submit"></label> </form> <form id="denialForm" name="denialForm" action="<%=request.getContextPath() + VerificationCodeFilter.DEFAULT_PROCESSING_URL%>" method="POST"> <input name="<%=BasicUserApprovalFilter.DEFAULT_APPROVAL_REQUEST_PARAMETER%>" value="not_<%=BasicUserApprovalFilter.DEFAULT_APPROVAL_PARAMETER_VALUE%>" type="hidden"/> <label><input name="deny" value="Deny" type="submit"></label> </form> </authz:authorize> </div> </body> </html>
Наконец, мне нужно было создать src / main / webapp / login.jsp, чтобы пользователи могли войти в систему.
<%@ page language="java" pageEncoding="UTF-8" contentType="text/html;charset=utf-8" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <html> <head> <title>Login</title> <link rel="stylesheet" type="text/css" media="all" href="http://demo.appfuse.org/appfuse-struts/styles/simplicity/theme.css"/> <style type="text/css"> h1 { margin-left: -300px; margin-top: 50px } </style> </head> <body> <h1>Login</h1> <form method="post" id="loginForm" action="<c:url value='/j_security_check'/>"> <fieldset style="padding-bottom: 0"> <ul> <c:if test="${param.error != null}"> <li class="error"> ${sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message} </li> </c:if> <li> <label for="j_username" class="required desc"> Username <span class="req">*</span> </label> <input type="text" class="text medium" name="j_username" id="j_username" tabindex="1"/> </li> <li> <label for="j_password" class="required desc"> Password <span class="req">*</span> </label> <input type="password" class="text medium" name="j_password" id="j_password" tabindex="2"/> </li> <li> <input type="submit" class="button" name="login" value="Login" tabindex="3"/> </li> </ul> </fieldset> </form> </body> </html>
Все изменения, описанные в приведенном выше разделе, необходимы для реализации OAuth, если вы создаете проект с помощью AppFuse WS 2.1. Это может показаться большим количеством кода, но я смог скопировать / вставить и заставить все это работать в приложении менее чем за 5 минут. Надеюсь, вы можете сделать то же самое. Я также планирую добавить его по умолчанию в следующую версию AppFuse. Теперь давайте посмотрим на интеграцию OAuth в клиент для аутентификации и извлечения данных из этого приложения.
Аутентификация и получение данных с клиентом.
Я изначально думал, что мое приложение GWT OAuth будет хорошим клиентом. Однако после 30 минут попыток заставить GWT 1.7.1 и плагин GWT Maven (1.1) работать с моим 64-битным Java 6 JDK на OS X я отказался. Поэтому я решил использовать приложение Ajax Login, которое я использовал в своих последних уроках по безопасности.
В этом примере я использовал OAuth2RestTemplate из Spring Security OAuth. Хотя это работает и работает хорошо, я все же хотел бы заставить вещи работать с GWT (или jQuery), чтобы продемонстрировать, как это сделать с чисто клиентской точки зрения.
Для начала я получил последний источник Ajax Login от GitHub (по состоянию на это утро) и внес некоторые изменения. Прежде всего, я добавил OAuth-зависимости Spring Security в pom.xml:
<repository> <id>spring-milestone</id> <url>http://s3.amazonaws.com/maven.springframework.org/milestone</url> </repository> ... <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth</artifactId> <version>1.0.0.M3</version> <exclusions> <exclusion> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> </exclusion> <exclusion> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> </exclusion> <exclusion> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> </exclusion> </exclusions> </dependency>
Затем я изменил src / main / webapp / WEB-INF / security.xml, добавил службу токенов OAuth и определил местоположение сервера OAuth.
<?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:oauth="http://www.springframework.org/schema/security/oauth2" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2.xsd"> ... <oauth:client token-services-ref="oauth2TokenServices"/> <beans:bean id="oauth2TokenServices" class="org.springframework.security.oauth2.consumer.token.InMemoryOAuth2ClientTokenServices"/> <oauth:resource id="appfuse" type="authorization_code" clientId="ajax-login" accessTokenUri="http://localhost:9000/appfuse-oauth/oauth/authorize" userAuthorizationUri="http://localhost:9000/appfuse-oauth/oauth/user/authorize"/>
Затем я создал контроллер, который использует OAuth2RestTemplate для выполнения запроса и получения данных из API приложения AppFuse OAuth. Я создал src / main / java / org / appfuse / examples / webapp / oauth / UsersApiController.java и заполнил его следующим кодом:
package org.appfuse.examples.webapp.oauth; import org.appfuse.model.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; import org.springframework.security.oauth2.consumer.*; import org.springframework.security.oauth2.consumer.token.OAuth2ClientTokenServices; 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.ResponseBody; import java.util.ArrayList; import java.util.List; @RequestMapping("/appfuse/users") @Controller public class UsersApiController { private OAuth2RestTemplate apiRestTemplate; @Autowired private OAuth2ClientTokenServices tokenServices; private static final String REMOTE_DATA_URL = "http://localhost:9000/appfuse-oauth/api/users"; @Autowired public UsersApiController(OAuth2ProtectedResourceDetails resourceDetails) { this.apiRestTemplate = new OAuth2RestTemplate(resourceDetails); } @RequestMapping(method = RequestMethod.GET) @ResponseBody public List<User> getUsers() { try { List users = apiRestTemplate.getForObject(REMOTE_DATA_URL, List.class); return new ArrayList<User>(users); } catch (InvalidTokenException badToken) { //we've got a bad token, probably because it's expired. OAuth2ProtectedResourceDetails resource = apiRestTemplate.getResource(); OAuth2SecurityContext context = OAuth2SecurityContextHolder.getContext(); if (context != null) { // this one is kind of a hack for this application // the problem is that the sparklr photos page doesn't remove the 'code=' request parameter. ((OAuth2SecurityContextImpl) context).setVerificationCode(null); } //clear any stored access tokens... tokenServices.removeToken(SecurityContextHolder.getContext().getAuthentication(), resource); //go get a new access token... throw new OAuth2AccessTokenRequiredException(resource); } } }
В этот момент я думал, что все будет работать, и потратил довольно много времени, стуча головой о стену, когда это не сработало. Когда я составлял письмо в список рассылки Enunciate users, я понял проблему. Похоже, что он работает, но со стороны сервера, и перенаправления обратно к клиенту не происходит. Приложение Ajax Login использует UrlRewriteFilter (для красивых URL-адресов) для перенаправления с / app / * на / $ 1, и это перенаправление теряло параметр кода в URL-адресе.
<rule> <from>/app/**</from> <to last="true" type="redirect">%{context-path}/$1</to> </rule>
Чтобы это исправить, я добавил use-query-string = «true» к корневому элементу в src / main / webapp / WEB-INF / urlrewrite.xml :
<urlrewrite default-match-type="wildcard" use-query-string="true">
После внесения всех этих изменений я запустил mvn jetty: run для обоих приложений и открыл http: // localhost: 8080 / appfuse / users в моем браузере. Все сработало, и на моем лице появилась улыбка. Я зарегистрировал изменения клиента в ajax-login на GitHub и пример appfuse-oauth в демонстрации AppFuse в Google Code . Если вы хотите увидеть этот пример в действии, я рекомендую вам ознакомиться с обоими проектами и сообщить мне, если вы обнаружите какие-либо проблемы.
От http://raibledesigns.com/rd/entry/integrating_oauth_with_appfuse_and