IdentityServer4使用ApiKey或Basic身份验证直接到API

2024-01-04

我正在使用 IdentityServer4 让我的客户通过 JavaScript 登录并访问网页和 api,并且运行良好。然而,有一个新的要求,而不是使用用户名和密码从身份服务器获取访问令牌,然后使用它通过承载身份验证访问 api...我需要使用“Basic”直接调用 api身份验证标头和 api 将与身份服务器确认身份。类似于下面用于访问 ZenDesk api 的代码...

        using (var client = new HttpClient())
        {
            var username = _configuration["ZenDesk:username"];
            var password = _configuration["ZenDesk:password"];
            var token = Convert.ToBase64String(Encoding.ASCII.GetBytes(username + ":" + password));
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", token);

            var response = client.PostAsync("https://...

对我如何实施这个有任何帮助吗? IdentityServer4 中是否内置了任何可以适应这种方法的东西?我将 .Net Core 3.1 用于 api 服务器和身份服务器。

另一种(看似常见)方法是为每个用户生成一个 api 密钥,然后允许用户像这样调用 api...

using (var client = new HttpClient())
{
    client.BaseAddress = new Uri(URL_HOST_API);
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("ApiKey", "123456456123456789");
…
}

想法?


事实证明,IdentityServer4 没有内置对 ApiKeys 的支持...但是 .Net Core 3.1 有 IAuthorizationHandler,它允许您为 ApiKeys 滚动自己的授权,并将其插入到具有依赖注入的流程中。

我这样做的方式是......拥有一个 ApiKey 和一个 ApiKeySecret。这样,UserId 就不会被公开...我的 IdentityServer4(服务器 C)上有一个名为 ApiKey 的数据库表,其中包含字段(ApiKeyId、UserId、ApiKey 和 ApiKeySecret)...ApiKeySecret 是一种单向哈希就像密码一样。

我向我的 IdentityServer4 项目(服务器 C)添加了一个 ApiKeyController...这将允许 ApiRequest 验证 ApiKeys。

所以...遵循流程:

服务器A:第三方.Net Core 3.1 Web服务器

服务器 B:MyApiServer .Net Core 3.1 Web 服务器

服务器 C:MyIdentityerServer4 .Net Core 3.1 IndentityServer4

基于对服务器 A 的请求(可能来自浏览器)。

然后,服务器 A 使用标头中的 Api Key 和 Api Key Secret 调用我的 API(服务器 B):

using (var client = new HttpClient())
{
    var url = _configuration["MyApiUrl"] + "/WeatherForecast";
    var apiKey = _configuration["MyApiKey"];
    var apiKeySecret = _configuration["MyApiKeySecret"];
    client.DefaultRequestHeaders.Add("x-api-key", apiKey);
    client.DefaultRequestHeaders.Add("secret-api-key", apiKeySecret);

    var response = client.GetAsync(url).Result;
    if (response.IsSuccessStatusCode)
    {
        var contents = response.Content.ReadAsStringAsync().Result;
        return contents;
    }
    return "StatusCode = " + response.StatusCode;
}

在我的 API 服务器(服务器 B)上,我添加了以下类,如果为 url 设置了 [Authorize] 类别,它将通过调用 IdentityServer4(服务器 C)上的 ApiKeyController 并返回来验证标头中的 ApiKeys HttpContext.Items 集合上的值 (UserId)。

基本上,系统已经为(我相信)services.AddAuthentication("Bearer") 定义了一个 IAuthorizationHandler...所以当添加第二个(或更多)时...它们将被调用,如果一个返回 Succeeded 则不再有调用...如果它们都失败,那么[授权]将失败。

public class ApiKeyAuthorizationHandler : IAuthorizationHandler
{
    private readonly ILogger<ApiKeyAuthorizationHandler> _logger;
    private readonly IConfiguration _configuration;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ApiKeyAuthorizationHandler(
        ILogger<ApiKeyAuthorizationHandler> logger,
        IConfiguration configuration,
        IHttpContextAccessor httpContextAccessor
        )
    {
        _logger = logger;
        _configuration = configuration;
        _httpContextAccessor = httpContextAccessor;
    }

    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        try
        {
            string apiKey = _httpContextAccessor.HttpContext.Request.Headers["x-api-key"].FirstOrDefault();
            string apiKeySecret = _httpContextAccessor.HttpContext.Request.Headers["secret-api-key"].FirstOrDefault();

            if (apiKey != null && apiKeySecret != null)
            {
                if (Authorize(apiKey, apiKeySecret))
                    SetSucceeded(context);
            }
            return Task.CompletedTask;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "HandleAsync");
            return Task.CompletedTask;
        }
    }

    public class ValidateResponse
    {
        public string UserId { get; set; }
    }
    private bool Authorize(string apiKey, string apiKeySecret)
    {
        try
        {
            using (var client = new HttpClient())
            {
                var url = _configuration["AuthorizationServerUrl"] + "/api/ApiKey/Validate";
                var json = JsonConvert.SerializeObject(new
                {
                    clientId = "serverb-api", // different ApiKeys for different clients
                    apiKey = apiKey,
                    apiKeySecret = apiKeySecret
                });
                var response = client.PostAsync(url, new StringContent(json, Encoding.UTF8, "application/json")).Result;
                if (response.IsSuccessStatusCode)
                {
                    var contents = response.Content.ReadAsStringAsync().Result;
                    var result = JsonConvert.DeserializeObject<ValidateResponse>(contents);
                    _httpContextAccessor.HttpContext.Items.Add("UserId", result.UserId);
                }
                return response.IsSuccessStatusCode;
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Authorize");
            return false;
        }
    }

    private void SetSucceeded(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();
        foreach (var requirement in pendingRequirements)
        {
            context.Succeed(requirement);
        }
    }
}

我还需要将以下内容添加到服务器 B 上的 Startup.cs 中:

services.AddSingleton<IAuthorizationHandler, ApiKeyAuthorizationHandler>();

为了完整起见,我在 IdentityServer4(服务器 C)上的代码:

ApiKeyController.cs

using System;
using MyIdentityServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace MyIdentityServer
{
    [Route("api/[controller]")]
    [ApiController]
    public class ApiKeyController : ControllerBase
    {
        private readonly ILogger<ApiKeyController> _logger;

        private readonly IApiKeyService _apiKeyService;
        public ApiKeyController(
            IApiKeyService apiKeyService,
            ILogger<ApiKeyController> logger
            )
        {
            _apiKeyService = apiKeyService;
            _logger = logger;
        }
        public class ValidateApiKeyRequest
        {
            public string ClientId { get; set; }
            public string ApiKey { get; set; }
            public string ApiKeySecret { get; set; }
        }
        [HttpPost("Validate")]
        [AllowAnonymous]
        [Consumes("application/json")]
        public IActionResult PostBody([FromBody] ValidateApiKeyRequest request)
        {
            try
            {
                (var clientId, var userId) = _apiKeyService.Verify(request.ApiKey, request.ApiKeySecret);

                if (request.ClientId == clientId && userId != null)
                    return Ok(new { UserId = userId });
                    // return new JsonResult(new { UserId = userId }); // maybe also return claims for client / user

                return Unauthorized();
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "HandleValidateApiKey apiKey={request.ApiKey} apiKeySecret={request.ApiKeySecret}");
                return Unauthorized();
            }
        }

        public class GenerateApiKeyRequest
        {
            public string ClientId { get; set; }
            public string UserId { get; set; }
        }
        [HttpPost("Generate")]
        [AllowAnonymous]
        public IActionResult Generate(GenerateApiKeyRequest request)
        {
            // generate and store in database
            (var apiKey, var apiKeySecret) = _apiKeyService.Generate(request.ClientId, request.UserId);

            return new JsonResult(new { ApiKey = apiKey, ApiKeySecret = apiKeySecret });
        }

    }
}

ApiKeyService.cs

using Arch.EntityFrameworkCore.UnitOfWork;
using EQIdentityServer.Data.Models;
using System;
using System.Security.Cryptography;

public namespace MyIndentityServer4.Services

public interface IApiKeyService
{
    (string, string) Verify(string apiKey, string apiKeySecret);
    (string, string) Generate(string clientId, string userId);
}

public class ApiKeyService : IApiKeyService
{
    IUnitOfWork _unitOfWork;

    public ApiKeyService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public (string, string) Verify(string apiKey, string apiKeySecret)
    {
        var repoApiKey = _unitOfWork.GetRepository<ClientUserApiKey>();

        var item = repoApiKey.GetFirstOrDefault(predicate: p => p.ApiKey == apiKey);
        if (item == null)
            return (null, null);

        if (!OneWayHash.Verify(item.ApiKeySecretHash, apiKeySecret))
            return (null, null);

        return (item?.ClientId, item?.UserId);
    }

    public (string, string) Generate(string clientId, string userId)
    {
        var repoApiKey = _unitOfWork.GetRepository<ClientUserApiKey>();

        string apiKey = null;
        string apiKeySecret = null;
        string apiKeySecretHash = null;

        var key = new byte[30];
        using (var generator = RandomNumberGenerator.Create())
            generator.GetBytes(key);
        apiKeySecret = Convert.ToBase64String(key);
            
        apiKeySecretHash = OneWayHash.Hash(apiKeySecret);

        var item = repoApiKey.GetFirstOrDefault(
            predicate: p => p.ClientId == clientId && p.UserId == userId
            );
        if (item != null)
        {
            // regenerate only secret for existing clientId/userId
            apiKey = item.ApiKey; // item.ApiKey = apiKey; // keep this the same, or you could have multiple for a clientId if you want
            item.ApiKeySecretHash = apiKeySecretHash;
            repoApiKey.Update(item);
        }
        else
        {
            // new for user
            key = new byte[30];

            while (true)
            {
                using (var generator = RandomNumberGenerator.Create())
                    generator.GetBytes(key);
                apiKey = Convert.ToBase64String(key);

                var existing = repoApiKey.GetFirstOrDefault(
                    predicate: p => p.ApiKey == apiKey
                    );

                if (existing == null)
                    break;
            }

            item = new ClientUserApiKey() { ClientId = clientId, UserId = userId, ApiKey = apiKey, ApiKeySecretHash = apiKeySecretHash };
            repoApiKey.Insert(item);
        }
        _unitOfWork.SaveChanges();

        return (apiKey, apiKeySecret);
    }        
}

我的型号:

public class ClientUserApiKey
{
    public long ClientUserApiKeyId { get; set; }

    [IndexColumn("IX_ApiKey_ClientIdUserId", 0)]
    public string ClientId { get; set; }

    [IndexColumn("IX_ApiKey_ClientIdUserId", 1)]
    public string UserId { get; set; }

    [IndexColumn]
    public string ApiKey { get; set; }

    [StringLength(128)]
    public string ApiKeySecretHash { get; set; }
}

然后,我的 WeatherForecastController 可以通过以下两种方式之一获取登录用户...通过 Bearer access_token 或我的 ApiKeys:

        string userId = null;
        if (User?.Identity.IsAuthenticated == true)
            userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier).Value;
        else
            userId = this.HttpContext.Items["UserId"]?.ToString(); // this comes from ApiKey validation
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

IdentityServer4使用ApiKey或Basic身份验证直接到API 的相关文章

  • 检查两个数是否是彼此的排列?

    给定两个数字 a b 使得 1 例如 123 是 312 的有效排列 我也不想对数字中的数字进行排序 如果您指的是数字的字符 例如 1927 和 9721 则 至少 有几种方法 如果允许排序 一种方法是简单地sprintf将它们放入两个缓冲
  • Qt-Qlist 检查包含自定义类

    有没有办法覆盖加载自定义类的 Qt QList 的比较机制 即在 java 中你只需要重写一个比较方法 我有一个带有我的自定义类模型的 QList QList
  • 获取按下的按钮的返回值

    我有一个在特定事件中弹出的表单 它从数组中提取按钮并将标签值设置为特定值 因此 如果您要按下或单击此按钮 该函数应返回标签值 我怎样才能做到这一点 我如何知道点击了哪个按钮 此时代码返回 DialogResult 但我想从函数返回 Tag
  • 将数组向左或向右旋转一定数量的位置,复杂度为 o(n)

    我想编写一个程序 根据用户的输入 正 gt 负 include
  • pthread_cond_timedwait() 和 pthread_cond_broadcast() 解释

    因此 我在堆栈溢出和其他资源上进行了大量搜索 但我无法理解有关上述函数的一些内容 具体来说 1 当pthread cond timedwait 因为定时器值用完而返回时 它如何自动重新获取互斥锁 互斥锁可能被锁定在其他地方 例如 在生产者
  • UML类图:抽象方法和属性是这样写的吗?

    当我第一次为一个小型 C 项目创建 uml 类图时 我在属性方面遇到了一些麻烦 最后我只是将属性添加为变量 lt
  • 如何将图像和 POST 数据上传到 Azure 移动服务 ApiController 终结点?

    我正在尝试上传图片and POST表单数据 尽管理想情况下我希望它是json 到我的端点Azure 移动服务应用 我有ApiController method HttpPost Route api upload databaseId sea
  • 将目录压缩为单个文件的方法有哪些

    不知道怎么问 所以我会解释一下情况 我需要存储一些压缩文件 最初的想法是创建一个文件夹并存储所需数量的压缩文件 并创建一个文件来保存有关每个压缩文件的数据 但是 我不被允许创建许多文件 只能有一个 我决定创建一个压缩文件 其中包含有关进一步
  • WPF TabControl,用C#代码更改TabItem的背景颜色

    嗨 我认为这是一个初学者的问题 我搜索了所有相关问题 但所有这些都由 xaml 回答 但是 我需要的是后台代码 我有一个 TabControl 我需要设置其项目的背景颜色 我需要在选择 取消选择和悬停时为项目设置不同的颜色 非常感谢你的帮助
  • Web API - 访问 DbContext 类中的 HttpContext

    在我的 C Web API 应用程序中 我添加了CreatedDate and CreatedBy所有表中的列 现在 每当在任何表中添加新记录时 我想填充这些列 为此目的我已经覆盖SaveChanges and SaveChangesAsy
  • 指针减法混乱

    当我们从另一个指针中减去一个指针时 差值不等于它们相距多少字节 而是等于它们相距多少个整数 如果指向整数 为什么这样 这个想法是你指向内存块 06 07 08 09 10 11 mem 18 24 17 53 7 14 data 如果你有i
  • 使用 System.Text.Json 即时格式化 JSON 流

    我有一个未缩进的 Json 字符串 例如 hash 123 id 456 我想缩进字符串并将其序列化为 JSON 文件 天真地 我可以使用缩进字符串Newtonsoft如下 using Newtonsoft Json Linq JToken
  • 如何返回 json 结果并将 unicode 字符转义为 \u1234

    我正在实现一个返回 json 结果的方法 例如 public JsonResult MethodName Guid key var result ApiHelper GetData key Data is stored in db as v
  • 从库中捕获主线程 SynchronizationContext 或 Dispatcher

    我有一个 C 库 希望能够将工作发送 发布到 主 ui 线程 如果存在 该库可供以下人员使用 一个winforms应用程序 本机应用程序 带 UI 控制台应用程序 没有 UI 在库中 我想在初始化期间捕获一些东西 Synchronizati
  • C++ 复制初始化和直接初始化,奇怪的情况

    在继续阅读本文之前 请阅读在 C 中 复制初始化和直接初始化之间有区别吗 https stackoverflow com questions 1051379 is there a difference in c between copy i
  • 插入记录后如何从SQL Server获取Identity值

    我在数据库中添加一条记录identity价值 我想在插入后获取身份值 我不想通过存储过程来做到这一点 这是我的代码 SQLString INSERT INTO myTable SQLString Cal1 Cal2 Cal3 Cal4 SQ
  • 控制到达非 void 函数末尾 -wreturn-type

    这是查找四个数字中的最大值的代码 include
  • C - 直接从键盘缓冲区读取

    这是C语言中的一个问题 如何直接读取键盘缓冲区中的数据 我想直接访问数据并将其存储在变量中 变量应该是什么数据类型 我需要它用于我们研究所目前正在开发的操作系统 它被称为 ICS OS 我不太清楚具体细节 它在 x86 32 位机器上运行
  • mysql-connector-c++ - “get_driver_instance”不是“sql::mysql”的成员

    我是 C 的初学者 我认为学习的唯一方法就是接触一些代码 我正在尝试构建一个连接到 mysql 数据库的程序 我在 Linux 上使用 g 没有想法 我运行 make 这是我的错误 hello cpp 38 error get driver
  • 防止索引超出范围错误

    我想编写对某些条件的检查 而不必使用 try catch 并且我想避免出现 Index Out of Range 错误的可能性 if array Element 0 Object Length gt 0 array Element 1 Ob

随机推荐

  • 如何使用ReportingService2010?

    我正在尝试使用报告服务器 Web 服务通过代码部署报告服务器解决方案 http 服务器名称 ReportServer ReportService2010 asmx wsdl http Server Name ReportServer Rep
  • 从 mongodb 数组中删除元素

    我是 mongodb 的新手 我想删除数组中的某些元素 我的文件如下 id ObjectId 4d525ab2924f0000000022ad name hello time stamp 2010 07 01T12 01 03 75 02
  • 覆盖 matplotlib 的平移工具 (wx)

    我正在使用 wxPython 面板中的 matplotlib 来执行一些繁重的绘图 我的问题是在使用本机平移工具时出现的 当您拖动平移手柄时 似乎 matplotlib 试图不断重绘画布 随着我绘制的数据量变得非常不稳定 已经使用数据集合等
  • 使用 AND 和 OR 运算符的 Solr 字段查询 (fq)

    我一直在努力使用 AND 和 OR 运算符形成 solr 字段查询 为什么 solr 对于 1 和 2 3 和 4 查询返回不同的结果 即使所有查询都有相同的逻辑 fq 名称 abc AND 城市 1 3 OR 名称 abc AND loc
  • 无法访问developer.apple.com 中的证书、标识符和配置文件

    我正在尝试遵循以下内容有效的配置文件均不包含设备 https stackoverflow com questions 28585821 none of the valid provisioning profiles include the
  • tkinter GUI 写入文本小部件并替换文本

    from tkinter import from tkinter import ttk parent Tk p ttk Panedwindow parent orient HORIZONTAL p pack fill BOTH expand
  • 无法访问已处置的对象。对象名称:FileBufferingReadStream

    我正在尝试在天蓝色中上传文件 所以基本上我正在尝试将文件转换为流 以便我可以在服务器上创建一个文件并写入其数据 public async Task UploadFileOnAzure string path string name IFor
  • 从外部文件调用 jQuery 日期选择器

    我对 jQuery 比较陌生 并且很难让 jQuery 日期选择器从外部 js 文件工作 最初 我将脚本创建为函数 但认为这样做会限制范围 并且无法在函数外部访问它 我还尝试将其定义为函数 并命名该函数 然后使用 document read
  • 允许用户在 iOS 应用程序内切换应用程序跟踪同意状态

    如何使用户能够从菜单中轻松切换同意inside具有新的应用程序跟踪透明度要求的实时 iOS 应用程序 我曾经通过复选标记按钮来跟踪用户同意情况 而无需 Apple 的 ATT 要求 我还可以使用此同意状态吗 我可以通过网络视图跟踪同意吗 T
  • 时差节目

    我正在使用以下函数来计算时差 它没有显示正确的输出 1 个月的时差后显示 2 分钟的差异 我的程序出了什么问题 public String TimestampDiff Timestamp t long t1 t getTime String
  • 打开新窗口时,Visual Studio 中的 Silverlight 调试停止工作

    我有一个托管 silverlight 应用程序的 aspx 页面 调试工作正常 当我直接转到页面时 silverlight 应用程序中的断点会被命中 但是当我从其托管 ASP NET 应用程序在新窗口中启动页面时 断点将停止被命中 有任何想
  • Zend_db 更新更好的错误报告

    当我更新记录时 我使用 更新 的结果来确定它是否正常工作 a this gt db gt insert self TABLE saveData a 1 表示更新了一条记录 a 0 表示它没有更新任何内容 如果表格没有任何改变 我可以得到 0
  • C 和 C++ 中的声明/定义作为语句

    当这不能在 C 中编译时我很困惑 int main for int i 0 i lt 4 i int a 5 A dependent statement may not be declaration return 0 我习惯了 C 来编译它
  • 为什么我使用 UpdateSourceTrigger=PropertyChanged ,TwoWay 还不够?

    你好 有源和目标文本框 txttarget 与 txtsource 绑定 当在 txtsource 中写入内容时 txttarget 发生了变化 一切都很好 但是在 txttarget 上写入时 我没有看到 txttarget 有任何变化
  • 四开不渲染

    我的四开文档突然停止渲染 我已重新安装 RStudio 和 quarto 软件包 但此问题仍然存在 当我打开新的四开文档时 我不再在新文档中看到样板材料 该文档是空的 就像我选择了一样Create Empty Document 但我没有 然
  • Spyder默认模块导入列表

    我正在尝试设置一个稍微定制的 Spyder 版本 当 Spyder 启动时 它会自动导入一长串模块 包括来自 matplotlib numpy scipy 等的模块 有没有办法将我自己的模块添加到该列表中 为了以防万一 我使用 Python
  • Linux:杀死后台任务

    如何终止 Linux 中最后生成的后台任务 Example doSomething doAnotherThing doB doC doD kill doB 你可以通过工作号码杀人 当您将任务置于后台时 您会看到类似以下内容的内容 scrip
  • 有多少内存使用字节枚举,这是否优化了 C# 中的内存/速度?

    在 C 中使用字节枚举进行小型枚举是一种好处还是一种好的做法 这会减少内存使用吗 这能提高速度吗 Example enum Fruits byte Apple Orange Banana 相比之下有什么优势 enum Fruits Appl
  • iPad Pro 图标和启动图像

    我正在开发一个基于 adobe air 的 iOS 应用程序 我需要知道最近为 iPad Pro 添加的资源的名称 启动图像 2048 x 2732 和图标 167x167 不幸的是 根据以前的名字很难猜出他们的名字 我找到 电子邮件受保护
  • IdentityServer4使用ApiKey或Basic身份验证直接到API

    我正在使用 IdentityServer4 让我的客户通过 JavaScript 登录并访问网页和 api 并且运行良好 然而 有一个新的要求 而不是使用用户名和密码从身份服务器获取访问令牌 然后使用它通过承载身份验证访问 api 我需要使