zoukankan      html  css  js  c++  java
  • 使用请求头认证来测试需要授权的 API 接口

    使用请求头认证来测试需要授权的 API 接口

    Intro

    有一些需要认证授权的接口在写测试用例的时候一般会先获取一个 token,然后再去调用接口,其实这样做的话很不灵活,一方面是存在着一定的安全性问题,获取 token 可能会有一些用户名密码之类的测试数据,还有就是获取 token 的话如果全局使用同一个 token 会很不灵活,如果我要测试没有用户信息的话还比较简单,我可以不传递 token,如果token里有两个角色,我要测试另外一个角色的时候,只能给这个测试用户新增一个角色然后再获取token,这样就很不灵活,于是我就尝试把之前写的自定义请求头认证的代码,整理了一下,集成到了一个 nuget 包里以方便其他项目使用,nuget 包是 WeihanLi.Web.Extensions,源代码在这里 https://github.com/WeihanLi/WeihanLi.Web.Extensions 有想自己改的可以直接拿去用,目前提供了基于请求头的认证和基于 QueryString 的认证两种认证方式。

    实现效果

    基于请求头动态配置用户的信息,需要什么样的信息就在请求头中添加什么信息,示例如下:

    再来看个单元测试的示例:

    [Fact]
    public async Task MakeReservationWithUserInfo()
    {
        using var request = new HttpRequestMessage(HttpMethod.Post, "/api/reservations");
    
        request.Headers.TryAddWithoutValidation("UserId", GuidIdGenerator.Instance.NewId()); // 用户Id
        request.Headers.TryAddWithoutValidation("UserName", Environment.UserName); // 用户名
        request.Headers.TryAddWithoutValidation("UserRoles", "User,ReservationManager"); //用户角色
    
        request.Content = new StringContent($@"{{""reservationUnit"":""nnnnn"",""reservationActivityContent"":""13211112222"",""reservationPersonName"":""谢谢谢"",""reservationPersonPhone"":""13211112222"",""reservationPlaceId"":""f9833d13-a57f-4bc0-9197-232113667ece"",""reservationPlaceName"":""第一多功能厅"",""reservationForDate"":""2020-06-13"",""reservationForTime"":""10:00~12:00"",""reservationForTimeIds"":""1""}}", Encoding.UTF8, "application/json");
    
        using var response = await Client.SendAsync(request);
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
    

    实现原理解析

    实现原理其实挺简单的,就是实现了一种基于 header 的自定义认证模式,从 header 中获取用户信息并进行认证,核心代码如下:

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (await Options.AuthenticationValidator(Context))
        {
            var claims = new List<Claim>();
            if (Request.Headers.TryGetValue(Options.UserIdHeaderName, out var userIdValues))
            {
                claims.Add(new Claim(ClaimTypes.NameIdentifier, userIdValues.ToString()));
            }
            if (Request.Headers.TryGetValue(Options.UserNameHeaderName, out var userNameValues))
            {
                claims.Add(new Claim(ClaimTypes.Name, userNameValues.ToString()));
            }
            if (Request.Headers.TryGetValue(Options.UserRolesHeaderName, out var userRolesValues))
            {
                var userRoles = userRolesValues.ToString()
                    .Split(new[] { Options.Delimiter }, StringSplitOptions.RemoveEmptyEntries);
                claims.AddRange(userRoles.Select(r => new Claim(ClaimTypes.Role, r)));
            }
    
            if (Options.AdditionalHeaderToClaims.Count > 0)
            {
                foreach (var headerToClaim in Options.AdditionalHeaderToClaims)
                {
                    if (Request.Headers.TryGetValue(headerToClaim.Key, out var headerValues))
                    {
                        foreach (var val in headerValues.ToString().Split(new[] { Options.Delimiter }, StringSplitOptions.RemoveEmptyEntries))
                        {
                            claims.Add(new Claim(headerToClaim.Value, val));
                        }
                    }
                }
            }
    
            // claims identity 's authentication type can not be null https://stackoverflow.com/questions/45261732/user-identity-isauthenticated-always-false-in-net-core-custom-authentication
            var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, Scheme.Name));
            var ticket = new AuthenticationTicket(
                principal,
                Scheme.Name
            );
            return AuthenticateResult.Success(ticket);
        }
    
        return AuthenticateResult.NoResult();
    }
    

    其实就是将请求头的信息读取到 Claims,然后返回一个 ClaimsPrincipalAuthenticationTicket,在读取 header 之前有一个 AuthenticationValidator 是用来验证请求是不是满足使用 Header 认证,是一个基于 HttpContext 的断言委托(Func<HttpContext, Task<bool>>),默认实现是验证是否有 UserId 对应的 Header,如果要修改可以通过 Startup 来配置

    使用示例

    Startup 配置,和其它的认证方式一样,Header 认证和 Query 认证也提供了基于 AuthenticationBuilder 的扩展,只需要在 services.AddAuthentication() 后增加 Header 认证的模式即可,示例如下:

    
    services.AddAuthentication(HeaderAuthenticationDefaults.AuthenticationSchema)
        .AddQuery(options =>
        {
            options.UserIdQueryKey = "uid";
        })
        .AddHeader(options =>
        {
            options.UserIdHeaderName = "X-UserId";
            options.UserNameHeaderName = "X-UserName";
            options.UserRolesHeaderName = "X-UserRoles";
        });
    

    默认的 Header 是 UserId/UserName/UserRoles,你也可以自定义为符合自己需要的配置,如果只是想新增一个转换可以配置 AdditionalHeaderToClaims 增加自己需要的请求头 => Claims 转换,AuthenticationValidator 也可以自定义,就是上面提到的会首先会验证是不是需要读取 Header,验证通过之后才会读取 Header 信息并认证

    测试示例

    有一个接口我需要登录之后才能访问,需要用户信息,类似下面这样

    [HttpPost]
    [Authorize]
    public async Task<IActionResult> MakeReservation(
        [FromBody] ReservationViewModel model
        )
    {
        // ...
    }
    

    在测试代码里我配置使用了 Header 认证,在请求的时候直接通过 Header 来控制用户的信息

    Startup 配置:

    services
        .AddAuthentication(HeaderAuthenticationDefaults.AuthenticationSchema)
        .AddHeader()
        // 使用 Query 认证
        //.AddAuthentication(QueryAuthenticationDefaults.AuthenticationSchema)
        //.AddQuery()
        ;
    

    测试代码:

    [Fact]
    public async Task MakeReservationWithUserInfo()
    {
        using var request = new HttpRequestMessage(HttpMethod.Post, "/api/reservations");
        request.Headers.TryAddWithoutValidation("UserId", GuidIdGenerator.Instance.NewId());
        request.Headers.TryAddWithoutValidation("UserName", Environment.UserName);
        request.Headers.TryAddWithoutValidation("UserRoles", "User,ReservationManager");
    
        request.Content = new StringContent($@"{{""reservationUnit"":""nnnnn"",""reservationActivityContent"":""13211112222"",""reservationPersonName"":""谢谢谢"",""reservationPersonPhone"":""13211112222"",""reservationPlaceId"":""f9833d13-a57f-4bc0-9197-232113667ece"",""reservationPlaceName"":""第一多功能厅"",""reservationForDate"":""2020-06-13"",""reservationForTime"":""10:00~12:00"",""reservationForTimeIds"":""1""}}", Encoding.UTF8, "application/json");
    
        using var response = await Client.SendAsync(request);
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
    
    [Fact]
    public async Task MakeReservationWithInvalidUserInfo()
    {
        using var request = new HttpRequestMessage(HttpMethod.Post, "/api/reservations");
    
        request.Headers.TryAddWithoutValidation("UserName", Environment.UserName);
    
        request.Content = new StringContent($@"{{""reservationUnit"":""nnnnn"",""reservationActivityContent"":""13211112222"",""reservationPersonName"":""谢谢谢"",""reservationPersonPhone"":""13211112222"",""reservationPlaceId"":""f9833d13-a57f-4bc0-9197-232113667ece"",""reservationPlaceName"":""第一多功能厅"",""reservationForDate"":""2020-06-13"",""reservationForTime"":""10:00~12:00"",""reservationForTimeIds"":""1""}}", Encoding.UTF8, "application/json");
    
        using var response = await Client.SendAsync(request);
        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
    }
    
    [Fact]
    public async Task MakeReservationWithoutUserInfo()
    {
        using var request = new HttpRequestMessage(HttpMethod.Post, "/api/reservations")
        {
            Content = new StringContent(
                @"{""reservationUnit"":""nnnnn"",""reservationActivityContent"":""13211112222"",""reservationPersonName"":""谢谢谢"",""reservationPersonPhone"":""13211112222"",""reservationPlaceId"":""f9833d13-a57f-4bc0-9197-232113667ece"",""reservationPlaceName"":""第一多功能厅"",""reservationForDate"":""2020-06-13"",""reservationForTime"":""10:00~12:00"",""reservationForTimeIds"":""1""}",
                Encoding.UTF8, "application/json")
        };
    
        using var response = await Client.SendAsync(request);
        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
    }
    

    More

    QueryString 认证和请求头认证是类似的,这里就不再赘述,只是把请求头上的参数转移到 QueryString 上了,觉得不够好用的可以直接 Github 上找源码修改, 也欢迎 PR,源码地址: https://github.com/WeihanLi/WeihanLi.Web.Extensions

    Reference

  • 相关阅读:
    java stackoverflowerror与outofmemoryerror区别
    JVM参数笔记
    记录一次JVM配置优化的案例
    JVM相关内容简介(转)
    Spring boot中的定时任务(计划任务)
    netty学习:UDP服务器与Spring整合(2)
    netty学习:UDP服务器与Spring整合
    maven 纯注解一步一步搭建Spring Mvc项目(入门)
    将class类对象转化成json的数据格式
    Spring五个事务隔离级别和七个事务传播行为
  • 原文地址:https://www.cnblogs.com/weihanli/p/13069931.html
Copyright © 2011-2022 走看看