Implementa la arquitectura multiusuario mediante espacios de nombres

La API de espacios de nombres te permite habilitar el multiusuario en la aplicación con facilidad, con solo seleccionar una string de espacio de nombres para cada usuario en web.xml mediante el paquete NamespaceManager.

Cómo configurar el espacio de nombres actual

Puedes obtener, configurar y validar espacios de nombres mediante NamespaceManager. El administrador de espacios de nombres te permite configurar un espacio de nombres actual de las API habilitadas para espacios de nombres. Debes establecer un espacio de nombres actual por adelantado mediante web.xml. El almacén de datos y Memcache lo usarán de forma automática.

La mayoría de los desarrolladores de App Engine usarán su dominio de Google Workspace (antes conocido como G Suite) como el espacio de nombres actual. Google Workspace te permite implementar la app en cualquier dominio que tengas, de modo que puedas usar con facilidad este mecanismo para configurar espacios de nombres distintos en dominios diferentes. Luego, puedes usar esos espacios de nombres independientes para segregar datos en todos los dominios. Para obtener más información, consulta Asigna dominios personalizados.

En la siguiente muestra de código, se indica cómo configurar el espacio de nombres actual con el dominio de Google Workspace que se usó para asignar la URL. En particular, esta string será la misma para todas las URL que se mapearon a través del mismo dominio de Google Workspace.

Puedes configurar espacios de nombres en Java mediante la interfaz de filtro del servlet antes de invocar métodos del servlet. En el siguiente ejemplo sencillo, se demuestra cómo usar el dominio de Google Workspace como el espacio de nombres actual:

// Filter to set the Google Apps domain as the namespace.
public class NamespaceFilter implements Filter {

  @Override
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
    // Make sure set() is only called if the current namespace is not already set.
    if (NamespaceManager.get() == null) {
      // If your app is hosted on appspot, this will be empty. Otherwise it will be the domain
      // the app is hosted on.
      NamespaceManager.set(NamespaceManager.getGoogleAppsNamespace());
    }
    chain.doFilter(req, res); // Pass request back down the filter chain
  }

El filtro del espacio de nombres debe configurarse en el archivo web.xml. Ten en cuenta que, si hay varias entradas de filtro, el primer espacio de nombres que se configure es el que se usará.

En la siguiente muestra de código, se indica cómo configurar el filtro de espacio de nombres en web.xml:

<!-- Configure the namespace filter. -->
<filter>
    <filter-name>NamespaceFilter</filter-name>
    <filter-class>com.example.appengine.NamespaceFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>NamespaceFilter</filter-name>
    <url-pattern>/sign</url-pattern>
</filter-mapping>

Para obtener más información general sobre el archivo web.xml y el mapeo de las rutas de URL a los servlets, consulta El descriptor de implementación: web.xml.

También puedes configurar un espacio de nombres nuevo para una operación temporal si restableces el espacio de nombres original una vez que se complete la operación mediante el patrón try/finally que se muestra a continuación:

// Set the namepace temporarily to "abc"
String oldNamespace = NamespaceManager.get();
NamespaceManager.set("abc");
try {
  //      ... perform operation using current namespace ...
} finally {
  NamespaceManager.set(oldNamespace);
}

Si no especificas un valor para namespace, el espacio de nombres se establece en una string vacía. La string namespace es arbitraria, pero también se limita a un máximo de 100 caracteres alfanuméricos, puntos, guiones bajos y guiones. De forma más explícita, las strings de espacio de nombres deben coincidir con la expresión regular [0-9A-Za-z._-]{0,100}.

Por regla general, todos los espacios de nombres que comienzan con “_” (guion bajo) se reservan para el uso del sistema. La regla de espacio de nombres del sistema no se aplica de manera forzosa, pero podrías encontrarte con consecuencias negativas no definidas si no la respetas.

Cómo evitar la filtración de datos

Uno de los riesgos asociados comúnmente con las aplicaciones multiusuario es el peligro de que los datos se filtren a través de los espacios de nombres. Las filtraciones de datos no deseadas pueden surgir de varias fuentes, incluidas las siguientes:

  • El uso de espacios de nombres con las API de App Engine que todavía no admiten los espacios de nombres. Por ejemplo, Blobstore no admite espacios de nombres. Si usas espacios de nombres con Blobstore, debes evitar el uso de consultas de Blobstore para solicitudes de usuario final o claves de Blobstore de fuentes no confiables
  • El uso de un medio de almacenamiento externo (en lugar de Memcache o el almacén de datos), a través de URL Fetch o de algún otro mecanismo, sin proporcionar un esquema de compartimentación para los espacios de nombres
  • La configuración de un espacio de nombres en función del dominio de correo electrónico del usuario. En la mayoría de los casos, no querrás que todas las direcciones de correo electrónico de un dominio tengas acceso a un espacio de nombres. El uso del dominio de correo electrónico también evita que tu aplicación use un espacio de nombres hasta que el usuario haya accedido.

Cómo implementar espacios de nombres

En las secciones siguientes se describe cómo implementar espacios de nombres con otras API y herramientas de App Engine.

Cómo crear espacios de nombres por usuario

Algunas aplicaciones necesitan crear espacios de nombres por usuario. Si deseas compartimentar los datos a nivel del usuario para los usuarios que accedieron, considera usar User.getUserId(), que muestra un ID permanente y único para el usuario. En el siguiente ejemplo de código, se muestra cómo usar las API de usuarios con este propósito:

if (com.google.appengine.api.NamespaceManager.get() == null) {
  // Assuming there is a logged in user.
  namespace = UserServiceFactory.getUserService().getCurrentUser().getUserId();
  NamespaceManager.set(namespace);
}

Normalmente, las aplicaciones que crean espacios de nombres por usuario también proporcionan páginas de destino específicas a usuarios diferentes. En estos casos, la aplicación necesita proporcionar un esquema de URL que dicte qué página de destino se le debe mostrar al usuario.

Cómo usar espacios de nombres con Datastore

De forma predeterminada, el almacén de datos usa la configuración del espacio de nombres actual en el administrador de espacios de nombres para solicitudes del almacén de datos. La API aplica este espacio de nombres actual a los objetos Key o Query cuando se crean. Por lo tanto, debes tener cuidado si una aplicación almacena objetos Key o Query en formularios serializados, ya que el espacio de nombres se conserva en esas serializaciones.

Si usas objetos deserializados Key y Query, asegúrate de que se comporten como se espera. La mayoría de las aplicaciones simples que usan un almacén de datos (put, query, get) sin usar otros mecanismos de almacenamiento funcionarán como se espera mediante la configuración del espacio de nombres actual antes de llamar a cualquier API de almacén de datos.

Los objetos Query y Key demuestran los siguientes comportamientos únicos con respecto a los espacios de nombres:

  • Los objetos Query y Key heredan el espacio de nombres actual cuando se construyen, salvo que establezcas un espacio de nombres explícito.
  • Cuando una aplicación crea un objeto Key nuevo a partir de un elemento principal, el Key nuevo hereda el espacio de nombres del elemento principal.
  • No hay ninguna API para Java que establezca de forma explícita el espacio de nombres de Key o Query.
En el siguiente ejemplo de código, se muestra el controlador de solicitudes SomeRequest para aumentar el recuento del espacio de nombres actual y el espacio de nombres con el nombre arbitrario -global- en una entidad de almacén de datos Counter.

public class UpdateCountsServlet extends HttpServlet {

  private static final int NUM_RETRIES = 10;

  @Entity
  public class CounterPojo {

    @Id
    public Long id;
    @Index
    public String name;
    public Long count;

    public CounterPojo() {
      this.count = 0L;
    }

    public CounterPojo(String name) {
      this.name = name;
      this.count = 0L;
    }

    public void increment() {
      count++;
    }
  }

  /**
   * Increment the count in a Counter datastore entity.
   **/
  public long updateCount(String countName) {

    CounterPojo cp = ofy().load().type(CounterPojo.class).filter("name", countName).first().now();
    if (cp == null) {
      cp = new CounterPojo(countName);
    }
    cp.increment();
    ofy().save().entity(cp).now();

    return cp.count;
  }

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws java.io.IOException {

    // Update the count for the current namespace.
    updateCount("request");

    // Update the count for the "-global-" namespace.
    String namespace = NamespaceManager.get();
    try {
      // "-global-" is namespace reserved by the application.
      NamespaceManager.set("-global-");
      updateCount("request");
    } finally {
      NamespaceManager.set(namespace);
    }
    resp.setContentType("text/plain");
    resp.getWriter().println("Counts are now updated.");
  }

Usa espacios de nombres con Memcache

De forma predeterminada, Memcache usa el espacio de nombres actual del administrador de espacios de nombres para solicitudes de Memcache. En la mayoría de los casos, no es necesario que configures explícitamente un espacio de nombres en Memcache, y hacerlo podría generar errores inesperados.

Sin embargo, existen algunas instancias únicas donde es apropiado configurar explícitamente un espacio de nombres en Memcache. Por ejemplo, tu aplicación podría tener datos comunes compartidos a través de todos los espacios de nombres (como una tabla que contiene códigos de país).

En el siguiente fragmento de código, se muestra cómo configurar el espacio de nombres en Memcache de manera explícita:

De forma predeterminada, la API de Memcache para Java consulta el administrador de espacio de nombres destinado al espacio de nombres actual del MemcacheService. También puedes indicar de forma explícita un espacio de nombres cuando creas el Memcache mediante getMemcacheService(Namespace). En la mayoría de las aplicaciones, no necesitas especificar de forma explícita un espacio de nombres.

El siguiente ejemplo de código demuestra cómo crear un Memcache que use el espacio de nombres actual en el administrador de espacios de nombres.

// Create a MemcacheService that uses the current namespace by
// calling NamespaceManager.get() for every access.
MemcacheService current = MemcacheServiceFactory.getMemcacheService();

// stores value in namespace "abc"
oldNamespace = NamespaceManager.get();
NamespaceManager.set("abc");
try {
  current.put("key", value); // stores value in namespace “abc”
} finally {
  NamespaceManager.set(oldNamespace);
}

Este ejemplo de código especifica explícitamente un espacio de nombres cuando se crea un servicio de Memcache:

// Create a MemcacheService that uses the namespace "abc".
MemcacheService explicit = MemcacheServiceFactory.getMemcacheService("abc");
explicit.put("key", value); // stores value in namespace "abc"

Cómo usar espacios de nombres con la lista de tareas en cola

De forma predeterminada, las listas de aplicaciones en cola usan el espacio de nombres actual tal como se configura en el administrador de espacios de nombres en el momento que se creó la tarea. En la mayoría de los casos, no es necesario que configures de forma explícita un espacio de nombres en la lista de tareas en cola, y hacerlo podría generar errores inesperados.

Los nombres de las tareas se comparten en todos los espacios de nombres. No puedes crear dos tareas con el mismo nombre, aunque usen espacios de nombres diferentes. Si deseas usar el mismo nombre de tarea para varios espacios de nombres, solo tienes que agregar los espacios de nombres al nombre de la tarea.

Cuando una tarea nueva llama al método add() de la lista de tareas en cola, esta lista copia el espacio de nombres actual (si corresponde) y el dominio de Google Workspace del administrador de espacios de nombres. Cuando se ejecuta la tarea, se restablecen el espacio de nombres actual y el de Google Workspace.

Si el espacio de nombres actual no se configura en la solicitud de origen (en otras palabras, si get() muestra null), la lista de tareas en cola establece el espacio de nombres como "" en las tareas ejecutadas.

Existen algunas instancias únicas en las que es apropiado configurar de forma explícita un espacio de nombres para una tarea que funcione en todos los espacios de nombres. Por ejemplo, puedes crear una tarea que acumule estadísticas de uso a través de todos los espacios de nombres. Podrás entonces configurar explícitamente el espacio de nombres de la tarea. En la siguiente muestra de código, se indica cómo configurar los espacios de nombres con la lista de tareas en cola de forma explícita.

Primero, crea un controlador de lista de tareas en cola que incremente el recuento en una entidad Counter de almacén de datos:

public class UpdateCountsServlet extends HttpServlet {

  private static final int NUM_RETRIES = 10;

  @Entity
  public class CounterPojo {

    @Id
    public Long id;
    @Index
    public String name;
    public Long count;

    public CounterPojo() {
      this.count = 0L;
    }

    public CounterPojo(String name) {
      this.name = name;
      this.count = 0L;
    }

    public void increment() {
      count++;
    }
  }

  /**
   * Increment the count in a Counter datastore entity.
   **/
  public long updateCount(String countName) {

    CounterPojo cp = ofy().load().type(CounterPojo.class).filter("name", countName).first().now();
    if (cp == null) {
      cp = new CounterPojo(countName);
    }
    cp.increment();
    ofy().save().entity(cp).now();

    return cp.count;
  }

y

// called from Task Queue
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
  String[] countName = req.getParameterValues("countName");
  if (countName.length != 1) {
    resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
    return;
  }
  updateCount(countName[0]);
}

Luego, crea tareas con un servlet:

public class SomeRequestServlet extends HttpServlet {

  // Handler for URL get requests.
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

    // Increment the count for the current namespace asynchronously.
    QueueFactory.getDefaultQueue()
        .add(TaskOptions.Builder.withUrl("/_ah/update_count").param("countName", "SomeRequest"));
    // Increment the global count and set the
    // namespace locally.  The namespace is
    // transferred to the invoked request and
    // executed asynchronously.
    String namespace = NamespaceManager.get();
    try {
      NamespaceManager.set("-global-");
      QueueFactory.getDefaultQueue()
          .add(TaskOptions.Builder.withUrl("/_ah/update_count").param("countName", "SomeRequest"));
    } finally {
      NamespaceManager.set(namespace);
    }
    resp.setContentType("text/plain");
    resp.getWriter().println("Counts are being updated.");
  }
}

Usa espacios de nombres con Blobstore

Blobstore no está segmentado por espacio de nombres. Para conservar un espacio de nombres en Blobstore, debes acceder a Blobstore a través de un medio de almacenamiento que tenga en cuenta el espacio de nombres (por el momento, solo Memcache, el almacén de datos y la lista de tareas en cola). Por ejemplo, si el objeto Key de un BLOB se almacena en una entidad de almacén de datos, puedes acceder a él con un objeto Key o Query del almacén de datos que reconozca el espacio de nombres.

Si una aplicación accede a Blobstore por medio de claves guardadas en un almacenamiento que tiene en cuenta el espacio de nombres, Blobstore no necesita segmentarse por espacio de nombres. Las aplicaciones deben evitar las filtraciones de BLOB entre espacios de nombres mediante las siguientes acciones:

  • No deben usar com.google.appengine.api.blobstore.BlobInfoFactory para solicitudes de usuario final. Puedes usar las consultas BlobInfo para solicitudes administrativas (como la creación de informes sobre todos los BLOB de la aplicación), pero usarlas para solicitudes de usuario final puede provocar filtraciones de datos, ya que no todos los registros de BlobInfo están compartimentados por espacio de nombres.
  • No deben usar claves de Blobstore de fuentes no confiables.

Cómo configurar espacios de nombres para consultas de Datastore

En la consola de Google Cloud, puedes configurar el espacio de nombres para las consultas de Datastore.

Si no quieres usar el espacio de nombres predeterminado, selecciona el que deseas usar del menú desplegable.

Usa espacios de nombres con el cargador masivo

El cargador masivo admite una marca --namespace=NAMESPACE que te permite especificar el espacio de nombres se usará. Cada espacio de nombres se maneja de manera independiente y, si deseas acceder a todos los espacios de nombres, deberás iterar a través de ellos.

Una instancia nueva de Index hereda el espacio de nombres de SearchService que se usó para crearla. Una vez que creaste una referencia a un índice, su espacio de nombres no se puede cambiar. Hay dos maneras de configurar el espacio de nombres de un SearchService antes de usarlo para crear un índice:

  • De forma predeterminada, un SearchService nuevo toma el espacio de nombres actual. Puedes configurar el espacio de nombres actual antes de crear el servicio:
// Set the current namespace to "aSpace"
NamespaceManager.set("aSpace");
// Create a SearchService with the namespace "aSpace"
SearchService searchService = SearchServiceFactory.getSearchService();
// Create an IndexSpec
IndexSpec indexSpec = IndexSpec.newBuilder().setName("myIndex").build();
// Create an Index with the namespace "aSpace"
Index index = searchService.getIndex(indexSpec);

Puedes especificar un espacio de nombres en SearchServiceConfig cuando creas un servicio:

// Create a SearchServiceConfig, specifying the namespace "anotherSpace"
SearchServiceConfig config =
    SearchServiceConfig.newBuilder().setNamespace("anotherSpace").build();
// Create a SearchService with the namespace "anotherSpace"
searchService = SearchServiceFactory.getSearchService(config);
// Create an IndexSpec
indexSpec = IndexSpec.newBuilder().setName("myindex").build();
// Create an Index with the namespace "anotherSpace"
index = searchService.getIndex(indexSpec);