如何在ASP.NET Core 2.0中复用ASP.NET Framework的machineKey解析认证令牌
Great question! When migrating from .NET Framework's OAuth Bearer authentication (which relies on machineKey) to ASP.NET Core 2.0+, you need to replicate the exact token decryption logic since Core doesn't have a built-in machineKey equivalent. Here's a step-by-step solution that maintains compatibility with your existing tokens:
1. Grab Required NuGet Packages
First, you'll need a couple of packages to bridge the gap between Core and the old machineKey encryption logic:
Install-Package Microsoft.AspNetCore.Authentication.OAuth Install-Package System.Web
The System.Web package lets us reuse the existing MachineKey class for decryption, so we don't have to rewrite AES/SHA1 encryption from scratch.
2. Build a MachineKey-Compatible Ticket Format
Create a custom MachineKeyTicketDataFormat that implements ISecureDataFormat<AuthenticationTicket>. This class will handle decrypting tokens using your legacy machineKey parameters:
using Microsoft.AspNetCore.Authentication; using System.Security.Claims; using System.Web.Security; public class MachineKeyTicketDataFormat : ISecureDataFormat<AuthenticationTicket> { private readonly string _decryptionKey; private readonly string _validationKey; private readonly string _decryptionAlgorithm; private readonly string _validationAlgorithm; public MachineKeyTicketDataFormat(string decryptionKey, string validationKey, string decryptionAlgorithm = "AES", string validationAlgorithm = "SHA1") { _decryptionKey = decryptionKey; _validationKey = validationKey; _decryptionAlgorithm = decryptionAlgorithm; _validationAlgorithm = validationAlgorithm; } // We don't need to generate tokens in Core (old apps handle that), so leave these empty public string Protect(AuthenticationTicket data) => throw new NotImplementedException("Only token decryption is supported"); public string Protect(AuthenticationTicket data, string purpose) => Protect(data); public AuthenticationTicket Unprotect(string protectedText) => Unprotect(protectedText, null); public AuthenticationTicket Unprotect(string protectedText, string purpose) { // Decrypt the token using the exact machineKey logic from .NET Framework var ticketBytes = MachineKey.Unprotect( Convert.FromBase64String(protectedText), "Microsoft.Owin.Security.OAuth", // This matches the purpose used by OWIN's OAuth middleware purpose ?? "Access_Token"); // Deserialize the decrypted bytes back into an AuthenticationTicket var serializer = new TicketSerializer(); return serializer.Deserialize(ticketBytes); } } // Replicate the TicketSerializer used by .NET Framework's OWIN OAuth middleware // This is critical for correctly reading the token's claim data public class TicketSerializer : IDataSerializer<AuthenticationTicket> { public byte[] Serialize(AuthenticationTicket model) { using var memoryStream = new MemoryStream(); using var binaryWriter = new BinaryWriter(memoryStream); WriteWithLength(binaryWriter, model.Identity.AuthenticationType); WriteClaims(binaryWriter, model.Identity.Claims); WriteProperties(binaryWriter, model.Properties); binaryWriter.Flush(); return memoryStream.ToArray(); } public AuthenticationTicket Deserialize(byte[] data) { using var memoryStream = new MemoryStream(data); using var binaryReader = new BinaryReader(memoryStream); var authType = ReadWithLength(binaryReader); var claims = ReadClaims(binaryReader); var properties = ReadProperties(binaryReader); var identity = new ClaimsIdentity(claims, authType); return new AuthenticationTicket(identity, properties); } private void WriteWithLength(BinaryWriter writer, string value) { if (string.IsNullOrEmpty(value)) { writer.Write(-1); return; } var bytes = Encoding.UTF8.GetBytes(value); writer.Write(bytes.Length); writer.Write(bytes); } private string ReadWithLength(BinaryReader reader) { var length = reader.ReadInt32(); return length == -1 ? null : Encoding.UTF8.GetString(reader.ReadBytes(length)); } private void WriteClaims(BinaryWriter writer, IEnumerable<Claim> claims) { var claimList = claims.ToList(); writer.Write(claimList.Count); foreach (var claim in claimList) { WriteWithLength(writer, claim.Type); WriteWithLength(writer, claim.Value); WriteWithLength(writer, claim.ValueType); WriteWithLength(writer, claim.Issuer); WriteWithLength(writer, claim.OriginalIssuer); } } private List<Claim> ReadClaims(BinaryReader reader) { var count = reader.ReadInt32(); var claims = new List<Claim>(count); for (var i = 0; i < count; i++) { claims.Add(new Claim( ReadWithLength(reader), ReadWithLength(reader), ReadWithLength(reader), ReadWithLength(reader), ReadWithLength(reader))); } return claims; } private void WriteProperties(BinaryWriter writer, AuthenticationProperties properties) { var dict = properties?.Items ?? new Dictionary<string, string>(); writer.Write(dict.Count); foreach (var kvp in dict) { WriteWithLength(writer, kvp.Key); WriteWithLength(writer, kvp.Value); } } private AuthenticationProperties ReadProperties(BinaryReader reader) { var count = reader.ReadInt32(); var dict = new Dictionary<string, string>(count); for (var i = 0; i < count; i++) { dict.Add(ReadWithLength(reader), ReadWithLength(reader)); } return new AuthenticationProperties(dict); } }
3. Configure Authentication in Startup
In Startup.cs, register your custom authentication scheme and the machineKey-compatible ticket format:
using Microsoft.AspNetCore.Authentication; public void ConfigureServices(IServiceCollection services) { // Your other service registrations... // Replace these with your actual machineKey values from Web.config var legacyDecryptionKey = "blablabla"; var legacyValidationKey = "blablabla"; services.AddAuthentication(options => { options.DefaultAuthenticateScheme = "LegacyBearer"; options.DefaultChallengeScheme = "LegacyBearer"; }) .AddScheme<AuthenticationSchemeOptions, LegacyBearerAuthenticationHandler>("LegacyBearer", options => { // Add any scheme-specific options here if needed }); // Register our custom ticket format with the service container services.AddSingleton<ISecureDataFormat<AuthenticationTicket>>(sp => new MachineKeyTicketDataFormat(legacyDecryptionKey, legacyValidationKey)); }
4. Build the Custom Bearer Handler
Create a LegacyBearerAuthenticationHandler to extract the token from the request header and use our custom format to decrypt it:
using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Net.Http.Headers; using System.Text.Encodings.Web; public class LegacyBearerAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> { private readonly ISecureDataFormat<AuthenticationTicket> _ticketDataFormat; public LegacyBearerAuthenticationHandler( IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, ISecureDataFormat<AuthenticationTicket> ticketDataFormat) : base(options, logger, encoder, clock) { _ticketDataFormat = ticketDataFormat; } protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { // Check for the Authorization header if (!Request.Headers.ContainsKey("Authorization")) return AuthenticateResult.NoResult(); if (!AuthenticationHeaderValue.TryParse(Request.Headers["Authorization"], out var authHeader)) return AuthenticateResult.Fail("Invalid Authorization header format"); // Only process Bearer tokens if (!string.Equals(authHeader.Scheme, "Bearer", StringComparison.OrdinalIgnoreCase)) return AuthenticateResult.NoResult(); var token = authHeader.Parameter; if (string.IsNullOrEmpty(token)) return AuthenticateResult.Fail("Bearer token is missing"); try { // Decrypt the token into an AuthenticationTicket var ticket = _ticketDataFormat.Unprotect(token); if (ticket == null) return AuthenticateResult.Fail("Failed to decrypt token"); // Check if the token has expired if (ticket.Properties.ExpiresUtc.HasValue && ticket.Properties.ExpiresUtc.Value < Clock.UtcNow) return AuthenticateResult.Fail("Token has expired"); // Create a successful authentication result var authResult = new AuthenticationTicket(ticket.Principal, ticket.Properties, Scheme.Name); return AuthenticateResult.Success(authResult); } catch (Exception ex) { return AuthenticateResult.Fail($"Token processing failed: {ex.Message}"); } } }
5. Enable Authentication Middleware
Finally, add the authentication middleware to your pipeline in Startup.cs:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // Your other middleware (static files, routing, etc.)... app.UseAuthentication(); app.UseAuthorization(); // Your endpoint mappings... }
Test It Out
Now you can mark your ASP.NET Core controllers/actions with [Authorize], and when requests come in with Bearer tokens generated by your old .NET Framework apps, the Core app will decrypt them using your existing machineKey and recognize the authenticated user—just like the legacy apps do.
内容的提问来源于stack exchange,提问作者Nick Kovalsky

