C#EntityFramework核心Endopoint JWT身份验证

时间:2017-06-16 14:26:27

标签: c# asp.net .net entity-framework

我正在研究使用Entity Framework Core在ASP.NET Core中编写的Web API。

目前我正面临一个阻止我入睡的问题:)

我使用名为TokenProviderMiddleWare的类保护我的端点(在本教程页面上跟随:https://stormpath.com/blog/token-authentication-asp-net-core

这是我从数据库中检索用户并检查提供的密码是否与数据库匹配的功能:

        private Task<ClaimsIdentity> GetUserIdentity(string email, string password)
        {
            var driver = context.Drivers.SingleOrDefault(d => d.Email == email);

            if (driver == null)
                return Task.FromResult<ClaimsIdentity>(null);

            if (driver.Password != password)
            {
                SetBadLoginAttempt(driver);
                context.SaveChangesAsync();
                return Task.FromResult<ClaimsIdentity>(null);
            }

            if (driver.IsLoginDisabled)
            {
                return Task.FromResult<ClaimsIdentity>(null);
            }

            ResetBadLoginAttempt(driver);
            context.SaveChangesAsync();

            return Task.FromResult(new ClaimsIdentity(
                new System.Security.Principal.GenericIdentity(email, "Token"),
                new Claim[] {
                    new Claim("fullName", driver.Name),
                }
            ));
        }`

如果我在同一时间运行两次登录,我收到此错误:

Connection id "0HL5KO6M27JFT": An unhandled exception was thrown by the application.
System.InvalidOperationException: An attempt was made to use the context 
while it is being configured. A DbContext instance cannot be used inside OnConfiguring since it is still being configured at this
point.

通过在此函数的第一行执行此操作可以解决此问题:

Driver driver = null;
lock(context)
{
    driver = context.Drivers.SingleOrDefault(d => d.Email == email);
}

但我认为这很丑陋而且不具备可扩展性。

简而言之,我想通过EntityFramework检查数据库中的用户。我的DbContext是由.NET Core通过构造函数注入的。我认为存在某种并发问题......

我使用此代码的这个类看起来像这样:

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using SmartoonAPI.Persistence;
using System.Linq;
using System.Collections.Generic;
using SmartoonDomain.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace SmartoonAPI.JWT
{
public class TokenProviderMiddleware
{
    private readonly RequestDelegate next;
    private readonly TokenProviderOptions options;
    private readonly ISmartoonContext context;

    public TokenProviderMiddleware(RequestDelegate next, IOptions<TokenProviderOptions> options, ISmartoonContext context)
    {
        this.context = context;
        this.next = next;
        this.options = options.Value;
    }

    public Task Invoke(HttpContext 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 = 400;
            return context.Response.WriteAsync("Bad request.");
        }

        return GenerateToken(context);
    }

    private async Task GenerateToken(HttpContext context)
    {
        ClaimsIdentity identity;
        if (!string.IsNullOrEmpty(context.Request.Form["email"]) && !string.IsNullOrEmpty(context.Request.Form["password"]))
            identity = await GetUserIdentity(context.Request.Form["email"], context.Request.Form["password"]);
        else
            identity = await GetApplicationIdentity(context.Request.Form["appid"], context.Request.Form["secret"]);

        if (identity == null)
        {
            context.Response.StatusCode = 400;
            await context.Response.WriteAsync("Login failed!");
            return;
        }

        var now = DateTime.UtcNow;

        // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims.
        // You can add other claims here, if you want:
        var claims = new List<Claim>()
        {
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim(JwtRegisteredClaimNames.Iat, now.ToUniversalTime().ToString(), ClaimValueTypes.Integer64)
        };

        claims.AddRange(identity.Claims);

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

        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, new JsonSerializerSettings { Formatting = Formatting.Indented }));
    }

    private LoginAble SetBadLoginAttempt(LoginAble loginAble)
    {
        if (loginAble.LoginDisabledOn.HasValue && (DateTime.Now - loginAble.LoginDisabledOn.Value).TotalMinutes > 30)
        {
            ResetBadLoginAttempt(loginAble);
            loginAble.BadLoginAttempt++;
            return loginAble;
        }

        loginAble.BadLoginAttempt++;

        if (loginAble.BadLoginAttempt < 3)
        {
            return loginAble;
        }
        else
        {
            loginAble.IsLoginDisabled = true;
            loginAble.LoginDisabledOn = DateTime.Now;
        }
        return loginAble;
    }

    private LoginAble ResetBadLoginAttempt(LoginAble loginAble)
    {
        loginAble.BadLoginAttempt = 0;
        loginAble.IsLoginDisabled = false;
        loginAble.LoginDisabledOn = null;
        return loginAble;
    }

    private Task<ClaimsIdentity> GetUserIdentity(string email, string password)
    {
        var driver = context.Drivers.SingleOrDefault(d => d.Email == email);

        if (driver == null)
            return Task.FromResult<ClaimsIdentity>(null);

        if (driver.Password != password)
        {
            SetBadLoginAttempt(driver);
            context.SaveChangesAsync();
            return Task.FromResult<ClaimsIdentity>(null);
        }

        if (driver.IsLoginDisabled)
        {
            return Task.FromResult<ClaimsIdentity>(null);
        }

        ResetBadLoginAttempt(driver);
        context.SaveChangesAsync();

        return Task.FromResult(new ClaimsIdentity(
            new System.Security.Principal.GenericIdentity(email, "Token"),
            new Claim[] {
                new Claim("fullName", driver.Name),
            }
        ));
    }

    private Task<ClaimsIdentity> GetApplicationIdentity(string appId, string secret)
    {
        var appCredential = context.AppCredentials.SingleOrDefault(a => a.AppId == appId);

        if (appCredential == null)
            return Task.FromResult<ClaimsIdentity>(null);

        if (appCredential.Secret != secret)
        {
            SetBadLoginAttempt(appCredential);
            context.SaveChangesAsync();
            return Task.FromResult<ClaimsIdentity>(null);
        }

        ResetBadLoginAttempt(appCredential);
        context.SaveChangesAsync();

        return Task.FromResult(new ClaimsIdentity(
            new System.Security.Principal.GenericIdentity(appCredential.AppId, "Token"),
            new Claim[] {
                new Claim("description", appCredential.Description),
            }
        ));
    }
}
}

1 个答案:

答案 0 :(得分:0)

ASP.NET Core中间件每个应用程序实例化一次,而默认情况下,数据库上下文在每个请求上实例化,并在完成时处置。将数据库上下文直接注入Invoke方法中应该可以解决问题。