Cómo: Cambiar el proveedor criptográfico para la clave privada de un certificado X.509
En este tema se muestra cómo cambiar el proveedor criptográfico utilizado para proporcionar una clave privada de un certificado X.509 y cómo integrar el proveedor en el marco de seguridad Windows Communication Foundation (WCF). Para obtener más información sobre el uso de certificados, consulte Trabajar con certificados.
El marco de seguridad WCF proporciona una manera de introducir nuevos tipos de token de seguridad tal y como se describe en Cómo: Crear un token personalizado. También es posible utilizar un token personalizado para reemplazar los tipos existentes de token proporcionados por el sistema.
En este tema, un token X.509 personalizado que proporciona una implementación diferente para la clave privada del certificado reemplaza el token de seguridad X.509 proporcionado por el sistema. Esto es útil en escenarios donde un proveedor criptográfico diferente al proveedor criptográfico predeterminado de Windows proporciona la clave privada real. Un ejemplo de un proveedor criptográfico alternativo es un módulo de seguridad de hardware que lleva a cabo todas las operaciones criptográficas relacionadas con clave privada, y no almacena las claves privadas en memoria, de modo que mejora la seguridad del sistema.
El siguiente ejemplo solamente sirve de demostración. No reemplaza el proveedor criptográfico predeterminado de Windows, pero muestra donde se pudo integrar este tipo de proveedor.
Procedimientos
Cada token de seguridad que tiene una clave o claves de seguridad asociadas debe implementar la propiedad SecurityKeys, que devuelve una colección de claves desde la instancia del token de seguridad. Si el token es un token de seguridad X.509, la colección contiene una instancia única de la clase X509AsymmetricSecurityKey que representa claves públicas y claves privadas asociadas al certificado. Para reemplazar el proveedor criptográfico predeterminado utilizado para proporcionar las claves del certificado, cree una nueva implementación de esta clase.
Crear una clave asimétrica X.509 personalizada
Defina una clase nueva derivada de la clase X509AsymmetricSecurityKey.
Reemplace la propiedad de solo lectura KeySize. Esta propiedad devuelve el tamaño de clave real del par de clave pública/privada del certificado.
Reemplace el método DecryptKey. El marco de seguridad WCF llama a este método para descifrar una clave simétrica con la clave privada del certificado. (La clave se cifró previamente con la clave pública del certificado.)
Reemplace el método GetAsymmetricAlgorithm. El marco de seguridad WCF llama a este método para obtener una instancia de la clase AsymmetricAlgorithm que representa el proveedor criptográfico para la clave privada o pública del certificado, según los parámetros pasados al método.
Opcional. Reemplace el método GetHashAlgorithmForSignature. Invalide este método si se requiere una implementación diferente de la clase HashAlgorithm.
Invalide el método GetSignatureFormatter. Este método devuelve una instancia de la clase AsymmetricSignatureFormatter que está asociada a la clave privada del certificado.
Invalide el método IsSupportedAlgorithm. Este método se utiliza para indicar si la implementación de clave de seguridad admite un algoritmo criptográfico determinado.
Friend Class CustomX509AsymmetricSecurityKey Inherits X509AsymmetricSecurityKey Private _certificate As X509Certificate2 Private _thisLock As New Object() Private _privateKeyAvailabilityDetermined As Boolean Private _privateKey As AsymmetricAlgorithm Private _publicKey As PublicKey Public Sub New(ByVal certificate As X509Certificate2) MyBase.New(certificate) Me._certificate = certificate End Sub Public Overrides ReadOnly Property KeySize() As Integer Get Return Me.PublicKey.Key.KeySize End Get End Property Private Overloads ReadOnly Property PrivateKey() As AsymmetricAlgorithm ' You need to modify this to obtain the private key using a different cryptographic ' provider if you do not want to use the default provider. Get If Not Me._privateKeyAvailabilityDetermined Then SyncLock ThisLock If Not Me._privateKeyAvailabilityDetermined Then Me._privateKey = Me._certificate.PrivateKey Me._privateKeyAvailabilityDetermined = True End If End SyncLock End If Return Me._privateKey End Get End Property Private Overloads ReadOnly Property PublicKey() As PublicKey Get If Me._publicKey Is Nothing Then SyncLock ThisLock If Me._publicKey Is Nothing Then Me._publicKey = Me._certificate.PublicKey End If End SyncLock End If Return Me._publicKey End Get End Property Private Overloads ReadOnly Property ThisLock() As Object Get Return _thisLock End Get End Property Public Overrides Function DecryptKey(ByVal algorithm As String, _ ByVal keyData() As Byte) As Byte() ' You can decrypt the key only if you have the private key in the certificate. If Me.PrivateKey Is Nothing Then Throw New NotSupportedException("Missing private key") End If Dim rsa = TryCast(Me.PrivateKey, RSA) If rsa Is Nothing Then Throw New NotSupportedException("Private key cannot be used with RSA algorithm") End If ' Support exchange keySpec, AT_EXCHANGE ? If rsa.KeyExchangeAlgorithm Is Nothing Then Throw New NotSupportedException("Private key does not support key exchange") End If Select Case algorithm Case EncryptedXml.XmlEncRSA15Url Return EncryptedXml.DecryptKey(keyData, rsa, False) Case EncryptedXml.XmlEncRSAOAEPUrl Return EncryptedXml.DecryptKey(keyData, rsa, True) Case Else Throw New NotSupportedException(String.Format("Algorithm {0} is not supported", algorithm)) End Select End Function Public Overrides Function GetAsymmetricAlgorithm(ByVal algorithm As String, _ ByVal privateKey As Boolean) As AsymmetricAlgorithm If privateKey Then If Me.PrivateKey Is Nothing Then Throw New NotSupportedException("Missing private key") End If Select Case algorithm Case SignedXml.XmlDsigDSAUrl If TryCast(Me.PrivateKey, DSA) IsNot Nothing Then Return (TryCast(Me.PrivateKey, DSA)) End If Throw New NotSupportedException("Private key cannot be used with DSA") Case SignedXml.XmlDsigRSASHA1Url, EncryptedXml.XmlEncRSA15Url, EncryptedXml.XmlEncRSAOAEPUrl If TryCast(Me.PrivateKey, RSA) IsNot Nothing Then Return (TryCast(Me.PrivateKey, RSA)) End If Throw New NotSupportedException("Private key cannot be used with RSA") Case Else Throw New NotSupportedException(String.Format("Algorithm {0} is not supported", algorithm)) End Select Else Select Case algorithm Case SignedXml.XmlDsigDSAUrl If TryCast(Me.PublicKey.Key, DSA) IsNot Nothing Then Return (TryCast(Me.PublicKey.Key, DSA)) End If Throw New NotSupportedException("Public key cannot be used with DSA") Case SignedXml.XmlDsigRSASHA1Url, EncryptedXml.XmlEncRSA15Url, EncryptedXml.XmlEncRSAOAEPUrl If TryCast(Me.PublicKey.Key, RSA) IsNot Nothing Then Return (TryCast(Me.PublicKey.Key, RSA)) End If Throw New NotSupportedException("Public key cannot be used with RSA") Case Else Throw New NotSupportedException(String.Format("Algorithm {0} is not supported", algorithm)) End Select End If End Function Public Overrides Function GetHashAlgorithmForSignature(ByVal algorithm As String) As HashAlgorithm If Not Me.IsSupportedAlgorithm(algorithm) Then Throw New NotSupportedException(String.Format("Algorithm {0} is not supported", algorithm)) End If Select Case algorithm Case SignedXml.XmlDsigDSAUrl, SignedXml.XmlDsigRSASHA1Url Return New SHA1Managed() Case Else Throw New NotSupportedException(String.Format("Algorithm {0} is not supported", algorithm)) End Select End Function Public Overrides Function GetSignatureFormatter(ByVal algorithm As String) As AsymmetricSignatureFormatter ' The signature can be created only if the private key is present. If Me.PrivateKey Is Nothing Then Throw New NotSupportedException("Private key is missing") End If ' Only one of the two algorithms is supported, not both. ' XmlDsigDSAUrl = "http://www.w3.org/2000/09/xmldsig#dsa-sha1"; ' XmlDsigRSASHA1Url = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; Select Case algorithm Case SignedXml.XmlDsigDSAUrl ' Ensure that this is a DSA algorithm object. Dim dsa = (TryCast(Me.PrivateKey, DSA)) If dsa Is Nothing Then Throw New NotSupportedException("Private key cannot be used DSA") End If Return New DSASignatureFormatter(dsa) Case SignedXml.XmlDsigRSASHA1Url ' Ensure that this is an RSA algorithm object. Dim rsa = (TryCast(Me.PrivateKey, RSA)) If rsa Is Nothing Then Throw New NotSupportedException("Private key cannot be used RSA") End If Return New RSAPKCS1SignatureFormatter(rsa) Case Else Throw New NotSupportedException(String.Format("Algorithm {0} is not supported", algorithm)) End Select End Function Public Overrides Function IsSupportedAlgorithm(ByVal algorithm As String) As Boolean Select Case algorithm Case SignedXml.XmlDsigDSAUrl Return (TypeOf Me.PublicKey.Key Is DSA) Case SignedXml.XmlDsigRSASHA1Url, EncryptedXml.XmlEncRSA15Url, EncryptedXml.XmlEncRSAOAEPUrl Return (TypeOf Me.PublicKey.Key Is RSA) Case Else Return False End Select End Function End Class
class CustomX509AsymmetricSecurityKey : X509AsymmetricSecurityKey { X509Certificate2 certificate; object thisLock = new Object(); bool privateKeyAvailabilityDetermined; AsymmetricAlgorithm privateKey; PublicKey publicKey; public CustomX509AsymmetricSecurityKey(X509Certificate2 certificate) : base(certificate) { this.certificate = certificate; } public override int KeySize { get { return this.PublicKey.Key.KeySize; } } AsymmetricAlgorithm PrivateKey { // You need to modify this to obtain the private key using a different cryptographic // provider if you do not want to use the default provider. get { if (!this.privateKeyAvailabilityDetermined) { lock (ThisLock) { if (!this.privateKeyAvailabilityDetermined) { this.privateKey = this.certificate.PrivateKey; this.privateKeyAvailabilityDetermined = true; } } } return this.privateKey; } } PublicKey PublicKey { get { if (this.publicKey == null) { lock (ThisLock) { if (this.publicKey == null) { this.publicKey = this.certificate.PublicKey; } } } return this.publicKey; } } Object ThisLock { get { return thisLock; } } public override byte[] DecryptKey(string algorithm, byte[] keyData) { // You can decrypt the key only if you have the private key in the certificate. if (this.PrivateKey == null) { throw new NotSupportedException("Missing private key"); } RSA rsa = this.PrivateKey as RSA; if (rsa == null) { throw new NotSupportedException("Private key cannot be used with RSA algorithm"); } // Support exchange keySpec, AT_EXCHANGE ? if (rsa.KeyExchangeAlgorithm == null) { throw new NotSupportedException("Private key does not support key exchange"); } switch (algorithm) { case EncryptedXml.XmlEncRSA15Url: return EncryptedXml.DecryptKey(keyData, rsa, false); case EncryptedXml.XmlEncRSAOAEPUrl: return EncryptedXml.DecryptKey(keyData, rsa, true); default: throw new NotSupportedException(String.Format("Algorithm {0} is not supported", algorithm)); } } public override AsymmetricAlgorithm GetAsymmetricAlgorithm(string algorithm, bool privateKey) { if (privateKey) { if (this.PrivateKey == null) { throw new NotSupportedException("Missing private key"); } switch (algorithm) { case SignedXml.XmlDsigDSAUrl: if ((this.PrivateKey as DSA) != null) { return (this.PrivateKey as DSA); } throw new NotSupportedException("Private key cannot be used with DSA"); case SignedXml.XmlDsigRSASHA1Url: case EncryptedXml.XmlEncRSA15Url: case EncryptedXml.XmlEncRSAOAEPUrl: if ((this.PrivateKey as RSA) != null) { return (this.PrivateKey as RSA); } throw new NotSupportedException("Private key cannot be used with RSA"); default: throw new NotSupportedException(String.Format("Algorithm {0} is not supported", algorithm)); } } else { switch (algorithm) { case SignedXml.XmlDsigDSAUrl: if ((this.PublicKey.Key as DSA) != null) { return (this.PublicKey.Key as DSA); } throw new NotSupportedException("Public key cannot be used with DSA"); case SignedXml.XmlDsigRSASHA1Url: case EncryptedXml.XmlEncRSA15Url: case EncryptedXml.XmlEncRSAOAEPUrl: if ((this.PublicKey.Key as RSA) != null) { return (this.PublicKey.Key as RSA); } throw new NotSupportedException("Public key cannot be used with RSA"); default: throw new NotSupportedException(String.Format("Algorithm {0} is not supported", algorithm)); } } } public override HashAlgorithm GetHashAlgorithmForSignature(string algorithm) { if (!this.IsSupportedAlgorithm(algorithm)) { throw new NotSupportedException(String.Format("Algorithm {0} is not supported", algorithm)); } switch (algorithm) { case SignedXml.XmlDsigDSAUrl: case SignedXml.XmlDsigRSASHA1Url: return new SHA1Managed(); default: throw new NotSupportedException(String.Format("Algorithm {0} is not supported", algorithm)); } } public override AsymmetricSignatureFormatter GetSignatureFormatter(string algorithm) { // The signature can be created only if the private key is present. if (this.PrivateKey == null) { throw new NotSupportedException("Private key is missing"); } // Only one of the two algorithms is supported, not both. // XmlDsigDSAUrl = "http://www.w3.org/2000/09/xmldsig#dsa-sha1"; // XmlDsigRSASHA1Url = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; switch (algorithm) { case SignedXml.XmlDsigDSAUrl: // Ensure that this is a DSA algorithm object. DSA dsa = (this.PrivateKey as DSA); if (dsa == null) { throw new NotSupportedException("Private key cannot be used DSA"); } return new DSASignatureFormatter(dsa); case SignedXml.XmlDsigRSASHA1Url: // Ensure that this is an RSA algorithm object. RSA rsa = (this.PrivateKey as RSA); if (rsa == null) { throw new NotSupportedException("Private key cannot be used RSA"); } return new RSAPKCS1SignatureFormatter(rsa); default: throw new NotSupportedException(String.Format("Algorithm {0} is not supported", algorithm)); } } public override bool IsSupportedAlgorithm(string algorithm) { switch (algorithm) { case SignedXml.XmlDsigDSAUrl: return (this.PublicKey.Key is DSA); case SignedXml.XmlDsigRSASHA1Url: case EncryptedXml.XmlEncRSA15Url: case EncryptedXml.XmlEncRSAOAEPUrl: return (this.PublicKey.Key is RSA); default: return false; } } }
El procedimiento siguiente muestra cómo integrar la implementación de clave de seguridad asimétrica personalizada X.509, creada en el procedimiento anterior con el marco de seguridad WCF para reemplazar el token de seguridad X.509 proporcionado por el sistema.
Reemplazar el token de seguridad X.509 proporcionado por el sistema por un token clave de seguridad asimétrico personalizado X.509.
Cree un token de seguridad X.509 personalizado que devuelve la clave de seguridad asimétrica personalizada X.509 en lugar de la clave de seguridad proporcionada por el sistema, tal y como se muestra en el ejemplo siguiente. Para obtener más información sobre tokens de seguridad personalizados, consulte Cómo: Crear un token personalizado.
Friend Class CustomX509SecurityToken Inherits X509SecurityToken Private _securityKeys As ReadOnlyCollection(Of SecurityKey) Public Sub New(ByVal certificate As X509Certificate2) MyBase.New(certificate) End Sub Public Overrides ReadOnly Property SecurityKeys() As ReadOnlyCollection(Of SecurityKey) Get If Me._securityKeys Is Nothing Then Dim temp As New List(Of SecurityKey)(1) temp.Add(New CustomX509AsymmetricSecurityKey(Me.Certificate)) Me._securityKeys = temp.AsReadOnly() End If Return Me._securityKeys End Get End Property End Class
class CustomX509SecurityToken : X509SecurityToken { ReadOnlyCollection<SecurityKey> securityKeys; public CustomX509SecurityToken(X509Certificate2 certificate) : base(certificate) { } public override ReadOnlyCollection<SecurityKey> SecurityKeys { get { if (this.securityKeys == null) { List<SecurityKey> temp = new List<SecurityKey>(1); temp.Add(new CustomX509AsymmetricSecurityKey(this.Certificate)); this.securityKeys = temp.AsReadOnly(); } return this.securityKeys; } } }
Cree un proveedor de token de seguridad personalizado que devuelve un token de seguridad X.509 personalizado, tal y como se muestra en el ejemplo. Para obtener más información a cerca de proveedores de tokens de seguridad personalizados, consulte Cómo crear un proveedor de tokens de seguridad personalizado.
Friend Class CustomX509SecurityTokenProvider Inherits SecurityTokenProvider Private _certificate As X509Certificate2 Public Sub New(ByVal certificate As X509Certificate2) Me._certificate = certificate End Sub Protected Overrides Function GetTokenCore(ByVal timeout As TimeSpan) As SecurityToken Return New CustomX509SecurityToken(_certificate) End Function End Class
class CustomX509SecurityTokenProvider : SecurityTokenProvider { X509Certificate2 certificate; public CustomX509SecurityTokenProvider(X509Certificate2 certificate) { this.certificate = certificate; } protected override SecurityToken GetTokenCore(TimeSpan timeout) { return new CustomX509SecurityToken(certificate); } }
Si es necesario utilizar la clave de seguridad personalizada en el lado del iniciador, cree un administrador de tokens de seguridad de cliente personalizados y las clases de credenciales de cliente personalizadas, tal y como se muestra en el ejemplo siguiente. Para obtener más información a cerca de las credenciales del cliente personalizadas y administradores de tokens de seguridad del cliente, consulte Tutorial: Crear credenciales de cliente y servicio personalizadas.
Friend Class CustomClientSecurityTokenManager Inherits ClientCredentialsSecurityTokenManager Private credentials As CustomClientCredentials Public Sub New(ByVal credentials As CustomClientCredentials) MyBase.New(credentials) Me.credentials = credentials End Sub Public Overrides Function CreateSecurityTokenProvider(ByVal tokenRequirement As SecurityTokenRequirement) _ As SecurityTokenProvider Dim result As SecurityTokenProvider = Nothing If tokenRequirement.TokenType = SecurityTokenTypes.X509Certificate Then Dim direction = tokenRequirement.GetProperty(Of MessageDirection) _ (ServiceModelSecurityTokenRequirement.MessageDirectionProperty) If direction = MessageDirection.Output Then If tokenRequirement.KeyUsage = SecurityKeyUsage.Signature Then result = New CustomX509SecurityTokenProvider(credentials.ClientCertificate.Certificate) End If Else If tokenRequirement.KeyUsage = SecurityKeyUsage.Exchange Then result = New CustomX509SecurityTokenProvider(credentials.ClientCertificate.Certificate) End If End If End If If result Is Nothing Then result = MyBase.CreateSecurityTokenProvider(tokenRequirement) End If Return result End Function End Class
class CustomClientSecurityTokenManager : ClientCredentialsSecurityTokenManager { CustomClientCredentials credentials; public CustomClientSecurityTokenManager(CustomClientCredentials credentials) : base(credentials) { this.credentials = credentials; } public override SecurityTokenProvider CreateSecurityTokenProvider(SecurityTokenRequirement tokenRequirement) { SecurityTokenProvider result = null; if (tokenRequirement.TokenType == SecurityTokenTypes.X509Certificate) { MessageDirection direction = tokenRequirement.GetProperty<MessageDirection>(ServiceModelSecurityTokenRequirement.MessageDirectionProperty); if (direction == MessageDirection.Output) { if (tokenRequirement.KeyUsage == SecurityKeyUsage.Signature) { result = new CustomX509SecurityTokenProvider(credentials.ClientCertificate.Certificate); } } else { if (tokenRequirement.KeyUsage == SecurityKeyUsage.Exchange) { result = new CustomX509SecurityTokenProvider(credentials.ClientCertificate.Certificate); } } } if (result == null) { result = base.CreateSecurityTokenProvider(tokenRequirement); } return result; } }
Public Class CustomClientCredentials Inherits ClientCredentials Public Sub New() End Sub Protected Sub New(ByVal other As CustomClientCredentials) MyBase.New(other) End Sub Protected Overrides Function CloneCore() As ClientCredentials Return New CustomClientCredentials(Me) End Function Public Overrides Function CreateSecurityTokenManager() As SecurityTokenManager Return New CustomClientSecurityTokenManager(Me) End Function End Class
public class CustomClientCredentials : ClientCredentials { public CustomClientCredentials() { } protected CustomClientCredentials(CustomClientCredentials other) : base(other) { } protected override ClientCredentials CloneCore() { return new CustomClientCredentials(this); } public override SecurityTokenManager CreateSecurityTokenManager() { return new CustomClientSecurityTokenManager(this); } }
Si es necesario utilizar la clave de seguridad personalizada en el lado del receptor, cree un administrador de tokens de seguridad de servicio personalizados y las clases de credenciales de servicio personalizadas, tal y como se muestra en el ejemplo siguiente. Para obtener más información a cerca de las credenciales de servicio personalizadas y administradores de tokens de seguridad de servicio, consulte Tutorial: Crear credenciales de cliente y servicio personalizadas.
Friend Class CustomServiceSecurityTokenManager Inherits ServiceCredentialsSecurityTokenManager Private credentials As CustomServiceCredentials Public Sub New(ByVal credentials As CustomServiceCredentials) MyBase.New(credentials) Me.credentials = credentials End Sub Public Overrides Function CreateSecurityTokenProvider(ByVal tokenRequirement As SecurityTokenRequirement) As SecurityTokenProvider Dim result As SecurityTokenProvider = Nothing If tokenRequirement.TokenType = SecurityTokenTypes.X509Certificate Then Dim direction = tokenRequirement.GetProperty(Of MessageDirection) _ (ServiceModelSecurityTokenRequirement.MessageDirectionProperty) If direction = MessageDirection.Input Then If tokenRequirement.KeyUsage = SecurityKeyUsage.Exchange Then result = New CustomX509SecurityTokenProvider(credentials.ServiceCertificate.Certificate) End If Else If tokenRequirement.KeyUsage = SecurityKeyUsage.Signature Then result = New CustomX509SecurityTokenProvider(credentials.ServiceCertificate.Certificate) End If End If End If If result Is Nothing Then result = MyBase.CreateSecurityTokenProvider(tokenRequirement) End If Return result End Function End Class
class CustomServiceSecurityTokenManager : ServiceCredentialsSecurityTokenManager { CustomServiceCredentials credentials; public CustomServiceSecurityTokenManager(CustomServiceCredentials credentials) : base(credentials) { this.credentials = credentials; } public override SecurityTokenProvider CreateSecurityTokenProvider(SecurityTokenRequirement tokenRequirement) { SecurityTokenProvider result = null; if (tokenRequirement.TokenType == SecurityTokenTypes.X509Certificate) { MessageDirection direction = tokenRequirement.GetProperty<MessageDirection>(ServiceModelSecurityTokenRequirement.MessageDirectionProperty); if (direction == MessageDirection.Input) { if (tokenRequirement.KeyUsage == SecurityKeyUsage.Exchange) { result = new CustomX509SecurityTokenProvider(credentials.ServiceCertificate.Certificate); } } else { if (tokenRequirement.KeyUsage == SecurityKeyUsage.Signature) { result = new CustomX509SecurityTokenProvider(credentials.ServiceCertificate.Certificate); } } } if (result == null) { result = base.CreateSecurityTokenProvider(tokenRequirement); } return result; } }
Public Class CustomServiceCredentials Inherits ServiceCredentials Public Sub New() End Sub Protected Sub New(ByVal other As CustomServiceCredentials) MyBase.New(other) End Sub Protected Overrides Function CloneCore() As ServiceCredentials Return New CustomServiceCredentials(Me) End Function Public Overrides Function CreateSecurityTokenManager() As SecurityTokenManager Return New CustomServiceSecurityTokenManager(Me) End Function End Class
public class CustomServiceCredentials : ServiceCredentials { public CustomServiceCredentials() { } protected CustomServiceCredentials(CustomServiceCredentials other) : base(other) { } protected override ServiceCredentials CloneCore() { return new CustomServiceCredentials(this); } public override SecurityTokenManager CreateSecurityTokenManager() { return new CustomServiceSecurityTokenManager(this); } }
Vea también
Tareas
Cómo crear un proveedor de tokens de seguridad personalizado
Referencia
X509AsymmetricSecurityKey
AsymmetricSecurityKey
SecurityKey
AsymmetricAlgorithm
HashAlgorithm
AsymmetricSignatureFormatter
Conceptos
Tutorial: Crear credenciales de cliente y servicio personalizadas
Cómo: Crear un autenticador de tokens de seguridad personalizado
Cómo: Crear un token personalizado
Arquitectura de seguridad