Asp.Net Core 1.0身份验证迁移到2.0

时间:2018-01-09 18:59:09

标签: asp.net-core-2.0 asp.net-core-webapi asp.net-authentication

我有一个Web API,我想迁移到Asp.Net Core 2.0。 API是安全的,我想将它迁移到2.0,因为我们完成了第一个循环。我尝试了一些东西,但是当我使用[Authenticate]属性保护我的控制器时,给定端点上的控制器永远不会被调用,因为用户未经过身份验证。

public partial class Startup
{
    public IConfigurationRoot Configuration { get; set; }

    private static JwtOptions _jwtOptions;

    private readonly IUserService _userService = new UserService();

    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
            .AddEnvironmentVariables();

        Configuration = builder.Build();
    }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        //Add DI and other services
        SetServices(services);

        services.Configure<ForwardedHeadersOptions>(options =>
        {
            options.ForwardedHeaders = ForwardedHeaders.XForwardedProto;
        });

        services.AddAuthentication(scheme => 
                      {
                          scheme.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                      })
                      .AddJwtBearer(options =>
                      {
                          options.TokenValidationParameters = new TokenValidationParameters
                          {
                              ValidateIssuer = true,
                              ValidateAudience = true,
                              ValidateLifetime = true,
                              ValidateIssuerSigningKey = true,
                              ValidIssuer = "aaa",
                              ValidAudience = "bbb",
                              IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("secret key"))
                          };
                       });

        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                      .AddCookie(options =>
                      {
                          options.ExpireTimeSpan = TimeSpan.FromDays(365);
                          options.Events = new CustomCookieAuthenticationEvents();
                          options.Cookie.Name = "access_token";
                      });

        services.Configure<CookieAuthenticationOptions>(options =>
        {
            options.Events = new Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents()
            {
                OnRedirectToLogin = ctx =>
                {
                    if (ctx.Request.Path.StartsWithSegments("/api") && ctx.Response.StatusCode == 200)
                    {
                        ctx.Response.StatusCode = (int)System.Net.HttpStatusCode.Unauthorized;
                        return Task.FromResult<object>(null);
                    }
                    else
                    {
                        ctx.Response.Redirect(ctx.RedirectUri);
                        return Task.FromResult<object>(null);
                    }
                }
            };
        });

        //Logger
        services.AddMvc(options =>
        {
            options.Filters.Add(new Loging.ApiExceptionFilter());
        });

        services.AddAuthentication(options =>
        {
            options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
        });

        // Add framework services.
        MvcOptions mvcOptions = new MvcOptions();
        mvcOptions.Filters.Add(new RequireHttpsAttribute());

        MvcJsonOptions jsonOptions = new MvcJsonOptions();
        jsonOptions.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        jsonOptions.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;

        services.AddMvc(options => options = mvcOptions).AddJsonOptions(options => options = jsonOptions);
        services.AddSession(options =>
        {
            // Set a short timeout for easy testing.
            options.IdleTimeout = TimeSpan.FromDays(365);
        });
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory, IOptions<JwtOptions> jwtOptions)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

        app.UseDeveloperExceptionPage();

        _jwtOptions = jwtOptions.Value;
        app.UseAuthentication();
        ConfigureAuth(app);

        app.Map(new PathString("/api/images"), x => x.UseBlobFileViewHandler());

        app.UseDefaultFiles();
        app.UseStaticFiles();
        app.UseSession();
        app.UseMvc(routes =>
        {
            routes.MapRoute
            (
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}"
            );

            routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Home", action = "Index" });
        });
    }

正如您所看到的,我尝试在startup.cs类上进行一些修改,但仍然无法弄清楚它是如何工作的。在文档中到处都是EF。那些不想使用EF实现的我们呢。

    public partial class Startup
        {
            private void ConfigureAuth(IApplicationBuilder app)
            {
                var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_jwtOptions.SecretKey));

                TokenProviderOptions tokenProviderOptions = new TokenProviderOptions
                {
                    Path = "/api/token",
                    Audience = _jwtOptions.Audience,
                    Issuer = _jwtOptions.Issuer,
                    SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256),
                    IdentityResolver = GetIdentity
                };

                TokenValidationParameters tokenValidationParameters = new TokenValidationParameters
                {
                    // The signing key must match!
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = signingKey,

                    // Validate the JWT Issuer (iss) claim
                    ValidateIssuer = true,
                    ValidIssuer = _jwtOptions.Issuer,

                    // Validate the JWT Audience (aud) claim
                    ValidateAudience = true,
                    ValidAudience = _jwtOptions.Audience,

                    // Validate the token expiry
                    ValidateLifetime = true,
                    LifetimeValidator = LifetimeValidator,

                    // If you want to allow a certain amount of clock drift, set that here:
                    ClockSkew = TimeSpan.Zero
                };

                app.UseSimpleTokenProvider(tokenProviderOptions, tokenValidationParameters);

            }

            private Task<ClaimsIdentity> GetIdentity(string email)
            {
                ServiceMessage<UserEntity> request = _userService.FindByEmailAsync(email).Result;

                if (request != null && request.Success && request.ResultObject != null)
                {
                    return Task.FromResult(CreateClaimsIdentity(request.ResultObject, "Token"));
                }

                // Credentials are invalid, or account doesn't exist
                return Task.FromResult<ClaimsIdentity>(null);
            }

            private ClaimsIdentity CreateClaimsIdentity(UserEntity user, string authenticationType)
            {
                List<Claim> claimCollection = new List<Claim>
                {
                    new Claim(ClaimTypes.NameIdentifier, user.Email, ClaimValueTypes.String),
                    new Claim(ClaimTypes.Role, user.Role, ClaimValueTypes.String),
                    new Claim(ClaimTypes.Name, user.Email.Split('@')[0], ClaimValueTypes.String),
                    new Claim(ClaimTypes.Expiration, DateTime.UtcNow.AddDays(365).Second.ToString(), ClaimValueTypes.DaytimeDuration)
                };

                ClaimsIdentity claimsIdentity = new ClaimsIdentity(claimCollection, authenticationType);

                return claimsIdentity;
            }

            private bool LifetimeValidator(DateTime? notBefore, DateTime? expires, SecurityToken token, TokenValidationParameters @params)
            {
                if (expires != null)
                {
                    return expires > DateTime.UtcNow;
                }
                return false;
            }

            public static string FromHex()
            {
                string hex = Guid.NewGuid().ToString();
                hex = hex.Replace("-", "");
                byte[] raw = new byte[hex.Length / 2];
                for (int i = 0; i < raw.Length; i++)
                {
                    raw[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
                }
                return Encoding.ASCII.GetString(raw);
            }
        }

    public class CustomCookieAuthenticationEvents : CookieAuthenticationEvents
        {
            public override Task RedirectToLogin(RedirectContext<CookieAuthenticationOptions> context)
            {
                if (context.HttpContext.Request.Path.StartsWithSegments("/api") && context.HttpContext.Response.StatusCode == 200)

                    context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                //return base.RedirectToLogin(context);

                return Task.FromResult((int)HttpStatusCode.Unauthorized);
            }
        }


    public class CustomJwtDataFormat : ISecureDataFormat<AuthenticationTicket>
        {
            private readonly string algorithm;
            private readonly TokenValidationParameters validationParameters;

            public CustomJwtDataFormat(string algorithm, TokenValidationParameters validationParameters)
            {
                this.algorithm = algorithm;
                this.validationParameters = validationParameters;
            }

            public AuthenticationTicket Unprotect(string protectedText) => Unprotect(protectedText, null);

            public AuthenticationTicket Unprotect(string protectedText, string purpose)
            {
                var handler = new JwtSecurityTokenHandler();
                ClaimsPrincipal principal = null;
                SecurityToken validToken = null;

                try
                {
                    principal = handler.ValidateToken(protectedText, this.validationParameters, out validToken);

                    var validJwt = validToken as JwtSecurityToken;

                    if (validJwt == null)
                    {
                        throw new ArgumentException("Invalid JWT");
                    }

                    if (!validJwt.Header.Alg.Equals(algorithm, StringComparison.Ordinal))
                    {
                        throw new ArgumentException($"Algorithm must be '{algorithm}'");
                    }

                    // Additional custom validation of JWT claims here (if any)
                }
                catch (SecurityTokenValidationException)
                {
                    return null;
                }
                catch (ArgumentException)
                {
                    return null;
                }

                // Validation passed. Return a valid AuthenticationTicket:
                return new AuthenticationTicket(principal, new Microsoft.AspNetCore.Authentication.AuthenticationProperties(), "Cookie");
            }

            // This ISecureDataFormat implementation is decode-only
            public string Protect(AuthenticationTicket data)
            {
                throw new NotImplementedException();
            }

            public string Protect(AuthenticationTicket data, string purpose)
            {
                throw new NotImplementedException();
            }
        }
    }

public class TokenProviderMiddleware
{
    private readonly RequestDelegate _next;
    private readonly TokenProviderOptions _options;
    private readonly ILogger _logger;
    private readonly JsonSerializerSettings _serializerSettings;
    private readonly TokenValidationParameters _tokenValidationParameters;

    private readonly ISocialAuthentificationServices _socialAuthentificationServices;

    public TokenProviderMiddleware(RequestDelegate next, IOptions<TokenProviderOptions> options, ILoggerFactory loggerFactory, ISocialAuthentificationServices socialAuthentificationServices, IOptions<TokenValidationParameters> tokenValidationParameters)
    {
        _socialAuthentificationServices = socialAuthentificationServices;

        _next = next;
        _logger = loggerFactory.CreateLogger<TokenProviderMiddleware>();

        _options = options.Value;
        _tokenValidationParameters = tokenValidationParameters.Value;
        ThrowIfInvalidOptions(_options);

        _serializerSettings = new JsonSerializerSettings
        {
            Formatting = Formatting.Indented
        };
    }

    public Task Invoke(HttpContext context)
    {
        //Add CORS to every response
        context.Response.Headers.Add("Access-Control-Allow-Headers", new string[] { "Authorization", "Content-Type" });
        context.Response.Headers.Add("Access-Control-Allow-Methods", new string[] { "OPTIONS", "POST", "GET", "DELETE", "PUT" });
        context.Response.Headers.Add("Access-Control-Allow-Origin", "*");

        if (context.Request.Method.Equals("OPTIONS", StringComparison.Ordinal))
        {
            context.Response.StatusCode = 204;
            return _next(context);
        }

        // If the request path doesn't match, skip
        if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal))
        {
            return _next(context);
        }

        // Request must be POST with Content-Type: application/x-www-form-urlencoded
        if (!context.Request.Method.Equals("POST") || !context.Request.HasFormContentType)
        {
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            return context.Response.WriteAsync("Bad request.");
        }

        _logger.LogInformation("Handling request: " + context.Request.Path);

        return GetToken(context);
    }

    private async Task GetToken(HttpContext context)
    {
        TokenData headers = GetHeaderContext(context);

        if (headers == null)
        {
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            await context.Response.WriteAsync("Invalid encrypted token.");
            return;
        }

        if (string.IsNullOrEmpty(headers.Provider))
        {
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            await context.Response.WriteAsync("Provider not definied.");
            return;
        }
        else if (string.IsNullOrEmpty(headers.Token))
        {
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            await context.Response.WriteAsync("Invalid token.");
            return;
        }
        else
        {
            var providers = (Mapurija.Models.Enum.Providers[])Enum.GetValues(typeof(Mapurija.Models.Enum.Providers));

            if (!headers.Provider.Contains(headers.Provider))
            {
                context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
                await context.Response.WriteAsync("Invalid token.");
                return;
            }
        }

        ServiceMessage<UserEntity> validation = null;

        int enumProvider = 0;
        int.TryParse(headers.Provider, out enumProvider);

        try
        {
            switch (enumProvider)
            {
                case (int)Mapurija.Models.Enum.Providers.Mapporia:
                    validation = await _socialAuthentificationServices.VerifyMapurijaTokenAsync(headers.Token);
                    break;
                case (int)Mapurija.Models.Enum.Providers.Facebook:
                    validation = await _socialAuthentificationServices.VerifyFacebookTokenAsync(headers.Token);
                    break;
                case (int)Mapurija.Models.Enum.Providers.Google:
                    validation = await _socialAuthentificationServices.VerifyFacebookTokenAsync(headers.Token);
                    break;
                default:
                    validation = null;
                    break;
            }
        }
        catch
        {
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            await context.Response.WriteAsync("Invalid request token!");
            return;
        }

        if (validation == null || !validation.Success || validation.ResultObject == null)
        {
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            await context.Response.WriteAsync(validation.ErrorMessage);
            return;
        }

        ClaimsIdentity identity = await _options.IdentityResolver(validation.ResultObject.Email);

        if (identity == null)
        {
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            await context.Response.WriteAsync("Invalid token.");
            return;
        }

        DateTime now = DateTime.UtcNow;

        // Specifically add the jti (nonce), iat (issued timestamp), and sub (subject/user) claims.
        // You can add other claims here, if you want:
        Claim[] claims = new Claim[]
        {
            new Claim(ClaimTypes.Name,validation.ResultObject.Email,ClaimValueTypes.String),
            new Claim(JwtRegisteredClaimNames.Sub,validation.ResultObject.Email,ClaimValueTypes.String),
            new Claim(JwtRegisteredClaimNames.Typ, validation.ResultObject.Role),
            new Claim(JwtRegisteredClaimNames.Jti, await _options.NonceGenerator()),
            new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(now).ToString(), ClaimValueTypes.Integer64),
            new Claim(JwtRegisteredClaimNames.Iss, _options.Issuer),
            new Claim(JwtRegisteredClaimNames.Aud, _options.Audience)
        };

        // Create the JWT and write it to a string
        JwtSecurityToken jwt = new JwtSecurityToken
        (
            issuer: _options.Issuer,
            audience: _options.Audience,
            claims: claims,
            notBefore: now,
            expires: now.Add(_options.Expiration),
            signingCredentials: _options.SigningCredentials
        );

        SecurityToken token;
        JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();

        string encodedJwt = handler.WriteToken(jwt);
        ClaimsPrincipal principal = handler.ValidateToken(encodedJwt, _tokenValidationParameters, out token);

        if (token == null)
        {
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            await context.Response.WriteAsync("Invalid token generated!");
            return;
        }

        var response = new
        {
            access_token = encodedJwt,
            expires_in = (int)_options.Expiration.TotalSeconds
        };

        // Serialize and return the response
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync(JsonConvert.SerializeObject(response, _serializerSettings));
    }

    private static void ThrowIfInvalidOptions(TokenProviderOptions options)
    {
        if (string.IsNullOrEmpty(options.Path))
        {
            throw new ArgumentNullException(nameof(TokenProviderOptions.Path));
        }

        if (string.IsNullOrEmpty(options.Issuer))
        {
            throw new ArgumentNullException(nameof(TokenProviderOptions.Issuer));
        }

        if (string.IsNullOrEmpty(options.Audience))
        {
            throw new ArgumentNullException(nameof(TokenProviderOptions.Audience));
        }

        if (options.Expiration == TimeSpan.Zero)
        {
            throw new ArgumentException("Must be a non-zero TimeSpan.", nameof(TokenProviderOptions.Expiration));
        }

        if (options.IdentityResolver == null)
        {
            throw new ArgumentNullException(nameof(TokenProviderOptions.IdentityResolver));
        }

        if (options.SigningCredentials == null)
        {
            throw new ArgumentNullException(nameof(TokenProviderOptions.SigningCredentials));
        }

        if (options.NonceGenerator == null)
        {
            throw new ArgumentNullException(nameof(TokenProviderOptions.NonceGenerator));
        }
    }

    private TokenData GetHeaderContext(HttpContext context)
    {
        string token = new StreamReader(context.Request.Body).ReadToEnd();

        if (string.IsNullOrEmpty(token))
        {
            return null;
        }

        var encrypted = Convert.FromBase64String(token);
        var decriptedFromJavascript = Mapurija.Services.Common.TokenDecrypter.DecryptStringFromBytes(encrypted, Mapurija.Services.Common.TokenDecrypter.KeyBytes, Mapurija.Services.Common.TokenDecrypter.Vi);

        TokenData result = JsonConvert.DeserializeObject< TokenData>(decriptedFromJavascript);
        return result;
    }

    /// <summary>
    /// Get this datetime as a Unix epoch timestamp (seconds since Jan 1, 1970, midnight UTC).
    /// </summary>
    /// <param name="date">The date to convert.</param>
    /// <returns>Seconds since Unix epoch.</returns>
    public static long ToUnixEpochDate(DateTime date) => (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);
}

public class TokenProviderOptions
    {
        /// <summary>
        /// The relative request path to listen on.
        /// </summary>
        /// <remarks>The default path is <c>/token</c>.</remarks>
        public string Path { get; set; } = "api/token";

        /// <summary>
        ///  The Issuer (iss) claim for generated tokens.
        /// </summary>
        public string Issuer { get; set; }

        /// <summary>
        /// The Audience (aud) claim for the generated tokens.
        /// </summary>
        public string Audience { get; set; }

        /// <summary>
        /// The expiration time for the generated tokens.
        /// </summary>
        /// <remarks>The default is five minutes (300 seconds).</remarks>
        public TimeSpan Expiration { get; set; } = TimeSpan.FromDays(365);

        /// <summary>
        /// The signing key to use when generating tokens.
        /// </summary>
        public SigningCredentials SigningCredentials { get; set; }

        /// <summary>
        /// Resolves a user identity given a username and password.
        /// </summary>
        public Func<string, Task<ClaimsIdentity>> IdentityResolver { get; set; }

        /// <summary>
        /// Generates a random value (nonce) for each generated token.
        /// </summary>
        /// <remarks>The default nonce is a random GUID.</remarks>
        public Func<Task<string>> NonceGenerator { get; set; } = new Func<Task<string>>(() => Task.FromResult(Guid.NewGuid().ToString()));

0 个答案:

没有答案