net microsoft microservice guide framework asp asp.net wcf design authentication architecture

asp.net - microservice - microsoft architecture



Sitio web ASP.NET+Aplicación Windows Forms+Servicio WCF: Credenciales del cliente (2)

Digamos que estoy considerando diseñar un servicio WCF cuyo objetivo principal es proporcionar servicios amplios que puedan ser utilizados por tres aplicaciones dispares: un sitio web orientado al público, una aplicación interna de Windows Forms y un dispositivo móvil inalámbrico. El propósito del servicio es doble: (1) consolidar el código relacionado con los procesos comerciales en una ubicación central y (2) bloquear el acceso a la base de datos heredada, finalmente y de una vez por todas, escondiéndolo detrás de un conjunto de servicios.

Actualmente, cada una de las tres aplicaciones tiene su propia persistencia y capas de dominio con vistas ligeramente diferentes de la misma base de datos. En lugar de las tres aplicaciones que hablan con la base de datos, hablarían con el servicio WCF, habilitando nuevas características de algunos clientes (el selector móvil actualmente no puede activar procesos para enviar correos electrónicos, obviamente) y centralizando los sistemas de notificación (en lugar de un tarea programada de sondeo de la base de datos cada cinco minutos para pedidos nuevos; solo AcceptNewOrder() ping al sistema de paginación de AcceptNewOrder() cuando el método de servicio AcceptNewOrder() sea ​​invocado por uno de estos clientes). En general, esto suena bastante sensato hasta ahora.

En términos de diseño general, sin embargo, estoy perplejo cuando se trata de seguridad. La aplicación Windows Forms actualmente solo usa principios de Windows; los empleados se almacenan en Active Directory y, al inicio de la aplicación, pueden iniciar sesión como el usuario actual de Windows (en cuyo caso no se requiere contraseña) o pueden proporcionar su nombre de dominio y contraseña. El cliente móvil no tiene ningún concepto de usuario; su conexión a la base de datos es una cadena codificada. Y el sitio web tiene miles de usuarios almacenados en la base de datos heredada. Entonces, ¿cómo implemento el modelo de identidad y configuro los puntos finales WCF para lidiar con esto?

En términos de la aplicación Windows Forms, este no es un gran problema: el proxy WCF se puede iniciar una vez y puede permanecer en la memoria, así que solo necesito las credenciales del cliente una vez (y puedo solicitarlas nuevamente si el proxy falla alguna vez). El cliente móvil solo puede tener una carcasa especial y usar un certificado X509 para la autenticación contra el servicio WCF. Pero, ¿qué hago con el sitio web?

En el caso del sitio web, se permite el acceso anónimo a algunos servicios. Y para los servicios que requieren autenticación en el papel hipotético de "Cliente", obviamente no quiero tener que autenticarlos en todas y cada una de las solicitudes por dos motivos:

  • Necesito su nombre de usuario y contraseña cada vez. Almacenar este par de información prácticamente en cualquier parte (la sesión, una cookie encriptada, la luna) parece una mala idea.
  • Tendría que golpear la tabla de usuarios en la base de datos para cada solicitud. Ay.

La única solución que se me ocurre es tratar el sitio web como un subsistema de confianza. El servicio WCF espera un certificado X509 particular del sitio web. El sitio web, utilizando la Autenticación de formularios internamente (que invoca un método AuthenticateCustomer() en el servicio que devuelve un resultado booleano), puede agregar un reclamo adicional a la lista de credenciales, algo como "[email protected] está conectado como un cliente." Entonces, de alguna manera, se podría construir un objeto personalizado de Identity e IPrincipal en el servicio con ese reclamo, el servicio WCF confiando en que el sitio web haya autenticado correctamente al cliente (sabrá que el reclamo no se ha manipulado, al menos, porque conocerá el certificado del sitio web con anticipación).

Con todo esto en su lugar, el código de servicio WCF podría decir cosas como [PrincipalPermission.Demand(Role=MyRoles.Customer)] o [PrincipalPermission.Demand(Role=MyRoles.Manager)] , y el Thread.CurrentPrincipal haría tener algo que representa un usuario (una dirección de correo electrónico para un cliente o un nombre distinguido para un empleado, ambos útiles para el registro y la auditoría).

En otras palabras, existirían dos puntos finales diferentes para cada servicio: uno que aceptara certificados de clientes conocidos X509 (para los dispositivos móviles y el sitio web) y uno que aceptara usuarios de Windows (para los empleados).

Lo siento, esto es demasiado largo. Entonces la pregunta es: ¿Tiene algo de esto sentido? ¿Tiene sentido la solución propuesta? ¿Y estoy haciendo esto demasiado complicado?


Bueno, creo que voy a echar una ojeada a mi propia pregunta ahora que he pasado unas horas jugando con varios enfoques.

Mi primer enfoque fue configurar la autenticación basada en certificados entre el servicio WCF y el sitio web público (el sitio web es un consumidor / cliente del servicio). Algunos makecert prueba generados con makecert , los makecert en las Trusted Root Certification Authorities Personal , Trusted People y de Trusted Root Certification Authorities (porque no me molestaba en generar los reales contra los servicios de certificados de nuestro dominio), algunas modificaciones de archivos de configuración, y genial, Ya está todo listo.

Para evitar que el sitio web tenga que mantener la información de usuario y contraseña para los usuarios, la idea es que una vez que un usuario inicia sesión en el sitio web a través de la Autenticación de formularios, el sitio web puede pasar el nombre de usuario (accesible a través de HttpContext.Current.User.Identity.Name ) como un UserNameSecurityToken opcional, además del X509CertificateSecurityToken que se usa realmente para proteger el mensaje. Si se encuentra el token de seguridad de nombre de usuario opcional, el servicio WCF diría "Oye, este subsistema de confianza dice que este usuario está autenticado correctamente, así que déjame configurar un MyCustomPrincipal para ese usuario e instalarlo en el hilo actual para que el servicio real el código puede inspeccionar esto ". Si no fuera así, se instalaría una versión anónima de MyCustomPrincipal .

Así que estuve durante cinco horas tratando de implementar esto, y con la ayuda de varios blogs , pude hacerlo. (Pasé la mayor parte del tiempo depurando un problema donde tenía todas las configuraciones y clases de soporte correctas, y luego instalé mis autorizaciones personalizadas después de iniciar el host, no antes, por lo que ninguno de mis esfuerzos estaba realmente en vigencia. Algunos días lo odio computadoras.) Tuve una TrustedSubsystemAuthorizationPolicy que hizo la validación del certificado X509, instalando MyCustomPrincipal anónimo, una TrustedSubsystemImpersonationAuthorizationPolicy que aceptó un token de nombre de usuario con una contraseña en blanco e instaló una función del cliente MyCustomPrincipal si vio que el principal del subsistema de confianza anónimo ya estaba instalado, y una UserNameAuthorizationPolicy que hizo una validación regular basada en nombre de usuario y contraseña para los otros puntos finales donde los certificados X509 no están siendo utilizados. Funcionó, y fue maravilloso.

Pero.

El momento de apuñalarme en el ojo vino cuando estaba jugando con el código proxy del cliente generado que el sitio web usaría para hablar con este servicio. Especificar el nombre de ClientCredentials en la propiedad ClientCredentials del objeto ClientBase<T> generado fue bastante fácil. Pero el principal problema es que las credenciales son específicas de ChannelFactory , no una invocación de método en particular.

Verá, el nuevo () up de un proxy de cliente WCF es más caro de lo que podría pensar . Escribí una aplicación rápida y sucia para probar el rendimiento yo mismo: tanto el nuevo () nuevo proxy como el llamar a un método diez veces me llevó unos 6 segundos mientras que el nuevo () creaba un proxy y llamaba solo al método 10 veces el costo aproximadamente 3 / 5ths de un segundo. Esa es solo una diferencia de rendimiento deprimente.

Entonces puedo implementar un pool o un caché para el proxy del cliente, ¿verdad? Bueno, no, no se soluciona fácilmente: la información de las credenciales del cliente se encuentra en el nivel de fábrica del canal porque podría usarse para asegurar el transporte, no solo el mensaje, y algunas vinculaciones mantienen un transporte real abierto entre las llamadas de servicio. Como las credenciales del cliente son únicas para el objeto proxy, esto significa que tendría que tener una instancia en caché exclusiva para cada usuario actualmente en el sitio web. ¡Es posible que muchos objetos proxy se encuentren en la memoria, y la bonita @ # $ @ # cerca del problema que estaba tratando de evitar en primer lugar! Y dado que tengo que tocar la propiedad de Endpoint todos modos para configurar el enlace para el token de nombre de usuario opcional de apoyo, no puedo aprovechar el caché de fábrica de canal automático que Microsoft agregó "de forma gratuita" en .NET 3.5.

Volviendo a la mesa de dibujo: mi segundo enfoque, y el que creo que terminaré usando por ahora, es seguir con la seguridad del certificado X509 entre el sitio web del cliente y el servicio WCF. Enviaré un encabezado SOAP personalizado de "Nombre de usuario" en mis mensajes, y el servicio WCF puede inspeccionar ese encabezado SOAP, determinar si proviene de un subsistema de confianza como el sitio web y, de ser así, instalar un MyCustomPrincipal de manera similar. manera como antes.

CodeProject y personas aleatorias en Google son cosas maravillosas porque me ayudaron a poner esto en funcionamiento rápidamente, incluso después de toparse con un extraño error de WCF cuando se trata de comportamientos de punto final personalizados en la configuración . Al implementar inspectores de mensajes en el lado del cliente y en el lado del servicio, uno para agregar el encabezado UserName, y otro para leerlo e instalar el principal correcto, este código está en un lugar donde simplemente puedo olvidarlo. Como no tengo que tocar la propiedad Endpoint , obtengo el almacenamiento en caché de la fábrica del canal incorporado de forma gratuita. Y dado que las ClientCredentials son las mismas para cualquier usuario que acceda al sitio web (de hecho, siempre son el certificado X509, solo cambia el valor del encabezado UserName dentro del mismo mensaje), agregar caché proxy de cliente o un grupo proxy es mucho más trivial.

Así que eso es lo que terminé haciendo. El código de servicio real en el servicio WCF puede hacer cosas como

// Scenario 1: X509Cert + custom UserName header yields for a Web site customer ... Console.WriteLine("{0}", Thread.CurrentPrincipal.Identity.Name); // prints out, say, "[email protected]" Console.WriteLine("{0}", Thread.CurrentPrincipal.IsInRole(MyRoles.Customer)); // prints out "True" // Scenario 2: My custom UserNameSecurityToken authentication yields for an employee ... Console.WriteLine("{0}", Thread.CurrentPrincipal.Identity.Name); // prints out, say, CN=Nick,DC=example, DC=com Console.WriteLine("{0}", Thread.CurrentPrincipal.IsInRole(MyRoles.Employee)); // prints out "True" // Scenario 3: Web site doesn''t pass in a UserName header ... Console.WriteLine("{0}", Thread.CurrentPrincipal.Identity.Name); // prints out nothing Console.WriteLine("{0}", Thread.CurrentPrincipal.IsInRole(MyRoles.Guest)); // prints out "True" Console.WriteLine("{0}", Thread.CurrentPrincipal.IsInRole(MyRoles.Customer)); // prints out "False"

No importa cómo se haya autenticado a estas personas, o que algunas vivan en SQL Server o que algunas vivan en Active Directory: PrincipalPermission.Demand y logging para fines de auditoría ahora es muy fácil.

Espero que esto ayude a alguna pobre alma en el futuro.


Para el acceso público anonymouse use basichttpbinding y también tenga lo siguiente en el archivo web.config