Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 9 de julio de 2013
SignalRComo comentamos por aquí hace tiempo, SignalR viene acompañado de su propia versión del atributo [Authorize], que permite restringir el acceso a hubs o a métodos concretos dentro de éstos de forma similar a como lo hacemos en otras tecnologías como ASP.NET MVC o Web API.

Si estamos trabajando en el entorno web, es decir, cuando cliente y servidor son puras tecnologías web y están ejecutándose en el mismo contexto, el servidor determina si el cliente SignalR está autenticado utilizando los mismos mecanismos de siempre, basados en la existencia de la cookie de autorización de ASP.NET y en los datos contenidos en ésta.

De esta forma, si un cliente entra con su navegador a nuestra web y supera el procedimiento de autenticación, a partir de ese momento todas sus peticiones llevarán adjunta la cookie de autorización, y ésta misma es la que usará SignalR para conceder o denegar el acceso a los métodos de los Hubs cuando accedamos a ellos usando el cliente Javascript.

Sin embargo, a través del formulario de contacto de Variable not found, me llega una consulta muy interesante del amigo Juan F.: ¿cómo podemos usar ese mismo atributo Authorize para controlar el acceso desde aplicaciones no web, las que usan el cliente genérico .NET de SignalR?

0. Solución conceptual

Como hemos comentado, SignalR permite indicar mediante el atributo Authorize que un método de un Hub puede ser ejecutado sólo por usuarios autenticados, o incluso especificar cuáles de ellos o sus roles:
[Authorize(Users="jmaguilar")]
public Task PrivateMessage(string message)
{
    return Clients.All.Message(
        DateTime.Now.ToLongTimeString() + " -> " + message);
}
Desde el punto de vista del servidor, la autorización se resuelve observando el IPrincipal asociado a la conexión física abierta con el servidor, disponible en la propiedad Context.User de la clase Hub. A su vez, éste IPrincipal es rellenado de forma automática por la plataforma a la vista de la información contenida en la cookie de autenticación, que normalmente viaja en las peticiones con el nombre .ASPXAUTH.

Está claro que la solución a la pregunta del amigo Juan pasa por obtener una de estas cookies que autorizan al usuario desde nuestra aplicación cliente .NET, y adjuntarla a la conexión que vamos a realizar al Hub. Se trata, por tanto, de un proceso previo a la apertura de la conexión con el Hub, más o menos con los siguientes pasos:
  1. Implementamos en el mismo servidor donde se encuentran los Hubs un método/acción/lo que sea capaz de autenticar al usuario y generar la cookie de autorización de ASP.NET.
     
  2. Creamos una conexión HTTP desde el cliente invocando a dicho método, al que suministramos las credenciales del usuario.
     
  3. Obtenemos la respuesta de dicha petición, y de ella extraemos el valor de la cookie .ASPXAUTH.
     
  4. Antes de iniciar la conexión con el Hub, adjuntamos la cookie a la petición para que el servidor nos reconozca.
Y esto sería todo. A partir de ese momento, el servidor ya tendría información suficiente como para permitir o denegar nuestra entrada al método o hub protegido por el atributo Authorize.

¿Algo lioso? Ya verás como no ;-)

1. Implementación de la autenticación (servidor)

Dado que la implementación depende de la tecnología (MVC, Webforms, Web API, WebPages…), vamos a simplificarla al máximo. Simplemente crearemos un archivo llamado Login.aspx, y en su code-behind introduciremos el siguiente código:
protected void Page_Load(object sender, EventArgs e)
{
    Response.Clear();
    var username = Request["username"];
    var password = Request["password"];

    // In real code, we could check if this user exists 
    // in the database.

    // For sake of brevity, we'll assume that the credentials 
    // are valid

    FormsAuthentication.SetAuthCookie(username, false);
    Response.End();
}
Obviamente, podríamos haber escrito el mismo código para MVC, en el interior de una acción:
public ActionResult Login(string userName, string password)
{
    ...
    // If the credentials are valid:
    FormsAuthentication.SetAuthCookie(userName, false);

    return new EmptyResult();
}
En cualquiera de los dos casos, el resultado es el mismo: la petición se responderá con un contenido nulo, pero con una bonita cookie que contiene información del usuario autenticado en el sitio web :-)

2. Obtención de la cookie de autenticación (cliente)

Desde la aplicación .NET, a continuación vamos a implementar un método que realice una petición hacia la página .aspx (o la acción MVC, da igual) para validar las credenciales y obtener la cookie si se supera la autenticación. Esto podemos conseguirlo de muchas formas, la plataforma ofrece varias clases para realizar peticiones HTTP, pero WebClient parece una buena candidata para usar al facilitarnos el acceso directo a las cookies generadas por el servidor:
private static Cookie GetAuthCookie(string loginUrl, string user, string pass)
{
    var url = loginUrl + "?username=" + user + "&password=" + pass;
    var request = WebRequest.Create(url) as HttpWebRequest;
    request.CookieContainer = new CookieContainer();
    var httpResponse = request.GetResponse() as HttpWebResponse;
    var cookie = httpResponse.Cookies[".ASPXAUTH"];
    httpResponse.Close();
    return cookie;
}
Este código podríamos mejorarlo bastante, por ejemplo, usando el verbo POST para enviar las credenciales. Sin embargo, he preferido dejarlo así para no hacerlo más extenso y que no nos distraiga de nuestro objetivo real.

3. Adjuntar la cookie al establecer la conexión con Signalr (cliente)

Ahora llega ya el momento de utilizar el método anterior en el proceso de establecimiento de la conexión del cliente SignalR. Tan sencillo como lo que vemos a continuación:
const string host = "http://myserver.com:1234";
var connection = new HubConnection(host);

Console.Write("Enter your username: ");
var username = Console.ReadLine();
Console.Write("Enter your password: ");
var password = Console.ReadLine();

// Get the cookie
var cookie = GetAuthCookie(host+"/login.aspx", username, password);

// Attach the cookie to the connection
connection.CookieContainer = new CookieContainer();
connection.CookieContainer.Add(cookie);

var proxy = connection.CreateHubProxy("EchoHub");

// ... other initialization code

// And, finally, start the connection
connection.Start();
Como podemos observar, el código es absolutamente trivial. El objeto HubConnection de SignalR dispone de una propiedad llamada CookieContainer donde podemos establecer las cookies que deseamos viajen al servidor durante el establecimiento de la conexión. Introducimos ahí la cookie de autorización obtenida previamente, y ¡listo!

A partir de este momento, ya podremos acceder a todos los métodos o Hubs protegidos mediante el atributo [Authorize], e incluso acceder a ellos si somos los usuarios específicamente autorizados.

Si os interesa verlo en acción, podéis descargar un proyecto de demostración desde mi Skydrive.

Publicado en Variable not found.

7 Comentarios:

Camilo Cabrales dijo...

Buenos días, excelente blog. Leí el libro pero no me quedo claro esto

estoy creando una aplicación en SIGNALR que tiene que leer de una base de datos, y enviar mensajes a los clientes (WEB), mi pregunta es: se puede crear un servicio windows que lea la base de datos y envié los mensajes a los clientes web (navegador)

josé M. Aguilar dijo...

Hola, Camilo!

Ante todo, gracias por comentar.

Sí, un servicio Windows podría hacer eso que dices y enviarle los mensajes a los clientes web. Pero el planteamiento sería:

1) tienes un hub al que están conectados los clientes.
2) tu servicio Windows se conectaría también al hub, como un cliente más, y le enviaría la información.
3) el hub, al recibir la información del servicio, haría un broadcast al resto de usuarios.

Saludos!

Camilo Cabrales dijo...

Hola Jose,
Como lo tengo actualmente es, un Hub que esta dentro del proyecto web que es al que se conectan los clientes.
No se si el servicio Windows se pueda conectar a ese mismo Hub para distribuir los mensajes a todos los clientes. Pero pues no he encontrado como hacerlo o no se si esta bien planteado así.
También he mirado un ejemplo de brodcast pero tengo la duda que si todos los clientes entran al método que distribuye los mensajes o es el primero (esto lo hacen con singleton). No se que me puedes aconsejar o cual sera la mejor forma de hacerlo ya que como lo hice fue un webmethod que actualiza los mensaje de cada cliente y no creo que sea la mejor solucion.
Gracias por tu tiempo

josé M. Aguilar dijo...

Hola!

En realidad puedes hacerlo de todas las formas :)

Si quieres que tu servicio se conecte directamente a Signalr, simplemente tienes que usar las bibliotecas cliente. si has leído el libro, ya al final se muestra cómo puedes conectarte a un servicio SignalR desde cualquier tipo de aplicación, incluidas apps de consola, o, por supuesto, servicios. Una vez conectado, llamarías a una acción del Hub, que es la que realizaría el broadcast.

Otra posibilidad es la que comentas; desde tu servicio Windows haces una llamada a un servicio web que se ejecuta dentro de la misma aplicación donde se encuentra signalr, y en la implementación sólo tendrías que acceder al contexto del hub (en el libro viene como "usar signalr desde otros procesos") y hacer el envío.

En cualquier caso, el mensaje lo recibirán todos los clientes conectados al hub.

Saludos.

Camilo Cabrales dijo...

Gracias de nuevo,
Lo que no se, es como obtener el Hub desde el servicio windows
(GlobalHost.ConnectionManager.GetHubContext ) como lo asocio al Hub que tengo en el proyecto Web, tengo que crear un Hub igual en el proyecto Windows?, o tengo que asociarle la Url donde esta publicado?

Gracias

josé M. Aguilar dijo...

Claro, el servicio Windows es un proceso independiente al hub, por lo que tienes que acceder a él mediante la URL a través de la cual se publican los servicios signalr.

Te recomiendo que veas los ejemplos del libro o que incluso los descargues para que veas cómo se realiza esta conexión.

GlobalHost.ConnectionManager.GetHubContext es lo que tendrías que usar si quisieras acceder al hub desde el servicio web, que era la otra alternativa que comentábamos.

Saludos.

Camilo Cabrales dijo...

Gracias por la ayuda voy a revisar