ASP.NET Web API的JWT身份验证

Ami*_*ich 238 c# security jwt asp.net-web-api

我正在尝试在我的Web API应用程序中支持JWT承载令牌(JSON Web令牌),我迷路了.

我看到对.NET Core和OWIN应用程序的支持.
我目前正在IIS中托管我的应用程序.

如何在我的应用程序中实现此身份验证模块?有什么办法可以使用<authentication>类似于我使用表单/ Windows身份验证的方式配置吗?

cuo*_*gle 559

我回答了这个问题:4年前如何使用HMAC 保护ASP.NET Web API.

现在,许多事情在安全方面发生了变化,尤其是JWT越来越受欢迎.在这里,我将尝试解释如何以最简单和最基本的方式使用JWT,因此我们不会迷失在OWIN,Oauth2,ASP.NET Identity ... :)的丛林中.

如果你不了解JWT令牌,你需要看一下:

https://tools.ietf.org/html/rfc7519

基本上,JWT令牌看起来像:

<base64-encoded header>.<base64-encoded claims>.<base64-encoded signature>
Run Code Online (Sandbox Code Playgroud)

例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ

JWT令牌有三个部分:

  1. 标题:JSON格式,编码为base64
  2. 声明:JSON格式,编码为base64.
  3. 签名:基于标题和声明创建和签名,编码为base64.

如果您使用上面带有令牌的网站jwt.io,您可以解码并查看如下所示的令牌:

在此输入图像描述

从技术上讲,JWT使用签名,签名是从头文件和声明中签名的,并使用标头中指定的安全算法(例如:HMACSHA256).因此,如果您在声明中存储任何敏感信息,则需要通过HTTP传输JWT.

现在,为了使用JWT身份验证,如果您拥有传统的Web Api系统,则实际上并不需要OWIN中间件.简单的概念是如何提供JWT令牌以及如何在请求到来时验证令牌.而已.

回到演示,让JWT令牌轻巧,我只有专卖店usernameexpiration time中智威汤逊.但是这样,您必须重新构建新的本地标识(主体)以添加更多信息,例如:roles ..如果您想要进行角色授权.但是,如果你想在JWT中添加更多信息,这取决于你,非常灵活.

您可以通过使用来自控制器的操作简单地提供JWT令牌端点,而不是使用OWIN中间件:

public class TokenController : ApiController
{
    // This is naive endpoint for demo, it should use Basic authentication
    // to provide token or POST request
    [AllowAnonymous]
    public string Get(string username, string password)
    {
        if (CheckUser(username, password))
        {
            return JwtManager.GenerateToken(username);
        }

        throw new HttpResponseException(HttpStatusCode.Unauthorized);
    }

    public bool CheckUser(string username, string password)
    {
        // should check in the database
        return true;
    }
}
Run Code Online (Sandbox Code Playgroud)

这是天真的动作,在生产中你应该使用POST请求或基本身份验证端点来提供JWT令牌.

如何基于生成令牌username

您可以使用System.IdentityModel.Tokens.Jwt从MS 调用的NuGet包生成令牌,如果您愿意,也可以使用其他包.在演示中,我使用HMACSHA256SymmetricKey:

/// <summary>
/// Use the below code to generate symmetric Secret Key
///     var hmac = new HMACSHA256();
///     var key = Convert.ToBase64String(hmac.Key);
/// </summary>
private const string Secret = "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==";

public static string GenerateToken(string username, int expireMinutes = 20)
{
    var symmetricKey = Convert.FromBase64String(Secret);
    var tokenHandler = new JwtSecurityTokenHandler();

    var now = DateTime.UtcNow;
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Name, username)
        }),

        Expires = now.AddMinutes(Convert.ToInt32(expireMinutes)),

        SigningCredentials = new SigningCredentials(
            new SymmetricSecurityKey(symmetricKey), 
            SecurityAlgorithms.HmacSha256Signature)
    };

    var stoken = tokenHandler.CreateToken(tokenDescriptor);
    var token = tokenHandler.WriteToken(stoken);

    return token;
}
Run Code Online (Sandbox Code Playgroud)

端点提供JWT令牌做,现在,如何验证JWT当请求到来时,在我所建的演示 JwtAuthenticationAttribute,从继承IAuthenticationFilter,有关认证过滤器的更多细节在这里.

使用此属性,您可以对任何操作进行身份验证,只需将此属性放在该操作上即可.

public class ValueController : ApiController
{
    [JwtAuthentication]
    public string Get()
    {
        return "value";
    }
}
Run Code Online (Sandbox Code Playgroud)

如果要验证WebApi的所有传入请求(不是特定于Controller或操作),您还可以使用OWIN中间件或DelegateHander

以下是身份验证过滤器的核心方法:

private static bool ValidateToken(string token, out string username)
{
    username = null;

    var simplePrinciple = JwtManager.GetPrincipal(token);
    var identity = simplePrinciple.Identity as ClaimsIdentity;

    if (identity == null)
        return false;

    if (!identity.IsAuthenticated)
        return false;

    var usernameClaim = identity.FindFirst(ClaimTypes.Name);
    username = usernameClaim?.Value;

    if (string.IsNullOrEmpty(username))
       return false;

    // More validate to check whether username exists in system

    return true;
}

protected Task<IPrincipal> AuthenticateJwtToken(string token)
{
    string username;

    if (ValidateToken(token, out username))
    {
        // based on username to get more information from database 
        // in order to build local identity
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, username)
            // Add more claims if needed: Roles, ...
        };

        var identity = new ClaimsIdentity(claims, "Jwt");
        IPrincipal user = new ClaimsPrincipal(identity);

        return Task.FromResult(user);
    }

    return Task.FromResult<IPrincipal>(null);
}
Run Code Online (Sandbox Code Playgroud)

工作流程是使用JWT库(上面的NuGet包)来验证JWT令牌然后返回ClaimsPrincipal.您可以执行更多验证,例如检查系统上是否存在用户,并根据需要添加其他自定义验证.用于验证JWT令牌并获取主体的代码:

public static ClaimsPrincipal GetPrincipal(string token)
{
    try
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var jwtToken = tokenHandler.ReadToken(token) as JwtSecurityToken;

        if (jwtToken == null)
            return null;

        var symmetricKey = Convert.FromBase64String(Secret);

        var validationParameters = new TokenValidationParameters()
        {
            RequireExpirationTime = true,
            ValidateIssuer = false,
            ValidateAudience = false,
            IssuerSigningKey = new SymmetricSecurityKey(symmetricKey)
        };

        SecurityToken securityToken;
        var principal = tokenHandler.ValidateToken(token, validationParameters, out securityToken);

        return principal;
    }
    catch (Exception)
    {
        //should write log
        return null;
    }
}
Run Code Online (Sandbox Code Playgroud)

如果验证了JWT令牌并且返回了principal,那么您应该构建新的本地身份并将更多信息放入其中以检查角色授权.

请记住config.Filters.Add(new AuthorizeAttribute());在全局范围内添加(默认授权)以防止对您的资源发出任何匿名请求.

您可以使用Postman来测试演示:

请求令牌(我上面提到的天真,仅用于演示):

GET http://localhost:{port}/api/token?username=cuong&password=1
Run Code Online (Sandbox Code Playgroud)

将JWT令牌放在标题中以获取授权请求,例如:

GET http://localhost:{port}/api/value

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s
Run Code Online (Sandbox Code Playgroud)

这个演示放在这里:https://github.com/cuongle/WebApi.Jwt

  • 哇这是我在很长一段时间内看到的最简单的解释.如果可以的话,+ 100 (6认同)
  • @Cuong Le解释得很好,但我想补充更多:如果你使用OWIN检查Microsoft.Owin.Security.Jwt中可用的UseJwtBearerAuthentication,你可以在WebAPI上使用这个owin中间件来自动验证每个传入的请求.使用owin启动类来注册中间件 (5认同)
  • @AmirPopovich您不需要在响应上设置令牌,令牌需要存储在客户端的其他位置,对于Web,您可以放入本地存储,每当您发送HTTP请求时,将令牌放在标头上. (5认同)
  • @Homam:对这个迟到的答案感到抱歉,最好的生成方法是:var`hmac = new HMACSHA256(); var key = Convert.ToBase64String(hmac.Key);` (4认同)
  • @CuongLe - 关于这个`public const string Secret ="856FECBA3B06519C8DDDBC80BB080553"; //你的symetric`,生成和存储秘密的最佳方法是什么? (3认同)
  • 任何使用CuongLe的repo演示代码的人都会注意到有一个错误,没有处理没有授权标头的请求,这意味着没有任何查询的任何查询都可以通过(端点不太安全!).来自@magicleon的拉取请求可以解决此问题:https://github.com/cuongle/WebApi.Jwt/pull/4 (3认同)
  • @CuongLe:谢谢你的回答.很棒.您使用的一个问题在SecurityTokenDescriptor中过期.到期时间过后会发生什么?我如何生成新令牌? (2认同)
  • 如果我可以向上投票1000次,我会.这是一个非常好的答案.谢谢! (2认同)
  • 在http://www.decatechlabs.com/how-to-secure-webapi-using-json-web-tokenjwt上逐步实现了一个非常好的实现 (2认同)
  • 我在您的代码上打开了一个拉取请求,因为我认为我在 Authorization 标头时发现了一个错误。如果对你来说没问题,我建议也编辑这个答案! (2认同)
  • https://jwt.io/表示您上面的示例密钥的签名无效.知道为什么吗? (2认同)

Ale*_*man 10

我已经设法以最小的努力来实现它(就像使用ASP.NET Core一样简单)。

为此,我使用OWIN Startup.cs文件和Microsoft.Owin.Security.Jwt库。

为了使应用程序正常运行,Startup.cs我们需要修改Web.config

<configuration>
  <appSettings>
    <add key="owin:AutomaticAppStartup" value="true" />
    ...
Run Code Online (Sandbox Code Playgroud)

下面是如何Startup.cs看起来应该:

using MyApp.Helpers;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Jwt;
using Owin;

[assembly: OwinStartup(typeof(MyApp.App_Start.Startup))]

namespace MyApp.App_Start
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseJwtBearerAuthentication(
                new JwtBearerAuthenticationOptions
                {
                    AuthenticationMode = AuthenticationMode.Active,
                    TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidAudience = ConfigHelper.GetAudience(),
                        ValidIssuer = ConfigHelper.GetIssuer(),
                        IssuerSigningKey = ConfigHelper.GetSymmetricSecurityKey(),
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true
                    }
                });
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如今,你们中的许多人都使用ASP.NET Core,因此正如您所看到的,它与我们现有的并没有太大区别。

首先确实让我感到困惑,我试图实现自定义提供程序,等等。但是我没想到它会如此简单。OWIN只是石头!

值得一提的是-在启用OWIN启动NSWag库后,我停止为我工作(例如,某些人可能想为Angular应用自动生成Typescript HTTP代理)。

解决方案也非常简单-我替换NSWagSwashbuckle,没有任何其他问题。


好的,现在共享ConfigHelper代码:

public class ConfigHelper
{
    public static string GetIssuer()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["Issuer"];
        return result;
    }

    public static string GetAudience()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["Audience"];
        return result;
    }

    public static SigningCredentials GetSigningCredentials()
    {
        var result = new SigningCredentials(GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256);
        return result;
    }

    public static string GetSecurityKey()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["SecurityKey"];
        return result;
    }

    public static byte[] GetSymmetricSecurityKeyAsBytes()
    {
        var issuerSigningKey = GetSecurityKey();
        byte[] data = Encoding.UTF8.GetBytes(issuerSigningKey);
        return data;
    }

    public static SymmetricSecurityKey GetSymmetricSecurityKey()
    {
        byte[] data = GetSymmetricSecurityKeyAsBytes();
        var result = new SymmetricSecurityKey(data);
        return result;
    }

    public static string GetCorsOrigins()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["CorsOrigins"];
        return result;
    }
}
Run Code Online (Sandbox Code Playgroud)

另一个重要方面-我通过Authorization标头发送了JWT令牌,因此打字稿代码如下所示:

(以下代码由NSWag生成)

@Injectable()
export class TeamsServiceProxy {
    private http: HttpClient;
    private baseUrl: string;
    protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;

    constructor(@Inject(HttpClient) http: HttpClient, @Optional() @Inject(API_BASE_URL) baseUrl?: string) {
        this.http = http;
        this.baseUrl = baseUrl ? baseUrl : "https://localhost:44384";
    }

    add(input: TeamDto | null): Observable<boolean> {
        let url_ = this.baseUrl + "/api/Teams/Add";
        url_ = url_.replace(/[?&]$/, "");

        const content_ = JSON.stringify(input);

        let options_ : any = {
            body: content_,
            observe: "response",
            responseType: "blob",
            headers: new HttpHeaders({
                "Content-Type": "application/json", 
                "Accept": "application/json",
                "Authorization": "Bearer " + localStorage.getItem('token')
            })
        };
Run Code Online (Sandbox Code Playgroud)

参见标题部分- "Authorization": "Bearer " + localStorage.getItem('token')


Ron*_*omb 7

就我而言,JWT 是由单独的 API 创建的,因此 ASP.NET 只需要对其进行解码和验证。与接受的答案相反,我们使用的是非对称算法 RSA,因此SymmetricSecurityKey上面提到的类将不起作用。

结果如下。

using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Threading;
using System.Threading.Tasks;

    public static async Task<JwtSecurityToken> VerifyAndDecodeJwt(string accessToken)
    {
        try
        {
            var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{securityApiOrigin}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
            var openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None);
            var validationParameters = new TokenValidationParameters()
            {
                ValidateLifetime = true,
                ValidateAudience = false,
                ValidateIssuer = false,
                RequireSignedTokens = true,
                IssuerSigningKeys = openIdConfig.SigningKeys,
            };
            new JwtSecurityTokenHandler().ValidateToken(accessToken, validationParameters, out var validToken);
            // threw on invalid, so...
            return validToken as JwtSecurityToken;
        }
        catch (Exception ex)
        {
            logger.Info(ex.Message);
            return null;
        }
    }
Run Code Online (Sandbox Code Playgroud)


Zee*_*dil 6

这是在ASP.NET Core Web API中使用JWT令牌的基于声明的身份验证的非常简单和安全的实现。

首先,您需要公开一个端点,该端点返回分配了用户声明的JWT令牌:

 /// <summary>
        /// Login provides API to verify user and returns authentication token.
        /// API Path:  api/account/login
        /// </summary>
        /// <param name="paramUser">Username and Password</param>
        /// <returns>{Token: [Token] }</returns>
        [HttpPost("login")]
        [AllowAnonymous]
        public async Task<IActionResult> Login([FromBody] UserRequestVM paramUser, CancellationToken ct)
        {

            var result = await UserApplication.PasswordSignInAsync(paramUser.Email, paramUser.Password, false, lockoutOnFailure: false);

            if (result.Succeeded)
            {
                UserRequestVM request = new UserRequestVM();
                request.Email = paramUser.Email;


                ApplicationUser UserDetails = await this.GetUserByEmail(request);
                List<ApplicationClaim> UserClaims = await this.ClaimApplication.GetListByUser(UserDetails);

                var Claims = new ClaimsIdentity(new Claim[]
                                {
                                    new Claim(JwtRegisteredClaimNames.Sub, paramUser.Email.ToString()),
                                    new Claim(UserId, UserDetails.UserId.ToString())
                                });


                //Adding UserClaims to JWT claims
                foreach (var item in UserClaims)
                {
                    Claims.AddClaim(new Claim(item.ClaimCode, string.Empty));
                }

                var tokenHandler = new JwtSecurityTokenHandler();
                  // this information will be retrived from you Configuration
                //I have injected Configuration provider service into my controller
                var encryptionkey = Configuration["Jwt:Encryptionkey"];
                var key = Encoding.ASCII.GetBytes(encryptionkey);
                var tokenDescriptor = new SecurityTokenDescriptor
                {
                    Issuer = Configuration["Jwt:Issuer"],
                    Subject = Claims,

                // this information will be retrived from you Configuration
                //I have injected Configuration provider service into my controller
                    Expires = DateTime.UtcNow.AddMinutes(Convert.ToDouble(Configuration["Jwt:ExpiryTimeInMinutes"])),

                    //algorithm to sign the token
                    SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)

                };

                var token = tokenHandler.CreateToken(tokenDescriptor);
                var tokenString = tokenHandler.WriteToken(token);

                return Ok(new
                {
                    token = tokenString
                });
            }

            return BadRequest("Wrong Username or password");
        }
Run Code Online (Sandbox Code Playgroud)

现在,您需要在startup.csConfigureServices内部的服务中添加身份验证,以将JWT身份验证添加为默认身份验证服务,如下所示:

services.AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
             .AddJwtBearer(cfg =>
             {
                 cfg.RequireHttpsMetadata = false;
                 cfg.SaveToken = true;
                 cfg.TokenValidationParameters = new TokenValidationParameters()
                 {
                     //ValidateIssuerSigningKey = true,
                     IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Encryptionkey"])),
                     ValidateAudience = false,
                     ValidateLifetime = true,
                     ValidIssuer = configuration["Jwt:Issuer"],
                     //ValidAudience = Configuration["Jwt:Audience"],
                     //IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Key"])),
                 };
             });
Run Code Online (Sandbox Code Playgroud)

现在,您可以将策略添加到授权服务中,如下所示:

services.AddAuthorization(options =>
            {
                options.AddPolicy("YourPolicyNameHere",
                                policy => policy.RequireClaim("YourClaimNameHere"));
            });
Run Code Online (Sandbox Code Playgroud)

另外,您也可以(不必要)从数据库填充所有声明,因为该声明仅在应用程序启动时运行一次,并将它们添加到以下策略中:

  services.AddAuthorization(async options =>
            {
                var ClaimList = await claimApplication.GetList(applicationClaim);
                foreach (var item in ClaimList)
                {                        
                    options.AddPolicy(item.ClaimCode, policy => policy.RequireClaim(item.ClaimCode));                       
                }
            });
Run Code Online (Sandbox Code Playgroud)

现在,您可以将“策略”过滤器放在您想要被授权的任何方法上,如下所示:

 [HttpPost("update")]
        [Authorize(Policy = "ACC_UP")]
        public async Task<IActionResult> Update([FromBody] UserRequestVM requestVm, CancellationToken ct)
        {
//your logic goes here
}
Run Code Online (Sandbox Code Playgroud)

希望这可以帮助


Ily*_*dik 5

我认为您应该使用一些 3d 方服务器来支持 JWT 令牌,并且 WEB API 2 中没有开箱即用的 JWT 支持。

然而,有一个 OWIN 项目支持某种格式的签名令牌(不是 JWT)。它用作简化的 OAuth 协议,为网站提供一种简单的身份验证形式。

你可以阅读更多关于它的信息,例如在这里

它相当长,但大部分是关于控制器和 ASP.NET Identity 的详细信息,您可能根本不需要。最重要的是

第 9 步:添加对 OAuth 承载令牌生成的支持

第 12 步:测试后端 API

在那里您可以阅读如何设置可以从前端访问的端点(例如“/token”)(以及有关请求格式的详细信息)。

其他步骤提供了有关如何将该端点连接到数据库等的详细信息,您可以选择所需的部分。