如何在.NET Core中正确设置WEB API的策略授权 [英] How to correctly setup Policy Authorization for WEB API in .NET Core
问题描述
我有这个Web API项目,没有UI.我的appsettings.json
文件有一个列出令牌和令牌属于哪个客户端的部分.因此,客户端只需要在标头中显示一个匹配的令牌即可.如果没有提供令牌或令牌无效,则应该返回401.
I have this Web API project, with no UI. My appsettings.json
file has a section listing tokens and which client they belong to. So the client will need to just present a matching token in the header. If no token is presented or an invalid one, then it should be returning a 401.
在ConfigureServices中,我设置授权
In ConfigureServices I setup authorization
.AddTransient<IAuthorizationRequirement, ClientTokenRequirement>()
.AddAuthorization(opts => opts.AddPolicy(SecurityTokenPolicy, policy =>
{
var sp = services.BuildServiceProvider();
policy.Requirements.Add(sp.GetService<IAuthorizationRequirement>());
}))
从我所见,这部分可以正确触发. 这是ClientTokenRequirement的代码
This part fires correctly from what I can see. Here is code for the ClientTokenRequirement
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ClientTokenRequirement requirement)
{
if (context.Resource is AuthorizationFilterContext authFilterContext)
{
if (string.IsNullOrWhiteSpace(_tokenName))
throw new UnauthorizedAccessException("Token not provided");
var httpContext = authFilterContext.HttpContext;
if (!httpContext.Request.Headers.TryGetValue(_tokenName, out var tokenValues))
return Task.CompletedTask;
var tokenValueFromHeader = tokenValues.FirstOrDefault();
var matchedToken = _tokens.FirstOrDefault(t => t.Token == tokenValueFromHeader);
if (matchedToken != null)
{
httpContext.Succeed(requirement);
}
}
return Task.CompletedTask;
}
当我们在ClientTokenRequirement
中并且没有匹配令牌时,它将返回
When we are in the ClientTokenRequirement
and have not matched a token it returns
return Task.CompletedTask;
完成此操作的方式在 https://docs.microsoft. com/en-us/aspnet/core/security/authorization/policies?view = aspnetcore-2.1
This is done how it is documented at https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.1
在有有效令牌的情况下,此方法可以正常工作,但是在没有令牌并且返回Task.Completed
时,则没有401,而是一个异常
This works correctly when there is a valid token, but when there isnt and it returns Task.Completed
, there is no 401 but an exception instead
InvalidOperationException:未指定authenticationScheme,也未找到DefaultChallengeScheme.
InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.
我还阅读了其他有关使用身份验证而不是授权的stackoverflow文章,但实际上,此策略授权"更适合于此目的.因此,我正在寻找有关如何防止这种异常的想法.
I have read other stackoverflow articles about using Authentication rather than Authorization, but really this policy Authorization is the better fit for purpose. So I am looking for ideas on how to prevent this exception.
推荐答案
有趣的是,我认为这只是身份验证,没有任何授权(至少不是您的问题) ).您当然想认证客户端,但是您似乎没有任何授权要求.身份验证是确定谁发出请求的过程,而授权是确定我们的请求者可以做什么的过程(更多此处).
Interestingly, I think this is just authentication, without any authorisation (at least not in your question). You certainly want to authenticate the client but you don't appear to have any authorisation requirements. Authentication is the process of determining who is making this request and authorisation is the process of determining what said requester can do once we know who it is (more here). You've indicated that you want to return a 401
(bad credentials) rather than a 403
(unauthorised), which I believe highlights the difference (more here).
为了在ASP.NET Core中使用您自己的身份验证逻辑,您可以编写自己的AuthenticationHandler
,它负责接收请求并确定
In order to use your own authentication logic in ASP.NET Core, you can write your own AuthenticationHandler
, which is responsible for taking a request and determining the User
. Here's an example for your situation:
public class ClientTokenHandler : AuthenticationHandler<ClientTokenOptions>
{
private readonly string[] _clientTokens;
public ClientTokenHandler(IOptionsMonitor<ClientTokenOptions> optionsMonitor,
ILoggerFactory loggerFactory, UrlEncoder urlEncoder, ISystemClock systemClock,
IConfiguration config)
: base(optionsMonitor, loggerFactory, urlEncoder, systemClock)
{
_clientTokens = config.GetSection("ClientTokens").Get<string[]>();
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var tokenHeaderValue = (string)Request.Headers["X-TOKEN"];
if (string.IsNullOrWhiteSpace(tokenHeaderValue))
return Task.FromResult(AuthenticateResult.NoResult());
if (!_clientTokens.Contains(tokenHeaderValue))
return Task.FromResult(AuthenticateResult.Fail("Unknown Client"));
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(
Enumerable.Empty<Claim>(),
Scheme.Name));
var authenticationTicket = new AuthenticationTicket(claimsPrincipal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(authenticationTicket));
}
}
以下是HandleAuthenticateAsync
中发生的情况的描述:
Here's a description of what's going on in HandleAuthenticateAsync
:
- 从请求中检索标头
X-TOKEN
.如果这是无效的,则表明我们无法验证请求(稍后会对此进行详细介绍). - 将从
X-TOKEN
标头中检索到的值与已知的客户端令牌列表进行比较.如果不成功,则表明身份验证失败(我们不知道这是谁-稍后还将对此进行详细介绍.) - 当客户端令牌与
X-TOKEN
请求标头匹配时,我们将创建一个新的AuthenticationTicket
/ClaimsPrincipal
/ClaimsIdentity
组合.这是我们对User
的表示-如果要将其他信息与客户相关联,则可以包含自己的Claim
而不是使用Enumerable.Empty<Claim>()
.
- The header
X-TOKEN
is retrieved from the request. If this is invalid, we indicate that we are unable to authenticate the request (more on this later). - The value retrieved from the
X-TOKEN
header is compared against a known list of client-tokens. If this is unsuccessful, we indicate that authentication failed (we don't know who this is - more on this later too). - When a client-token matches the
X-TOKEN
request header, we create a newAuthenticationTicket
/ClaimsPrincipal
/ClaimsIdentity
combo. This is our representation of theUser
- you can include your ownClaim
s instead of usingEnumerable.Empty<Claim>()
if you want to associate additional information with the client.
您应该可以在大部分情况下保持原样,并且进行一些更改(我已经简化为既要简短回答又要填补问题中的空白):
You should be able to use this as-is for the most part, with a few changes (I've simplified to both keep the answer short and fill in a few gaps from the question):
- 构造函数将
IConfiguration
的实例作为最终参数,然后将其用于从本例中的appsettings.json
中读取string[]
.您可能会以不同的方式执行此操作,因此您可以根据需要使用DI注入当前正在此处使用的内容. - 我已经将
X-TOKEN
硬编码为提取令牌时要使用的标头名称.您可能会为此使用一个不同的名称,并且从您的问题中可以看出您没有对它进行硬编码,这更好.
- The constructor takes an instance of
IConfiguration
as the final parameter, which is then used to read astring[]
from, in my example,appsettings.json
. You are likely doing this differently, so you can just use DI to inject whatever it is you're currently using here, as needed. - I've hardcoded
X-TOKEN
as the header name to use when extracting the token. You'll likely be using a different name for this yourself and I can see from your question that you're not hardcoding it, which is better.
关于此实现的另一点注意事项是同时使用AuthenticateResult.NoResult()
和AuthenticateResult.Fail(...)
.前者指示我们没有足够的信息来执行身份验证,而后者指示我们拥有了所需的一切,但身份验证失败.对于像您这样的简单设置,我认为如果愿意,在两种情况下都可以使用Fail
.
One other thing to note about this implementation is the use of both AuthenticateResult.NoResult()
and AuthenticateResult.Fail(...)
. The former indicates that we did not have enough information in order to perform the authentication and the latter indicates that we had everything we needed but the authentication failed. For a simple setup like yours, I think you'd be OK using Fail
in both cases if you'd prefer.
您需要的第二件事是ClientTokenOptions
类,该类在上面的AuthenticationHandler<ClientTokenOptions>
中使用.对于此示例,这是单线的:
The second thing you'll need is the ClientTokenOptions
class, which is used above in AuthenticationHandler<ClientTokenOptions>
. For this example, this is a one-liner:
public class ClientTokenOptions : AuthenticationSchemeOptions { }
这用于配置AuthenticationHandler
-随时将某些配置移到此处(例如,上面的_clientTokens).这还取决于您希望它具有怎样的可配置性和可重用性-作为另一个示例,您可以在此处定义标头名称,但这取决于您.
This is used for configuring your AuthenticationHandler
- feel free to move some of the configuration into here (e.g. the _clientTokens from above). It also depends on how configurable and reusable you want this to be - as another example, you could define the header name in here, but that's up to you.
最后,要使用ClientTokenHandler
,您需要在ConfigureServices
中添加以下内容:
Lastly, to use your ClientTokenHandler
, you'll need to add the following to ConfigureServices
:
services.AddAuthentication("ClientToken")
.AddScheme<ClientTokenOptions, ClientTokenHandler>("ClientToken", _ => { });
在这里,我们只是根据自己的自定义ClientToken
方案将ClientTokenHandler
注册为AuthenticationHandler
.我不会在这里像这样对"ClientToken"
进行硬编码,但是,再次,这只是一种简化.最后,时髦的_ => { }
是一个回调,给了ClientTokenOptions
实例以进行修改:我们在这里不需要它,因此实际上它只是一个空的lambda.
Here, we're just registering ClientTokenHandler
as an AuthenticationHandler
under our own custom ClientToken
scheme. I wouldn't hardcode "ClientToken"
here like this, but, again, this is just a simplification. The funky _ => { }
at the end is a callback that is given an instance of ClientTokenOptions
to modify: we don't need that here, so it's just an empty lambda, effectively.
InvalidOperationException:未指定authenticationScheme,也未找到DefaultChallengeScheme.
InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.
您的错误消息中的"DefaultChallengeScheme"现已通过上面对services.AddAuthentication("ClientToken")
的调用进行了设置("ClientToken"是方案名称).
The "DefaultChallengeScheme" in your error message has now been set with the call to services.AddAuthentication("ClientToken")
above ("ClientToken" is the scheme name).
如果要使用这种方法,则需要删除ClientTokenRequirement
内容.您可能还会发现浏览Barry Dorrans的 BasicAuthentication 项目很有趣-它遵循相同的模式作为官方的ASP.NET Core AuthenticationHandler
,但入门更简单.如果您不关心可配置性和可重用性方面,那么我提供的实现应该适合您的目的.
If you want to go with this approach, you'll need to remove your ClientTokenRequirement
stuff. You might also find it interesting to have a look through Barry Dorrans's BasicAuthentication project - it follows the same patterns as the official ASP.NET Core AuthenticationHandler
s but is simpler for getting started. If you're not concerned about the configurability and reusability aspects, the implementation I've provided should be fit for purpose.
这篇关于如何在.NET Core中正确设置WEB API的策略授权的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!