ASP.NET Core 2.0 Bearer认证无需使用Identity

34

当我昨天开始在.NET Core 2.0上实现自包含的Bearer身份验证WebAPI时,我认为我的目标非常简单,但到现在为止,我仍然没有得到任何类似的结果。以下是我正在尝试做的事情:

  • 实现受Bearer令牌保护的WebAPI
  • 从同一项目中的端点发行令牌和刷新令牌
  • 使用[Authorize]属性控制对API表面的访问
  • 不使用ASP.Net Identity(我的用户/成员资格要求更轻)

我完全可以在登录中构建identity/claims/principal并将其添加到请求上下文中,但我还没有看到任何不使用Identity在Core 2.0 WebAPI中发布和使用auth/refresh tokens的示例。我看到了1.x MSDN关于使用cookies而不使用Identity的示例,但那并不能满足上述要求。

我觉得这可能是一个常见的场景,而且它不应该这么难(也许并不是,可能只是缺乏文档/示例?)。据我所知,IdentityServer4与Core 2.0 Auth不兼容,opendiddict似乎需要Identity。我也不想在单独的进程中托管令牌端点,而是在同一WebAPI实例中。

有人可以指向一个具体的示例,或至少给出一些指导步骤/选项吗?


3
我也想看一份样品。 - Piotr Stulinski
身份验证与JWT机制解耦。请阅读此文此文。问候。 - Isaac Ojeda
2个回答

26

我对其进行了编辑,以使其与ASP.NET Core 2.0兼容。


首先,需要一些Nuget包:

  • Microsoft.AspNetCore.Authentication.JwtBearer
  • Microsoft.AspNetCore.Identity
  • System.IdentityModel.Tokens.Jwt
  • System.Security.Cryptography.Csp

然后是一些基本的数据传输对象。

// Presumably you will have an equivalent user account class with a user name.
public class User
{
    public string UserName { get; set; }
}

public class JsonWebToken
{
    public string access_token { get; set; }

    public string token_type { get; set; } = "bearer";

    public int expires_in { get; set; }

    public string refresh_token { get; set; }
}

进入实际功能,您需要一个登录/令牌Web方法来向用户发送授权令牌。

[Route("api/token")]
public class TokenController : Controller
{
    private ITokenProvider _tokenProvider;

    public TokenController(ITokenProvider tokenProvider) // We'll create this later, don't worry.
    {
        _tokenProvider = tokenProvider;
    }

    public JsonWebToken Get([FromQuery] string grant_type, [FromQuery] string username, [FromQuery] string password, [FromQuery] string refresh_token)
    {
        // Authenticate depending on the grant type.
        User user = grant_type == "refresh_token" ? GetUserByToken(refresh_token) : GetUserByCredentials(username, password);

        if (user == null)
            throw new UnauthorizedAccessException("No!");

        int ageInMinutes = 20;  // However long you want...

        DateTime expiry = DateTime.UtcNow.AddMinutes(ageInMinutes);

        var token = new JsonWebToken {
            access_token = _tokenProvider.CreateToken(user, expiry),
            expires_in   = ageInMinutes * 60
        };

        if (grant_type != "refresh_token")
            token.refresh_token = GenerateRefreshToken(user);

        return token;
    }

    private User GetUserByToken(string refreshToken)
    {
        // TODO: Check token against your database.
        if (refreshToken == "test")
            return new User { UserName = "test" };

        return null;
    }

    private User GetUserByCredentials(string username, string password)
    {
        // TODO: Check username/password against your database.
        if (username == password)
            return new User { UserName = username };

        return null;
    }

    private string GenerateRefreshToken(User user)
    {
        // TODO: Create and persist a refresh token.
        return "test";
    }
}

您可能已经注意到,令牌创建仍然只是通过某个想象中的ITokenProvider传递的“魔法”。请定义令牌提供程序接口。
public interface ITokenProvider
{
    string CreateToken(User user, DateTime expiry);

    // TokenValidationParameters is from Microsoft.IdentityModel.Tokens
    TokenValidationParameters GetValidationParameters();
}

我使用RSA安全密钥在JWT上实现了令牌创建。所以...

public class RsaJwtTokenProvider : ITokenProvider
{
    private RsaSecurityKey _key;
    private string _algorithm;
    private string _issuer;
    private string _audience;

    public RsaJwtTokenProvider(string issuer, string audience, string keyName)
    {
        var parameters = new CspParameters { KeyContainerName = keyName };
        var provider = new RSACryptoServiceProvider(2048, parameters);

        _key = new RsaSecurityKey(provider);

        _algorithm = SecurityAlgorithms.RsaSha256Signature;
        _issuer = issuer;
        _audience = audience;
    }

    public string CreateToken(User user, DateTime expiry)
    {
        JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();

        ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(user.UserName, "jwt"));

        // TODO: Add whatever claims the user may have...

        SecurityToken token = tokenHandler.CreateJwtSecurityToken(new SecurityTokenDescriptor
        {
            Audience = _audience,
            Issuer = _issuer,
            SigningCredentials = new SigningCredentials(_key, _algorithm),
            Expires = expiry.ToUniversalTime(),
            Subject = identity
        });

        return tokenHandler.WriteToken(token);
    }

    public TokenValidationParameters GetValidationParameters()
    {
        return new TokenValidationParameters
        {
            IssuerSigningKey = _key,
            ValidAudience = _audience,
            ValidIssuer = _issuer,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromSeconds(0) // Identity and resource servers are the same.
        };
    }
}

现在您正在生成令牌。是时候实际验证并连接它们了。请转到您的Startup.cs。

ConfigureServices()中:

var tokenProvider = new RsaJwtTokenProvider("issuer", "audience", "mykeyname");
services.AddSingleton<ITokenProvider>(tokenProvider);

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options => {
        options.RequireHttpsMetadata = false;
        options.TokenValidationParameters = tokenProvider.GetValidationParameters();
    });

// This is for the [Authorize] attributes.
services.AddAuthorization(auth => {
    auth.DefaultPolicy = new AuthorizationPolicyBuilder()
        .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
        .RequireAuthenticatedUser()
        .Build();
});

然后 Configure()

public void Configure(IApplicationBuilder app)
{
    app.UseAuthentication();

    // Whatever else you're putting in here...

    app.UseMvc();
}

希望我没有漏掉什么,这应该是你所需的全部内容。

愉快的结果是...

[Authorize] // Yay!
[Route("api/values")]
public class ValuesController : Controller
{
    // ...
}

2
我甚至没有考虑过使用新的Core2 auth堆栈等来自己创建 - 我认为这应该是开箱即用的。无论如何,你的解决方案唯一缺少的是刷新令牌,但鉴于上述问题,这很微不足道。一个问题 - 这些安全令牌是不透明还是透明的?(即,当呈现令牌时,身份验证堆栈是否会取消保护并将身份附加到webapi上下文中,还是需要额外的步骤?)谢谢Mitch! - pseabury
它解密令牌并为您设置上下文身份。在您的控制器中,User.Identity.Name 将是传递到 JWT 中的用户名。 - Mitch
是的,我还没有开始刷新令牌 - 它与JWT生成代码相当独立。通过一些随机哈希生成令牌,存储它,并在刷新调用期间检查它。这段代码是为了快速创建API而编写的,在.NET Core beta阶段。如果有人有一个更简单的实现方法,使用新功能,那就太好了。 - Mitch
1
在Configure()方法中,我遇到了一个错误,指出UseJwtBearerAuthentication()已被废弃并引用了一些复杂的文章(更像是MS代码人员之间仍在实施这个过程中的内部讨论)。 我不想使用任何类型的Identity Server。 请有人告诉我使此操作生效的唯一方法不是回滚到asp.net core 1.1。 MS:为什么要这样对待我们? - alexb
好的,我的代码库终于升级到2.0了,所以我调整了我的答案。 - Mitch
1
请注意,在Mac OS X上使用.NET Core 2时,当执行“var provider = new RSACryptoServiceProvider(2048, parameters);”时会出现System.PlatformNotSupportedException。在StackOverflow上有关于此问题的独立讨论线程... - Nico de Wet

15

跟随 @Mitch 的回答:在移动到 .NET Core 2.0 时,身份验证堆栈发生了相当大的变化。以下答案仅使用新实现。

using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;

namespace JwtWithoutIdentity
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(cfg =>
                {
                    cfg.RequireHttpsMetadata = false;
                    cfg.SaveToken = true;

                    cfg.TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidIssuer = "me",
                        ValidAudience = "you",
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")) //Secret
                    };

                });

            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseAuthentication();

            app.UseMvc();
        }
    }
}

令牌控制器

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using JwtWithoutIdentity.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;

namespace JwtWithoutIdentity.Controllers
{
    public class TokenController : Controller
    {

        [AllowAnonymous]
        [Route("api/token")]
        [HttpPost]
        public async Task<IActionResult> Token(LoginViewModel model)
        {

            if (!ModelState.IsValid) return BadRequest("Token failed to generate");

            var user = (model.Password == "password" && model.Username == "username");

            if (!user) return Unauthorized();

            //Add Claims
            var claims = new[]
            {
                new Claim(JwtRegisteredClaimNames.UniqueName, "data"),
                new Claim(JwtRegisteredClaimNames.Sub, "data"),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            };

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")); //Secret
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

            var token = new JwtSecurityToken("me",
                "you",
                claims,
                expires: DateTime.Now.AddMinutes(30),
                signingCredentials: creds);

            return Ok(new JsonWebToken()
            {
                access_token = new JwtSecurityTokenHandler().WriteToken(token),
                expires_in = 600000,
                token_type = "bearer"
            });
        }
    }
}

值控制器

using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace JwtWithoutIdentity.Controllers
{
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // GET api/values
        [Authorize]
        [HttpGet]
        public IEnumerable<string> Get()
        {
            var name = User.Identity.Name;
            var claims = User.Claims;

            return new string[] { "value1", "value2" };
        }
    }
}

希望这可以帮助到你!


3
谢谢您发布这篇文章,我一直在寻找类似的内容。我只是想知道,如果您没有使用“用户:身份”,为什么仍然保留了声明和身份呢? - Bogdan Daniel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接