Creación de un API REST protegido por OAuth2

14
102185

Creación de un API REST protegido por OAuth2

Índice de contenidos

Introducción

En este tutorial vamos a construir una aplicación web que expone un API Rest en donde los usuarios registrados podrán usar dicho API para gestionar sus notas (alta, baja, modificaciones y consultas).

El API estará protegido por el protocolo estándar OAuth2 para que aplicaciones externas puedan consultar el API en nombre del usuario.

Sobre los API REST:

Rest es un estilo de arquitectura elegante y legible en donde se hace uso de los verbos HTTP para realizar las operaciones de alta (POST), baja (DELETE), modificación (PUT) y consulta (GET) de información. (Endpoints de la aplicacion del tutorial:)

Una finalidad que se persigue es la de facilitar el trabajo a los programadores que hacen uso de ella, de manera que se incremente su productividad.

Una buena API REST debe intentar cumplir los siguientes requisitos (obviamente además debe ser rápida y escalable):

  1. Estar bien documentada.
  2. En caso de errores notificar al programador de una forma clara y suficiente para que el pueda averiguar que ha pasado y si es culpa suya corregir el problema.
  3. Proporcionar un mecanismo de filtro o consulta de selección de resultados.
  4. Proporcionar un mecanismo de respuesta parcial, en donde el programador pueda indicar que campos de información desea obtener, normalmente para ahorrar ancho de banda e incrementar velocidad en consultas desde dispositivos con recursos limitados.
  5. Estar versionado, para no afectar en futuras versiones a aplicaciones que usen versiones más antiguas del API.
  6. Ofecer un mecanismo de selección del formato de los datos de salida (normalmente XML o JSON).

En este tutorial cubriremos los puntos: 2, 3, 4 y 5.

El punto 1 está documentado pero en los comentarios del código fuente, faltaría una url o documento que podrían consultar los programadores).

Para conseguir el punto 6, puedes consultar el siguiente artículo Spring MVC. Servicios REST respondiendo en JSON o XML.

Sobre OAuth2:

El protocolo estándar OAuth2 permite que las aplicaciones puedan acceder de manera delimitada a los datos (u operaciones) ubicados
en otro servicio o aplicación (proveedor de servicio) en nombre del usuario sin tener que tener que para ello darle las credenciales (usuario/password, etc) a esta aplicación tercera.

En la sección referencias y enlaces interesantes tenéis una excelente charla en castellano sobre OAuth2.

Este protocolo ofrece grandes posibilidades de integración entre aplicaciones, sin tener que compartir contraseñas, permitiendo además que el usuario pueda revocar privilegios a las aplicaciones cuando desee.

Anteriormente a OAuth, existian diversas soluciones propietarias para que las aplicaciones externas pudieran acceder a sus datos y/o servicios, por ejemplo, Google ya ofrecía ClientLogin y AuthSub.

OAuth2 se basa en el concepto de token de accesso, de manera que hay diversas formas de conseguir el token de acceso necesario para poder acceder al servicio
sin tener que en ningún momento introducir las credenciales de acceso en la aplicación cliente que desea acceder a los datos. A este proceso de obtención del token de acceso se le conoce como baile OAuth2

En este tutorial se cubrirá el baile authorization_code, pues la aplicación cliente es una aplicación ubicada en un servidor (tomcat, jetty, apache, etc).

Es un tema extenso, si quereis profundizar más en el tema os recomiendo que leais el libro Getting Started With OAuth2 de O’Relly o que veais la charla en castellano que comenté previamente.

El ejemplo, la aplicación SmallNotes:

SmallNotes es una aplicación web que expone un API Rest en donde los usuarios registrados podrán usar dicho API para gestionar sus notas (alta, baja, modificaciones y consultas).

Las notas se componen de un titulo, un contenido y una serie de enlaces.

Los endpoints (urls) que expone dicho API junto con su documentación está detallado en la sección descripción de los endpoints.

Para probar el API he creado una aplicación web cliente que de consume el API que expone SmallNotes, poniéndose previamente en funcionamiento el baile OAuth2 para obtener el accessToken necesario para invocar dichos endPoints del API.

También usar el plugin RestClient para Firefox el cual además de permitirte hacer peticiones REST, hace de cliente OAuth2 sabiendo que tiene que hacer cuando recibe del servidor el error de «necesitas un accessToken» para acceder al recurso.

A continuación vemos unas capturas de pantalla de todo el ciclo:

Haz click en las imágenes para agrandarlas:

a) Configuración de OAuth2:

Configuración de OAuth2

b) Solicita al usuario que se autenticación en la aplicación SmallNotes:

OAuth2 Autenticación

c) Solitica al usuario que autorización de los
permisos que solicita acceder la aplicación externa:

OAuth2 autorización

d) Respuesta recibida:

Respuesta recibida

También puedes ejecutar la aplicación SmallNotesExternalWebApp:

a) Autenticación en la aplicación SmallNotesExternalWebApp:

b) Al hacer clic en la opción smallNotesExternalWebApp invocará mediante RestTemplate el API REST protegido por OAuth2:

Tecnologías usadas:

Para construir la aplicación he elegido las siguientes tecnologías:

  • Git:Como sistema de control de versiones que además permite un trabajo más ágil y cómodo.
  • Maven:Para la gestión de la construcción proyecto.
  • JPA2:Como framework de persistencia.
  • Liquibase:Como gestor del ciclo de vida de la base de datos, así como para introducir datos de prueba.
  • Mockito, JUnit:Para la creación de tests unitarios y/o integración.
  • MySQL y H2:Como base de datos para entorno de desarrollo y/o pruebas. (si no fuera un tutorial usaría Redis u otra solución)
  • Spring MVC:Cómo framework de desarrollo Web y su soporte REST.
  • Spring Security:Cómo framework de seguridad.
  • Spring Security OAuth2:Implementación de OAuth2 basada de Spring MVC y Spring Security.

Descripción de los endpoints:

URL: Verbo HTTP: Descripción: Resultado (JSON):
/v1/api/notes/ GET Todas las notas del usuario logado.
[{«id»:1,»content»:»content_1″,»created»:1285322400000,»title»:»titulo_1″,»links»:[{«id»:1,»url»:»http://www.carlos-garcia.es»},{«id»:2,»url»:»https://adictosaltrabajo.com»}]},{«id»:2,»content»:»content_2″,»created»:1285326000000,»title»:»titulo_2″},{«id»:3,»content»:»content_3″,»created»:1285327200000,»title»:»titulo_3″,»links»:[{«id»:3,»url»:»http://www.autentia.com»}]}]
/v1/api/notes?fields=id,title GET Los campos id y title de todas las notas del usuario logado (respuesta parcial).
[{«id»:1,»title»:»titulo_1″},{«id»:2,»title»:»titulo_2″},{«id»:3,»title»:»titulo_3″}]
/v1/api/notes?q=title:titulo_1 GET Todas las notas del usuario logado cuyo valor para el campo «title» sea «titulo_1» (consulta o filtro).
[{«id»:1,»content»:»content_1″,»created»:1285322400000,»title»:»titulo_1″,»links»:[{«id»:1,»url»:»http://www.carlos-garcia.es»},{«id»:2,»url»:»https://adictosaltrabajo.com»}]}]
/v1/api/notes/1 GET Recupera la nota 1
{«id»:1,»content»:»content_1″,»created»:1285322400000,»title»:»titulo_1″,»links»:[{«id»:1,»url»:»http://www.carlos-garcia.es»},{«id»:2,»url»:»https://adictosaltrabajo.com»}]}
/v1/api/notes/1/links GET Recupera los links de la nota con id 1 (asociativa)
[{«id»:1,»url»:»http://www.carlos-garcia.es»},{«id»:2,»url»:»https://adictosaltrabajo.com»}]
/v1/api/notes/ POST Crea una nota.

En la petición debemos enviar la cabecera HTTP Content-Type: application/json y en el playload de la petición:

{
«title»: «mobiletest»,
«content»: «Un complemento educativo para profesores y alumnos»,
«links»: [
{ «url»: «http://www.mobiletest.es» },
{ «url»: «http://www.carlos-garcia.es» },
{ «url»: «http://www.autentia.com» }
]
}
HTTP 200 OK
/v1/api/notes/5 DELETE Elimina la nota con identificador 5 HTTP 200 OK
/v1/api/notes/1 PUT Actualiza el contenido de la nota con identificador 1

En la petición debemos enviar la cabecera HTTP Content-Type: application/json y en el playload de la petición:

{
«title»: «title_1 actualizado»,
«content»: «content_1 actualizado»,
}
HTTP 200 OK

Estructura de la base de datos a los que accederá el API REST:

Al ejecutar el proyecto, Liquibase creará la base de datos si fuese necesario con el siguiente esquema (definidos en el archivo /src/main/resources/liquibase/changeLog_1_0.xml):

Aunque no esté familiarizado con Liquibase, creo que observando el archivo es suficiente como para ver las tablas y sus relaciones.

/src/main/resources/liquibase/changeLog_1_0.xml




	
		table creations
		
		
			
				
			
			
				
			
			
				
			
			
				
			
			
				
			
		
		
		
			
				
			
			
				
			
			
				
			
			
				
			
			
			
				
			
		
		
	
		
			
				
			
			
				
			
			
				
			
		
		
	

Datos de prueba a los que consultará el API REST

Liquibase introducirá los siguientes datos de prueba definidos en el siguiente archivo /src/main/resources/liquibase/changeLog_1_1-test.xml):

/src/main/resources/liquibase/changeLog_1_1-test.xml

	


	
		Inserts for small_notes_users
		
			
			
			
			
			
		
		
			
			
			
			
			
		
		
			
			
			
			
			
		
		
			
			
			
			
			
		
		
			
			
			
			
			
		
		
			
			
			
			
			
		
		
			
			
			
		
		
			
			
			
		
		
			
			
			
		
	

Estructura del proyecto:

Cómo se puede ver en la siguiente captura de pantalla, el proyecto tiene bastante contenido. Para hacer más legible el tutorial nos centraremos en los que directamente afectan a la temática (REST y OAuth2),
dejándo a un lado detalles como Liquibase, JPA, Mockito, etc.

Creo que las clases y la configuración del proyecto están bien estructuradas semáticamente por paquetes (modelo, persistencia, servicios, oauth2, tests, etc.).

Captura de pantalla de la estructura del proyecto SmallNotes (haz clic para agrandar la imagen)

Captura de pantalla de la estructura del proyecto SmallNotes.

Las clases más importantes

es.carlosgarcia.smallnotes.service.SmallNotesServiceImpl

Esta clase proporciona la funcionalidad necesaria para cumplir la funcionalidad que expone el API REST.

	package es.carlosgarcia.smallnotes.service;

import java.util.Date;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;

import org.apache.commons.collections.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import es.carlosgarcia.smallnotes.model.Link;
import es.carlosgarcia.smallnotes.model.Note;
import es.carlosgarcia.smallnotes.model.User;
import es.carlosgarcia.smallnotes.repository.LinkRepository;
import es.carlosgarcia.smallnotes.repository.NoteRepository;
import es.carlosgarcia.smallnotes.repository.UserRepository;

/**
 * Main implementation of service to work with notes.
 * All methods are transactional.
 * 
 * @author Carlos García. 
 * @see http://www.carlos-garcia.es
 * @see http://www.autentia.com
 */
@Service
@Transactional(readOnly=false)
public class SmallNotesServiceImpl implements SmallNotesService {
	private final Logger logger = LoggerFactory.getLogger(SmallNotesServiceImpl.class);
	
	private NoteRepository noteRepository;
	private UserRepository userRepository;
	private LinkRepository linkRepository;
	
	public SmallNotesServiceImpl(){
		super();
	}
	
	@Inject
	public SmallNotesServiceImpl(NoteRepository noteRepository, UserRepository userRepository, LinkRepository linkRepository){
		super();
		
		this.noteRepository = noteRepository;
		this.userRepository = userRepository;
		this.linkRepository = linkRepository;
	}

	@Override
	public void create(Note note) {
		logger.debug("creating note title {}, owner: {}", note.getTitle(), note.getOwner().getId());
		
		note.setCreated(new Date());
		
		// Ensure that links has reference to its note
		List links = note.getLinks();
		if (links != null){
			for (Link link : links){
				link.setNote(note);
			}
		}
		
		this.noteRepository.saveOrUpdate(note);
		
		if (CollectionUtils.isNotEmpty(links)){
			for (Link link : links) {
				logger.debug("creating note link url: {}", link.getUrl());
				link.setNote(note);
				this.linkRepository.saveOrUpdate(link);
			}
		}
	}

	@Override
	public void update(Note note) {
		Note persistedNote = this.getNote(note.getId());
		
		if (persistedNote == null){
			throw new IllegalArgumentException("Note not exists, id: " + note.getId());
		}
		
		persistedNote.setContent(note.getContent());
		persistedNote.setTitle(note.getTitle());
		
		this.noteRepository.saveOrUpdate(persistedNote);
	}
	
	@Override
	public void delete(Note note) {
		logger.debug("deleting note id {}, owner: {}", note.getId(), note.getOwner().getId());
		this.noteRepository.delete(note);
	}

	@Override
	@Transactional(readOnly=true)
	public List getAll(User user, Map params) {
		logger.debug("getting notes by user id {}", user.getId());
		
		List notes = noteRepository.getAllByUserEmail(user.getEmail(), params);
		
		logger.debug("num notes readed {}", CollectionUtils.size(notes));
		return notes;
	}

	@Override
	@Transactional(readOnly=true)
	public Note getNote(Integer noteId) {
		Note note = noteRepository.findByPK(Note.class, noteId);
		return note;
	}

	@Override
	public User getUserByEmail(String email) {
		return this.userRepository.getUserByEmail(email);
	}
	
	
	@Override
	public int deleteNotes(User user) {
		List notes = this.getAll(user, null);
		int numDeleted = 0;
		
		if (notes != null) {
			for (Note note : notes) {
				this.noteRepository.delete(note);
				numDeleted++;
			}
		}
		
		return numDeleted;
	}
}

es.carlosgarcia.smallnotes.web.oauth2.api.SmallNotesAPI

Controller de Spring MVC que implementa el API REST securizado por OAuth2

Observe que:

  • Los endpoints están protegidos con @PreAuthorize haciendo uso de las funciones definidas en el expresion handler oauthWebExpressionHandler

    (observe el archivo /src/main/resources/security/smallNotes-security-rules.xml)
  • La expresión de @PreAuthorize indica que debe ser un accessToken perteneciente a un usuario authenticado y que además tenga un escope y un role especifico.

    Con «usuario authenticado» me refiero a que hay un baile OAuth2 de nombre Client Credentials Flow para acceder a recursos protegidos por OAuth2 desde aplicaciones sin intervención del usuario.
  • Observer la sección Exception Handlers en donde mapeamos excepciones de negocio a instancias de RestApiError para notificar al usuario del problema.

    En la sección referencias dispone de un enlace a otra forma de realizar esta tarea así como documentación útil.

package es.carlosgarcia.smallnotes.web.oauth2.api;

import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
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;
import org.springframework.web.bind.annotation.ResponseStatus;

import es.carlosgarcia.smallnotes.exception.AccessDeniedException;
import es.carlosgarcia.smallnotes.exception.NoteNotFoundException;
import es.carlosgarcia.smallnotes.model.Link;
import es.carlosgarcia.smallnotes.model.Note;
import es.carlosgarcia.smallnotes.model.User;
import es.carlosgarcia.smallnotes.service.SmallNotesService;

/**
 * API protected by OAuth2
 * @author Carlos García
 * @see http://www.carlos-garcia.es
 * @see http://www.autentia.com
 */
@Controller
@RequestMapping(value = "/v1/")
public class SmallNotesAPI implements InitializingBean {
	private static final Logger logger  = LoggerFactory.getLogger(SmallNotesAPI.class);
	
	private SmallNotesService smallNotesService;
	
	public SmallNotesAPI(){
		super();
	}
	
	@Autowired
	public SmallNotesAPI(SmallNotesService smallNotesService){
		logger.debug("Creating SmallNotesAPIImpl");
		this.smallNotesService = smallNotesService;
	}
	
	@Override
	public void afterPropertiesSet() throws Exception {
		Validate.notNull(this.smallNotesService);
	}
	
	/**
	 * @param filter If not null, filter to be applied. Example: q?title:titulo1,content:Contenido1
	 * @param fields If not null. Partial response. Example: ?fields=id,title
	 * @return Note list of the logged user.
	 */
	@RequestMapping(value = "/api/notes", method = RequestMethod.GET)
	@PreAuthorize("#oauth2.isUser() and #oauth2.clientHasRole('ROLE_USER') and #oauth2.hasScope('read')")
	@ResponseBody 
	public List getNotes(@RequestParam(required=false, value="q") String filter, @RequestParam(required=false, value="fields") String fields) {
		logger.debug("SmallNotesAPIImpl.getNotes. filter {}, fields {}", filter, fields);
		
		User user = this.getLoggedUser();

		Map params = null;
		
		if (StringUtils.isNotBlank(filter)){
			params = SmallNotesApiHelper.parseFilter(filter);
		}
		
		List notes = this.smallNotesService.getAll(user, params);
		
		if (logger.isDebugEnabled()){
			int numNotes = 0;
			if (notes != null){
				numNotes = notes.size();
			}
			logger.debug("num notes that match filter {}", numNotes);
		}
		
		// ¿Is partial response requested?
		if (StringUtils.isNotBlank(fields) && (notes != null)){
			return SmallNotesApiHelper.applyPartialResponse(notes, fields);
		} else {
			return notes;
		}
	}
	

	/**
	 * Create a note.
	 * @param note Note to be created
	 */
	@RequestMapping(value = "/api/notes", method = RequestMethod.POST)
	@PreAuthorize("#oauth2.isUser() and #oauth2.clientHasRole('ROLE_USER') and #oauth2.hasScope('write')")
	@ResponseStatus(value=HttpStatus.OK)
	public void createNote(@RequestBody Note note) {
		User user = this.getLoggedUser();
		note.setOwner(user);
		smallNotesService.create(note);
	}

	/**
	 * Delete all notes of user logged in.
	 * @return The number of notes deleted.
	 */
	@RequestMapping(value = "/api/notes", method = RequestMethod.DELETE)
	@PreAuthorize("#oauth2.isUser() and #oauth2.clientHasRole('ROLE_USER') and #oauth2.hasScope('write')")
	@ResponseStatus(value=HttpStatus.OK)
	@ResponseBody
	public int deleteNotes() {
		User user = this.getLoggedUser();
		int  numDeleted = this.smallNotesService.deleteNotes(user);
		return numDeleted;
	}
	

	/**
	 * Get a note by id.
	 * @param noteId Note identify.
	 * @param fields If not null. Partial response. Example: ?fields=id,title
	 * @return A note.
	 */
	@RequestMapping(value = "/api/notes/{noteId}", method = RequestMethod.GET)
	@PreAuthorize("#oauth2.isUser() and #oauth2.clientHasRole('ROLE_USER') and #oauth2.hasScope('read')")
	@ResponseBody 
	public Note getNote(@PathVariable Integer noteId, @RequestParam(required=false, value="fields") String fields) {
		Note note = this.getUserNote(noteId);
		
		logger.debug("SmallNotesAPIImpl.getNote Id: {}", noteId);

		if (StringUtils.isBlank(fields)){
			return note;
		} else {
			return SmallNotesApiHelper.applyPartialResponse(note, fields);	
		}
	}

	/**
	 * Delete a note.
	 * @param noteId Code of the note to be deleted
	 */
	@RequestMapping(value = "/api/notes/{noteId}", method = RequestMethod.DELETE)
	@PreAuthorize("#oauth2.isUser() and #oauth2.clientHasRole('ROLE_USER') and #oauth2.hasScope('write')")
	@ResponseStatus(value=HttpStatus.OK)
	public void deleteNote(@PathVariable Integer noteId) {
		Note note = this.getUserNote(noteId);
		if (note != null){
			this.smallNotesService.delete(note);
		}
	}

	/**
	 * Update a note.
	 * @param noteId Note identify.
	 * @param note Note content to be created
	 */
	@RequestMapping(value = "/api/notes/{noteId}", method = RequestMethod.PUT)
	@PreAuthorize("#oauth2.isUser() and #oauth2.clientHasRole('ROLE_USER') and #oauth2.hasScope('write')")
	@ResponseStatus(value=HttpStatus.OK)
	public void updateNote(@PathVariable Integer noteId, @RequestBody Note note) {
		if ((note == null) || (noteId != note.getId())){
			throw new IllegalArgumentException("Invalid note");
		}
		
		// this method check that logger user is the owner of note
		Note notePersisted = this.getUserNote(noteId);

		note.setOwner(notePersisted.getOwner());
		
		this.smallNotesService.update(note);
	}   
	
	/**
	 * Get links of a note
	 * @param noteId Note code
	 * @return The links of the note.
	 */
	@RequestMapping(value = "/api/notes/{noteId}/links", method = RequestMethod.GET)
	@PreAuthorize("#oauth2.isUser() and #oauth2.clientHasRole('ROLE_USER') and #oauth2.hasScope('read')")
	@ResponseBody 
	public List getNoteLinks(@PathVariable Integer noteId) {
		Note note = this.getUserNote(noteId);
		
		logger.debug("SmallNotesAPIImpl.getNoteLinks Id: {}, user {}", noteId);
		
		return note.getLinks();
	}
	
	/**
	 * Error if user try to access to any other url.
	 */
	@RequestMapping(value = "/api/**", method = RequestMethod.GET)
	public void notExistsUrlHandler(HttpServletRequest request) {
		throw new IllegalArgumentException("Requested url not exists: " + request.getRequestURI());
	}
	
	/**
	 * Get a note validate that user is the owner.
	 * @param noteId Note code to get.
	 * @return Return the note
	 */
	private Note getUserNote(Integer noteId) {
		Note note = this.smallNotesService.getNote(noteId);
		
		if (note == null){
			throw new NoteNotFoundException(noteId);
		}
		
		User user = this.getLoggedUser();
		if (! user.equals(note.getOwner())){
		 	throw new AccessDeniedException(noteId);
		}
		
		return note;
	}
	
	/**
	 * @return The logged user
	 */
	private User getLoggedUser(){
		String email =  null;
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		Object	   principal  = authentication.getPrincipal();
		
		
		if (principal instanceof UserDetails) {
			email =  ((UserDetails) principal).getUsername();
		} else if (principal instanceof UsernamePasswordAuthenticationToken){
			email = ((UsernamePasswordAuthenticationToken) principal).getName();
		} else if (principal instanceof OAuth2Authentication){
		    email = ((OAuth2Authentication) principal).getUserAuthentication().getName();
		}
		
		logger.debug("Logged user email {}", email);
		
		Validate.notNull(email);
		
		User user  = this.smallNotesService.getUserByEmail(email);

		logger.debug("Logged user {}", user);
		
		return user;
	}

	
	/**
	 * Exception Handlers
	 */
	
	@ExceptionHandler(NoteNotFoundException.class)
	protected @ResponseBody ResponseEntity handleNoteNotFoundException(NoteNotFoundException noteNotFoundException, HttpServletRequest request, HttpServletResponse response) {
		RestApiError restApiError = new RestApiError(HttpStatus.NOT_FOUND, ApiErrorCode.NOTE_NOT_FOUND, noteNotFoundException.getMessage(), noteNotFoundException.getMessage(), this.getInfoUrl(ApiErrorCode.NOTE_NOT_FOUND));
		return SmallNotesApiHelper.createAndSendResponse(restApiError);
	}

	@ExceptionHandler(AccessDeniedException.class)
	protected @ResponseBody ResponseEntity handleAccessDeniedException(AccessDeniedException accessDeniedException, HttpServletRequest request, HttpServletResponse response) {
		RestApiError restApiError = new RestApiError(HttpStatus.UNAUTHORIZED, ApiErrorCode.ACCESS_DENIED, accessDeniedException.getMessage(), accessDeniedException.getMessage(), this.getInfoUrl(ApiErrorCode.ACCESS_DENIED));
		return SmallNotesApiHelper.createAndSendResponse(restApiError);
	}   

	@ExceptionHandler(SecurityException.class)
	protected @ResponseBody ResponseEntity handleSecurityException(SecurityException exception, HttpServletRequest request, HttpServletResponse response) {
		RestApiError restApiError = new RestApiError(HttpStatus.UNAUTHORIZED, ApiErrorCode.SECURITY, exception.getMessage(), exception.getMessage(), this.getInfoUrl(ApiErrorCode.SECURITY));
		return SmallNotesApiHelper.createAndSendResponse(restApiError);
	}
	
	@ExceptionHandler(Exception.class)
	protected  @ResponseBody  ResponseEntity handleException(Exception exception, HttpServletRequest request, HttpServletResponse response) {
		RestApiError restApiError = new RestApiError(HttpStatus.BAD_REQUEST, ApiErrorCode.GENERIC, exception.getMessage(), exception.getMessage(), this.getInfoUrl(ApiErrorCode.GENERIC));
		return SmallNotesApiHelper.createAndSendResponse(restApiError);
	}
	
	private String getInfoUrl(ApiErrorCode code){
		return "http://yourAppUrlToDocumentedApiCodes.com/api/support/" + code.ordinal();
	}
}

es.carlosgarcia.smallnotes.web.oauth2.api.SmallNotesApiHelper

Esta clase tiene métodos de utilidad relacionados con el procesado de parámetros de filtro y respuesta parcial.

package es.carlosgarcia.smallnotes.web.oauth2.api;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.ArrayUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

import es.carlosgarcia.smallnotes.model.Note;

/**
 * Helper method for SmallNotesApi
 * @author Carlos García
 * @see http://www.carlos-garcia.es
 * @see http://www.autentia.com
 */
public class SmallNotesApiHelper {
	
	public static ResponseEntity createAndSendResponse(RestApiError restApiError) {
		HttpHeaders headers = new HttpHeaders();
		headers.set("Cache-Control", "no-store");
		headers.set("Pragma", "no-cache");
		headers.setContentType(MediaType.APPLICATION_JSON);
		ResponseEntity responseEntity = new ResponseEntity(restApiError, headers, restApiError.getHttpStatusCode());
		return responseEntity;
	}
	
	/**
	 * Parse filter string to build a map with format .
	 * @param filter Query String, Example: title:titulo1,content:Contenido1
	 * @return A map with format 
	 */
	public static Map parseFilter(String filter) {
		HashMap map = new HashMap();
		StringTokenizer fields  = new StringTokenizer(filter, ",");
		Note note = new Note();
		String fieldName  = null;
		
		try {
			while (fields.hasMoreTokens()){
				StringTokenizer field = new StringTokenizer(fields.nextToken(), ":");
				if (field.countTokens() != 2){
					throw new IllegalArgumentException(filter);
				}
				
				fieldName  = field.nextToken();
				String fieldValue = field.nextToken();
				
				// Verify that fieldName exists as field on Note class
				PropertyUtils.getSimpleProperty(note, fieldName);
	
				map.put(fieldName, fieldValue);
			}
		} catch (NoSuchMethodException e) {
			throw new IllegalArgumentException(String.format("Property %s does not exists", fieldName));
		} catch (IllegalAccessException ex){
			throw new IllegalArgumentException(filter);
		} catch (InvocationTargetException ex){
			throw new IllegalArgumentException(filter);
		}
		
		return map;
	}
	
	/**
	 * Apply partial response to Note
	 * @param notes Full property note 
	 * @param fields Fields to return.
	 * @return A Note with only "fields" with values. 
	 */	
	public static Note applyPartialResponse(Note note, String fields){
		String[] fieldsToIgnore =  SmallNotesApiHelper.constructPartialResponseFieldsToIgnore(fields);
		Note partialResponseNote = new Note();
		BeanUtils.copyProperties(note, partialResponseNote, fieldsToIgnore);
		return partialResponseNote;			
	}
	
	
	/**
	 * Apply partial response.
	 * @param notes Full property note list 
	 * @param fields Fields to return.
	 * @return A Note list with only "fields" with values. 
	 */
	public static List applyPartialResponse(List notes, String fields){
		String[] fieldsToIgnore =  SmallNotesApiHelper.constructPartialResponseFieldsToIgnore(fields);
		
		List partialResponseNotes = new ArrayList(notes.size());
		for (Note note : notes){
			Note partialResponseNote = new Note();
			BeanUtils.copyProperties(note, partialResponseNote, fieldsToIgnore);
			partialResponseNotes.add(partialResponseNote);
		}
		
		// Important: Note class is has @JsonSerialize(include = Inclusion.NON_DEFAULT) setting and for this reason Partial response is working fine. 
		return partialResponseNotes;
	}
	
	
	/**
	 * @param fields Fields that user want to get response only.
	 * @return And String[] with the others fields, (fields to be ignored)
	 */
	private static String[] constructPartialResponseFieldsToIgnore(String fields){
		 String[] partialResponseFields = fields.split(",");
		 String	  currentFieldName = null;
		 
		 Note note = new Note();
		 
		 // First, validate that all fields exists on class
		 try {
			 for (int i = 0, num = partialResponseFields.length; i < num; i++){
				 currentFieldName = partialResponseFields[i];
				 PropertyUtils.getSimpleProperty(note, currentFieldName);
			 }
		 } catch (NoSuchMethodException e) {
			 throw new IllegalArgumentException(String.format("Property %s does not exists", currentFieldName));
		 } catch (IllegalAccessException ex){
			 throw new IllegalArgumentException(fields);
		 } catch (InvocationTargetException ex){
			 throw new IllegalArgumentException(fields);
		 } catch (Exception ex){
			 throw new IllegalArgumentException(fields);
		 }
			
		 
		 Set fieldsToIgnoreList = new TreeSet();
		 
		// Constuct Field to Ignore array
		PropertyDescriptor[] properties = BeanUtils.getPropertyDescriptors(note.getClass());
		for (int i = 0, numProperties = properties.length; i < numProperties; i++){
			if (! ArrayUtils.contains(partialResponseFields, properties[i].getName())){
				fieldsToIgnoreList.add(properties[i].getName());
			}
		}
		String[] fieldsToIgnore =  fieldsToIgnoreList.toArray(new String[0]);
		
		return fieldsToIgnore;
	}
}

es.carlosgarcia.smallnotes.web.oauth2.api.RestApiError

Representa el error de negocio que será enviado (en JSON) al cliente cuando sea necesario.

package es.carlosgarcia.smallnotes.web.oauth2.api;

import org.springframework.http.HttpStatus;

/**
 * Rest api error.
 * @author Carlos García
 * @see http://www.carlos-garcia.es
 * @see http://www.autentia.com
 */
public class RestApiError {
	
    private final HttpStatus httpStatusCode;
    private final ApiErrorCode apiCode;
    private final String message;
    private final String devMessage;
    private final String infoUrl;

    public RestApiError(HttpStatus httpStatusCode, ApiErrorCode apiCode, String message, String devMessage, String infoUrl) {
        this.httpStatusCode = httpStatusCode;
        this.apiCode = apiCode;
        this.message = message;
        this.devMessage = devMessage;
        this.infoUrl = infoUrl;
    }

    public HttpStatus getHttpStatusCode() {
        return this.httpStatusCode;
    }
 
    public ApiErrorCode getApiCode() {
        return this.apiCode;
    }

    public String getMessage() {
        return this.message;
    }

    public String getDeveloperMessage() {
        return this.devMessage;
    }

    public String getInfoUrl() {
        return this.infoUrl;
    }
}

es.carlosgarcia.smallnotes.web.oauth2.api.SmallNotesApiTest

Tests unitarios de la clase es.carlosgarcia.smallnotes.web.oauth2.api.SmallNotesApi.

package es.carlosgarcia.smallnotes.web.oauth2.api;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.OAuth2Authentication;

import es.carlosgarcia.smallnotes.model.Note;
import es.carlosgarcia.smallnotes.model.User;
import es.carlosgarcia.smallnotes.service.SmallNotesService;

/**
 * Unit tests for class SmallNotesApiImpl
 * @author Carlos García
 * @see http://www.carlos-garcia.es
 * @see http://www.autentia.com
 */
@RunWith(MockitoJUnitRunner.class)
public class SmallNotesApiTest {

	@Mock
	private SecurityContext securityContext;
	
	@Mock
	private OAuth2Authentication principal;
	
	@Mock
	private SmallNotesService smallNotesService;
	
	@Mock
	private Authentication authentication;
	
	@Mock
	private User loggedUser;
	
	
	@Before
	public void initAuthentication(){
		final String USER_EMAIL_1 = "cgpcosmad@gmail.com";
		
		Mockito.when(securityContext.getAuthentication()).thenReturn(authentication);
		Mockito.when(authentication.getPrincipal()).thenReturn(principal);
		
		Mockito.when(principal.getUserAuthentication()).thenReturn(authentication);		
		Mockito.when(authentication.getName()).thenReturn(USER_EMAIL_1);
		Mockito.when(smallNotesService.getUserByEmail(USER_EMAIL_1)).thenReturn(loggedUser);
		
		SecurityContextHolder.setContext(securityContext);
	}
	
	@Test
	public void shouldGetAllUserNotesWhenNoFilter(){
		Mockito.when(smallNotesService.getAll(loggedUser, null)).thenReturn(null);
		
		SmallNotesAPI smallNotesApi = new SmallNotesAPI(smallNotesService);
		
		smallNotesApi.getNotes(null, null);
		
		// Verify that getAll method has been called
		Mockito.verify(smallNotesService).getAll(loggedUser, null);
	}
	
	
	@Test
	public void shouldCreateNote(){
		Note note = Mockito.mock(Note.class);
		Mockito.when(note.getId()).thenReturn(null);
		
		SmallNotesAPI smallNotesApi = new SmallNotesAPI(smallNotesService);
		smallNotesApi.createNote(note);
		
		// Verify that create method has been called
		Mockito.verify(smallNotesService).create(note);
	}
	
	
	@Test
	public void shouldDeleteNote(){
		final int NOTE_ID_TO_BE_DELETED = 1;
		
		Note note = Mockito.mock(Note.class);
		Mockito.when(note.getId()).thenReturn(NOTE_ID_TO_BE_DELETED);
		Mockito.when(note.getOwner()).thenReturn(this.loggedUser);
		Mockito.when(smallNotesService.getNote(NOTE_ID_TO_BE_DELETED)).thenReturn(note);
		
		SmallNotesAPI smallNotesApi = new SmallNotesAPI(smallNotesService);
		smallNotesApi.deleteNote(NOTE_ID_TO_BE_DELETED);
		
		// Verify that delete method has been called. 
		Mockito.verify(smallNotesService).delete(note);
	}
}

Archivos de configuración

src/main/resources/security/security.xml

Configuración normal de los beans necesarios para una seguridad clásica por formulario de usuario/contraseña:




	
   
  	
		
			
		
	

/src/main/resources/security/smallNotes-security-rules.xml

Definición de la protección de endpoints, en donde podemos observar:

  • En la url /oauth/token es donde en el baile oauth2, un usuario ya authenticado (en la url /oauth/authorize) obtiene un accessToken para el token de autorización que obtuvo en la primera parte del baile de oauth2.

    Los parámetros que debe recibir son los especificados en el estándar de OAuth2 (code, el hash de client_id+client_secret, state y redirect_uri), estos serán procesados en el filtro clientCredentialsTokenEndpointFilter y validados en clientAuthenticationManager

  • La url /v1/api/notes/** define la protección del Api REST por OAuth2. Será el filtro resourceServerFilter el que extraiga el accessToken de BD (haciendo uso de TokenStore) e introducciendo el OAuth2Authentication en el contexto de seguridad de SpringSecurity SecurityContextHolder.getContext().setAuthentication(authentication);
	


	
		
		
		
		
		
	

	
		
		
		
		
	
	
	
	
		
		
		
		
	

/src/main/resources/security/smallNotes-security-oauth2.xml

En el siguiente archivo destacaremos:

  • tokenStore:

    Es la clase que se encargará de acceder a las tablas de base de datos relacionadas con los accessTokens.

  • clientDetailsService: Es la clase que se encargará de acceder a las tablas de base de datos relaconadas con las aplicaciones clientes que solicitan autorización, es decir, esta clase se usa principalmente en el baile OAuth2 para conseguir el accessToken).

  • userApprovalHandler: Esta clase se encarga de ver si una determinada petición ya estaba aprobada por el usuario.

  • accessConfirmationController: Esta clase se encarga de mostrar el gui de solicitud de autorización así como de recibir la respuesta del usuario en ese gui.

  • oauth:authorization-server: Este tag se engarga de crear y configurar los Beans y Filtros necesarios tanto para el proceso de authorización como para el endpoint de entrega de accessToken una vez que el usuario ha autorizado a la aplicación. Más concretamente este tag crea dos Beans principales:

    • AuthorizationEndPoint: Se encarga del proceso de autorización OAuth2 generando el token de autorización y redirigiendo al usuario al endPoint dónde escucha TokenEndPoint.

    • TokenEndPoint:

      Se encarga de generar un accessToken (y persistirlo) para un código de autorización que recibe como parámetro

  • resourceServerFilter: Esta clase se encarga de registrar un filtro que extraerá el accessToken de la cabecera HTTP Authorization (o el parametro accessToken) consultará en BD ese accessToken y si existe construirá el OAut2Authentication y lo meterá en el contexto de SpringSecurity.




	
	
	
	
		
	
	

	
		
	
	
	
		
	
	
	
			
	
	

	
		
	


	
		
		
		
		
			
		
	
	
	
		
	
	
	
		
	
 
	

	    	
	
		
	
	
		
			
				
				
				
			
		
	 

	
		
	
		
	

/src/main/resources/liquibase/changeLog_OAuth2.xml

Es un archivo de creación de las tablas relacionadas con OAuth2 en formato Liquibase, el cuál se he creado extrayendo dicha definición del código fuente de Spring Security OAuth2.

Un par de observaciones a destacar:

  1. El campo authentication de la tabla oauth_access_token es una serialización de la instancia OAuth2Authentication asociada a ese accessToken, es decir a los permisos exactos que concedió el usuario en el momento de crear el token.
  2. La tabla oauth_client_details contiene las aplicaciones autorizadas para obtener accessTokens. (mediante el baile OAuth).
  3. El campo authorized_grant_types de la tabla oauth_client_details indica que clases de bailes permitimos para ese cliente, implicit, authorization_code, etc. (es un tema extenso, ver la documentación o preguntarme).
  4. El campo scope de la tabla oauth_client_details nos sirve para securizar los recursos de forma lógica.

Por ejemplo scopes de Facebook y los scopes de Google




	
        
            
            
            
            
            
            
            
        
        
        
            
                
            
            
            
            
            
            
            
            
            
            
        
        
        
            
            
        
        
        
            
            
            
        
    

	
		Insert one OAuth2Client
		
			
			
			
			
				
							
			
							
							
		
	

Descargar código fuente

A continuación puede descargarse los proyectos.

Ambos proyectos son projectos maven, por lo que puede por ejemplo iniciarlos mediante el comando:

mvn tomcat:run -Dmaven.tomcat.port=8080 (smallNotes)
mvn tomcat:run -Dmaven.tomcat.port=9080 (smallNotesExternalWebApp)

Para smallNotes, debes establecer las propiedades de conexión a la bd (que será creada automáticamente) en el archivo appcore.properties

Referencias y enlaces interesantes:

Conclusiones

A fecha de hoy, 25-Julio-2012, el proyecto Spring Security OAuth2, está a punto de ser una release (continua en milestone). Desde mi punto de vista, aunque el código fuente es de gran calidad y viene con un buen ejemplo, he echado mucho en falta más documentación para poder comprender como encajan las piezas, así que en muchos casos he tenido que ir analizando el código fuente.

Supongo que será cuestión de tiempo que la comunidad vaya nutriendo este estupendo proyecto con más documentación, tutoriales y ejemplos.

Es una implementación madura de la especificación OAuth2 (y OAuth1), además es muy completa pues se cubren todos los flujos (grantTypes) para la obtención de accessToken para cada tipo de aplicación (JavaScript, móviles, servidor, dispositivo tipo televisión, ...)

Espero que os haya sido de utilidad.

Un saludo.

Carlos García.

14 COMENTARIOS

  1. I changed the properties in smallNotes to (I don’t want to install mysql to run the example):

    app.environment=developer
    db.driverClassName=org.h2.Driver
    db.url=jdbc:h2:mem:smallNotesDB;DB_CLOSE_DELAY=-1
    db.username=sa
    db.password=

    with my browser pointing to http://localhost:8080/smallNotes/ everything is fine. I can go on an log with user1@smallnotes.es / 1234

    But when I try to access using a rest call (with firefox rest client) I receive the error:

    {«message»:»Access is denied»,»infoUrl»:»http://yourAppUrlToDocumentedApiCodes.com/api/support/3″,»httpStatusCode»:»BAD_REQUEST»,»apiCode»:»GENERIC»,»developerMessage»:»Access is denied»}

    ps: I can see the access token gerenation + the confirmation screen. The problem happens when I call http://localhost:8080/smallNotes/v1/api/notes/1 on firefox restclient. Any Idea?

  2. Hi Wanderlei.

    Check that you add the header \\\»Authorization: Bearer \\\»
    instead of \\\»Authorization: OAuth \\\»

    Be sure that accesstoken is not expired too.

    Regards

  3. Hi Wanderlei.

    Check that you requested the needed scope (read,write) the your requests. (First Image of OAuth2 Dance capture screens)

  4. Check that you add the header \\\»Authorization: Bearer \\\»
    instead of \\\»Authorization: OAuth \\\»

    Yep, looks that is the problem. My Restclient use Authorization: OAuth instead of Authorization: Bearer . But even adding manually a new authorization header with content Bearer does not work. =( And also \\\»Authorization: OAuth \\\» should not be supported? Thank you again.

  5. Buenas Carlos,

    Primero felicitarte por este gran tutorial, es lo mejor que he encontrado. Pero me encuentro con un problema.

    Estoy probando con el pluging RestClient pero cuando llego a la captura que tu numeras como C y acepto el acceso, el sistema me devuelve un código que no es el token, me lo devuelve a la dirección que indico como endpoint de re dirección (Algo como así localhost:8080/web_service/?code=jnGZuJ). Mi pregunta es, ¿cual sería el siguiente paso para obtener el token a partir del código?

    Muchas gracias por tu ayuda.
    Saludos

  6. Gracias Rafa.

    Veo que te has quedado a la mitad del baile OAuth, es decir, en el proceso de autenticación del token aleatorio por parte del usuario en el servicio al que pretendes acceder (En este caso SmallNotes).

    El motivo es que la URL de autorización debe ser una del la aplicación Proveedora del servicio (SmallNotes), es decir la que tiene el recurso a proteger y cuyo acceso debe ser autorizado por el usuario (El usuario debe autorizar el acceso a SmallNotesExternalWebApp)

    Es decir, la url no puede ser localhost:8080/web_service/?code=jnGZuJ, sino que tiene que ser una http://localhost:8080/smallNotes/

    Escribela igual que en las capturas de pantalla.

    Saludos

  7. Gracias Carlos, has sido de gran ayuda.

    Estoy de acuerdo contigo en la falta de documentación sobre la tecnología, y la que hay es muy pobre.
    Compraré el libro de O’Relly para pulir conocimientos.

    Saludos

    Muchas gracias

  8. Buenas Carlos,

    Ya tengo implementada mi api REST con Oauth2, pero me gustaría adaptar tu ejemplo para usar el grant_types de tipo password , quiero que los clientes móviles obtengan el token directamente.

    He añadido a la definición oauth:authorization-server el campo oauth:password pero no funciona me dice que el SecurityContext esta vacío.

    Para el cliente estoy usando Spring for Android, agradecería tu ayuda.

    Saludos

  9. Buenos días,
    Excelente tutorial. Es importante también recalcar que para ser uso de oAuth 2 es obligatorio realizar las peticiones mediante https (SSL).

    Saludos

  10. Buen Dia Carlos
    Descargue el codigo no realize ningun tipo de cambio en el codigo, solo lo copile y los desplegue en mi JBoss 7.1.1, y en el despligue me genera el siguiente error:

    11:57:02,979 ERROR [org.springframework.web.context.ContextLoader] (MSC service thread 1-2) Context initialization failed: org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Failed to import bean definitions from URL location [classpath:/smallNotes.xml]
    Offending resource: ServletContext resource [/WEB-INF/smallNotes-servlet.xml]; nested exception is org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Failed to import bean definitions from URL location [classpath:/security/smallNotes-security.xml]
    Offending resource: class path resource [smallNotes.xml]; nested exception is org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Unable to locate Spring NamespaceHandler for XML schema namespace [http://www.springframework.org/schema/security]
    Offending resource: class path resource [security/smallNotes-security.xml]

    at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:68) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:85) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:76) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.importBeanDefinitionResource(DefaultBeanDefinitionDocumentReader.java:245) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseDefaultElement(DefaultBeanDefinitionDocumentReader.java:196) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:181) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:140) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:111) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.registerBeanDefinitions(XmlBeanDefinitionReader.java:493) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:390) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:334) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:302) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:174) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:209) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:180) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.web.context.support.XmlWebApplicationContext.loadBeanDefinitions(XmlWebApplicationContext.java:125) [spring-web-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.web.context.support.XmlWebApplicationContext.loadBeanDefinitions(XmlWebApplicationContext.java:94) [spring-web-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.context.support.AbstractRefreshableApplicationContext.refreshBeanFactory(AbstractRefreshableApplicationContext.java:131) [spring-context-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.obtainFreshBeanFactory(AbstractApplicationContext.java:522) [spring-context-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:436) [spring-context-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:385) [spring-web-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:284) [spring-web-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:111) [spring-web-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.apache.catalina.core.StandardContext.contextListenerStart(StandardContext.java:3392) [jbossweb-7.0.13.Final.jar:]
    at org.apache.catalina.core.StandardContext.start(StandardContext.java:3850) [jbossweb-7.0.13.Final.jar:]
    at org.jboss.as.web.deployment.WebDeploymentService.start(WebDeploymentService.java:90) [jboss-as-web-7.1.1.Final.jar:7.1.1.Final]
    at org.jboss.msc.service.ServiceControllerImpl$StartTask.startService(ServiceControllerImpl.java:1811)
    at org.jboss.msc.service.ServiceControllerImpl$StartTask.run(ServiceControllerImpl.java:1746)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) [rt.jar:1.7.0_75]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) [rt.jar:1.7.0_75]
    at java.lang.Thread.run(Thread.java:745) [rt.jar:1.7.0_75]
    Caused by: org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Failed to import bean definitions from URL location [classpath:/security/smallNotes-security.xml]
    Offending resource: class path resource [smallNotes.xml]; nested exception is org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Unable to locate Spring NamespaceHandler for XML schema namespace [http://www.springframework.org/schema/security]
    Offending resource: class path resource [security/smallNotes-security.xml]

    at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:68) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:85) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:76) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.importBeanDefinitionResource(DefaultBeanDefinitionDocumentReader.java:245) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseDefaultElement(DefaultBeanDefinitionDocumentReader.java:196) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:181) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:140) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:111) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.registerBeanDefinitions(XmlBeanDefinitionReader.java:493) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:390) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:334) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:302) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:174) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:209) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.importBeanDefinitionResource(DefaultBeanDefinitionDocumentReader.java:239) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    … 27 more
    Caused by: org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Unable to locate Spring NamespaceHandler for XML schema namespace [http://www.springframework.org/schema/security]
    Offending resource: class path resource [security/smallNotes-security.xml]

    at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:68) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:85) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:80) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.error(BeanDefinitionParserDelegate.java:316) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1416) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1409) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:184) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:140) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:111) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.registerBeanDefinitions(XmlBeanDefinitionReader.java:493) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:390) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:334) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:302) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:174) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:209) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.importBeanDefinitionResource(DefaultBeanDefinitionDocumentReader.java:239) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    … 38 more

    11:57:03,028 ERROR [org.apache.catalina.core.ContainerBase.[jboss.web].[default-host].[/smallNotes]] (MSC service thread 1-2) Excepción enviando evento inicializado de contexto a instancia de escuchador de clase org.springframework.web.context.ContextLoaderListener: org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Failed to import bean definitions from URL location [classpath:/smallNotes.xml]
    Offending resource: ServletContext resource [/WEB-INF/smallNotes-servlet.xml]; nested exception is org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Failed to import bean definitions from URL location [classpath:/security/smallNotes-security.xml]
    Offending resource: class path resource [smallNotes.xml]; nested exception is org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Unable to locate Spring NamespaceHandler for XML schema namespace [http://www.springframework.org/schema/security]
    Offending resource: class path resource [security/smallNotes-security.xml]

    at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:68) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:85) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:76) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.importBeanDefinitionResource(DefaultBeanDefinitionDocumentReader.java:245) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseDefaultElement(DefaultBeanDefinitionDocumentReader.java:196) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:181) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:140) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:111) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.registerBeanDefinitions(XmlBeanDefinitionReader.java:493) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:390) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:334) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:302) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:174) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:209) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:180) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.web.context.support.XmlWebApplicationContext.loadBeanDefinitions(XmlWebApplicationContext.java:125) [spring-web-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.web.context.support.XmlWebApplicationContext.loadBeanDefinitions(XmlWebApplicationContext.java:94) [spring-web-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.context.support.AbstractRefreshableApplicationContext.refreshBeanFactory(AbstractRefreshableApplicationContext.java:131) [spring-context-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.obtainFreshBeanFactory(AbstractApplicationContext.java:522) [spring-context-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:436) [spring-context-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:385) [spring-web-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:284) [spring-web-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:111) [spring-web-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.apache.catalina.core.StandardContext.contextListenerStart(StandardContext.java:3392) [jbossweb-7.0.13.Final.jar:]
    at org.apache.catalina.core.StandardContext.start(StandardContext.java:3850) [jbossweb-7.0.13.Final.jar:]
    at org.jboss.as.web.deployment.WebDeploymentService.start(WebDeploymentService.java:90) [jboss-as-web-7.1.1.Final.jar:7.1.1.Final]
    at org.jboss.msc.service.ServiceControllerImpl$StartTask.startService(ServiceControllerImpl.java:1811)
    at org.jboss.msc.service.ServiceControllerImpl$StartTask.run(ServiceControllerImpl.java:1746)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) [rt.jar:1.7.0_75]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) [rt.jar:1.7.0_75]
    at java.lang.Thread.run(Thread.java:745) [rt.jar:1.7.0_75]
    Caused by: org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Failed to import bean definitions from URL location [classpath:/security/smallNotes-security.xml]
    Offending resource: class path resource [smallNotes.xml]; nested exception is org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Unable to locate Spring NamespaceHandler for XML schema namespace [http://www.springframework.org/schema/security]
    Offending resource: class path resource [security/smallNotes-security.xml]

    at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:68) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:85) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:76) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.importBeanDefinitionResource(DefaultBeanDefinitionDocumentReader.java:245) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseDefaultElement(DefaultBeanDefinitionDocumentReader.java:196) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:181) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:140) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:111) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.registerBeanDefinitions(XmlBeanDefinitionReader.java:493) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:390) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:334) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:302) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:174) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:209) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.importBeanDefinitionResource(DefaultBeanDefinitionDocumentReader.java:239) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    … 27 more
    Caused by: org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Unable to locate Spring NamespaceHandler for XML schema namespace [http://www.springframework.org/schema/security]
    Offending resource: class path resource [security/smallNotes-security.xml]

    at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:68) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:85) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:80) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.error(BeanDefinitionParserDelegate.java:316) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1416) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1409) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:184) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:140) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:111) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.registerBeanDefinitions(XmlBeanDefinitionReader.java:493) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:390) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:334) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:302) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:174) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:209) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.importBeanDefinitionResource(DefaultBeanDefinitionDocumentReader.java:239) [spring-beans-3.1.1.RELEASE.jar:3.1.1.RELEASE]
    … 38 more

  11. Primero de todo buen trabajo.

    He ejecutado los dos proyectos con un par de modificaciones como el user de la base de datos.

    y al ejecutar el primer link despues de hacer login me falla

    http://localhost:8080/smallNotes/oauth/authorize?client_id=smallNotesExternalWebApp_clientID&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2FsmallNotesExternalWebApp%2Foauth%2Fapi%2Fnotes&response_type=code&scope=read+write&state=6FYww5

    la url del primer link salta esto con error 404, podrias echar un rallo de luz?

    gracias

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad