...
Consideraciones de Seguridad
Si el socket se declara en una página que tiene permitido el acceso a usuarios que han iniciado sesión con un rol específico, es posible agregar la URL de la solicitud de protocolo push al conjunto de las URL restringidas.
La URL de solicitud de protocolo push se compone del prefijo URI /fundeweb.push/, seguido del nombre del canal. Por ejemplo, si la seguridad administrada por el contenedor definida en el web.xml, ya ha controlado el acceso a la página /user/foo.xhtml, de los usuarios que hayan iniciado sesión con el rol USER en el patrón de URL de ejemplo /user/* como se muestra a continuación:
| Bloque de código |
|---|
|
<security-constraint>
<web-resource-collection>
<web-resource-name>Restrict access to role USER.</web-resource-name>
<url-pattern>/user/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>USER</role-name>
</auth-constraint>
</security-constraint> |
.. y la página /user/foo.xhtml a su vez contiene un <fw:socket channel="foo">, entonces necesita agregar una restricción en el patrón de URL de solicitud de protocolo push de /fundeweb.push/foo como se muestra a continuación:
| Bloque de código |
|---|
|
<security-constraint>
<web-resource-collection>
<web-resource-name>Restrict access to role USER.</web-resource-name>
<url-pattern>/user/*</url-pattern>
<url-pattern>/omnifaces.push/foo</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>USER</role-name>
</auth-constraint>
</security-constraint> |
Como seguridad adicional, particularmente para aquellos canales públicos que no pueden ser restringidos por restricciones de seguridad, <fw:socket> registrará todos los canales declarados hasta el momento en la sesión HTTP, y cualquier solicitud de apertura de websocket entrante será verificada contra los canales registrados hasta el momento en la sesión HTTP. En caso de que el canal sea desconocido (por ejemplo, adivinado al azar o falsificado por los usuarios finales o reconectado manualmente después de que expire la sesión), el websocket se cerrará inmediatamente con el código de motivo de cierre 1008 (CloseReason.CloseCodes.VIOLATED_POLICY). Además, cuando se destruye la sesión HTTP, todos los canales de sesión y vista que aún estén abiertos se cerrarán explícitamente desde el lado del servidor con el código de motivo de cierre 1000 (CloseReason.CloseCodes.NORMAL_CLOSURE). Solo los sockets de ámbito de aplicación permanecerán abiertos y accesibles desde el lado del servidor, incluso cuando la sesión o vista asociada con la página en el lado del cliente ha expirado.
Sugerencias de Diseño con EJBs
En caso de que desee activar un push desde el backend EAR/EJB a una conexión push con ámbito de aplicación, puede utilizar eventos de JBoss Seam. Primero, cree una clase de bean personalizada que represente el evento push, como por ejemplo, PushEvent, y que contenga lo necesario para pasar como mensaje push.
| Bloque de código |
|---|
|
public final class PushEvent implemments org.jboss.seam.core.EventObject {
private final String message;
public PushEvent(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
} |
Después, utiliza org.jboss.seam.core.Events.raiseEvent(org.jboss.seam.core.EventObject), para lanzar el evento
| Bloque de código |
|---|
|
public void onSomeEntityChange(Entity entity) {
org.jboss.seam.core.Events.instance().raiseEvent((new PushEvent(entity.getSomeProperty()));
} |
Para terminar, usar la anotación @Observer en algún componente de ámbito EVENT, STATELESS o APPLICATION, y delegue en es.um.atica.faces.push.PushContext como se indica a continuación:
| Bloque de código |
|---|
|
@Push
private PushContext someChannel;
@Observer
public void onPushEvent(PushEvent event) {
someChannel.send(event.getMessage());
} |
Es importante advertir, que el componente con ámbito EVENT, no debe ser el mismo que el de la página de origen, por la sencilla razón de que no hay forma de realizar una solicitud HTTP en ese momento. Por esa misma razón, un componente con ámbito PAGE o SESSION, no funcionaría (ya que requieren respectivamente el estado de vista Faces y la sesión HTTP que solo se pueden identificar mediante una solicitud HTTP). Una conexión push con ámbito de view o session, tampoco funcionaría, por lo que la conexión push realmente debe tener ámbito application. El FacesContext tampoco estará disponible en el método que recibe el evento anterior.
En caso de que el disparador del evento del backend EAR/EJB sea un método de servicio asincrónico, que a su vez se inicia en el lado WAR, entonces puede hacer uso de callbacks del lado WAR. Deje que el método de servicio tome una instancia de callback como parámetro, como por ejemplo, la interfaz java.util.function.Consumer.
| Bloque de código |
|---|
|
@Asynchronous
public void someAsyncServiceMethod(Entity entity, Consumer<Object> callback) {
// ... (some long process)
callback.accept(entity.getSomeProperty());
} |
...
| Bloque de código |
|---|
|
@In(create = true)
private SomeService someService;
@Push
private PushContext someChannel;
public void someAction() {
someService.someAsyncServiceMethod(entity, message -> someChannel.send(message));
} |
Esta sería la única forma en caso de que desee enviar de forma asincrónica un mensaje a una conexión push con ámbito de view o session, y/o desee pasar algo desde FacesContext o de ámbito EVENT/PAGE/SESSION como argumento (con modificador final).
Sugerencias de Diseño en Cluster
En el caso que la aplicación este desplegada en un cluster con varios nodos, y el evento push se lanza desde un nodo diferente al que esta conectado el cliente (Navegador Web), entonces este no será notificado. Una solución es usar temas (topic) JMS, lanzar el evento push a través de JMS, y usar MDB (message driven bean) para reenviar el evento push a JBoss Seam en cada nodo.
Con JBoss Seam tenemos varios componentes para ayudar a trabajar con JMS:
Veamos un ejemplo puro con JMS:
| Bloque de código |
|---|
|
@Singleton
@TransactionAttribute(NOT_SUPPORTED)
public class PushManager {
@Resource(lookup = "java:/jms/topic/push")
private Topic jmsTopic;
@In
private JMSContext jmsContext;
public void fireEvent(PushEvent pushEvent) {
try {
Message jmsMessage = jmsContext.createMessage();
jmsMessage.setStringProperty("message", pushEvent.getMessage());
jmsContext.createProducer().send(jmsTopic, jmsMessage);
}
catch (Exception e) {
// Handle.
}
}
} |