GraphQL:驗證與受權

GraphQL 既是一種用於 API 的查詢語言也是一個知足你數據查詢的運行時。GraphQL 對你的 API 中的數據提供了一套易於理解的完整描述,使得客戶端可以準確地得到它須要的數據,並且沒有任何冗餘,也讓 API 更容易地隨着時間推移而演進,還能用於構建強大的開發者工具。
api

                                  ——出自 https://graphql.cnmvc

因爲HotChocklate是是基於asp.net core框架,因此受權策略與原生的asp.net core mvc項目大同小異,都是經過固定角色,自定義策略等方式來進行的。下面的例子就是經過一個自定義策略的例子來進行的。
app

而且受權是在實體類和實體類的屬性上的,而不是像以前在Controller的Action上,對應的是一個api的url。框架

看實例瞭解詳情:asp.net

添加Nuget包ide

HotChocolate.AspNetCore工具

HotChocolate.Dataui

HotChocolate.Data.EntityFrameworkurl

HotChocolate.AspNetCore.Authorizationspa

權限類

namespace GraphQLDemo02
{
    /// <summary>
    /// 用戶或角色或其餘憑據實體
    /// </summary>
    public class Permission
    {
        /// <summary>
        /// 用戶或角色或其餘憑據名稱
        /// </summary>
        public virtual string Name
        { get; set; }
        /// <summary>
        /// 請求Url
        /// </summary>
        public virtual string Url
        { get; set; }
    }
}

建立自定義策略處理類

using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;
using System;


namespace GraphQLDemo02
{
    /// <summary>
    /// 權限受權Handler
    /// </summary>
    public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
    {
        /// <summary>
        /// 驗證權限
        /// </summary>
        /// <param name="context"></param>
        /// <param name="requirement"></param>
        /// <returns></returns>
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
            //這裏能夠過濾屬性受權
            Console.WriteLine(context.Resource.GetType().GetProperty("Path").GetValue(context.Resource));
            //是否通過驗證
            var isAuthenticated = context?.User?.Identity?.IsAuthenticated;
            if (isAuthenticated.HasValue && isAuthenticated.Value)
            {
                context.Succeed(requirement);
            }
            else
            {
                context.Fail();
            }
            return Task.CompletedTask;
        }
    }
}

AuthorizationRequirement類

using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using System;


namespace GraphQLDemo02
{


    /// <summary>
    /// 必要參數類
    /// </summary>
    public class PermissionRequirement : IAuthorizationRequirement
    { 
        /// <summary>
        /// 認證受權類型
        /// </summary>
        public string ClaimType { internal get; set; }
       
        /// <summary>
        /// 發行人
        /// </summary>
        public string Issuer { get; set; }
        /// <summary>
        /// 訂閱人
        /// </summary>
        public string Audience { get; set; }
        /// <summary>
        /// 過時時間
        /// </summary>
        public TimeSpan Expiration { get; set; }
        /// <summary>
        /// 簽名驗證
        /// </summary>
        public SigningCredentials SigningCredentials { get; set; }       
        /// <summary>
        /// 構造
        /// </summary>
        /// <param name="deniedAction">拒約請求的url</param>
        /// <param name="permissions">權限集合</param>
        /// <param name="claimType">聲明類型</param>
        /// <param name="issuer">發行人</param>
        /// <param name="audience">訂閱人</param>
        /// <param name="signingCredentials">簽名驗證明體</param>
        public PermissionRequirement(string claimType, string issuer, string audience, SigningCredentials signingCredentials, TimeSpan expiration)
        {
            ClaimType = claimType;                
            Issuer = issuer;
            Audience = audience;
            Expiration = expiration;
            SigningCredentials = signingCredentials;
        }
    }
}

Token生成類

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;


namespace GraphQLDemo02
{
    public class JwtToken
    {
        /// <summary>
        /// 獲取基於JWT的Token
        /// </summary>
        /// <param name="username"></param>
        /// <returns></returns>
        public static dynamic BuildJwtToken(Claim[] claims, PermissionRequirement permissionRequirement)
        {
            var now = DateTime.UtcNow;
            var jwt = new JwtSecurityToken(
                issuer: permissionRequirement.Issuer,
                audience: permissionRequirement.Audience,
                claims: claims,
                notBefore: now,
                expires: now.Add(permissionRequirement.Expiration),
                signingCredentials: permissionRequirement.SigningCredentials
            );
            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
            var response = new
            {
                Status = true,
                access_token = encodedJwt,
                expires_in = permissionRequirement.Expiration.TotalMilliseconds,
                token_type = "Bearer"
            };
            return response;
        }
    }
}

Starup.cs

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Security.Claims;
using System.Text;
using Microsoft.EntityFrameworkCore;


namespace GraphQLDemo02
{
    public class Startup
    {
        public IConfiguration Configuration { get; }


        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public void ConfigureServices(IServiceCollection services)
        {          
            services.AddPooledDbContextFactory<AdventureWorks2016Context>(
              (services, options) => options
              .UseSqlServer(Configuration.GetConnectionString("ConnectionString"))
              .UseLoggerFactory(services.GetRequiredService<ILoggerFactory>()))
              .AddGraphQLServer()
              .AddAuthorization()
              .AddQueryType<Query>()
              .AddFiltering()
              .AddSorting()
              .AddProjections();
            AddAuth(services);
        }




        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseAuthentication();
            app.UseRouting();
            app.UseAuthorization();


            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGraphQL();
            });
        }
        void AddAuth(IServiceCollection services)
        {
            //讀取配置文件
            var audienceConfig = Configuration.GetSection("Audience");
            var symmetricKeyAsBase64 = audienceConfig["Secret"];
            var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);
            var signingKey = new SymmetricSecurityKey(keyByteArray);
            var tokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = signingKey,
                ValidateIssuer = true,
                ValidIssuer = audienceConfig["Issuer"],
                ValidateAudience = true,
                ValidAudience = audienceConfig["Audience"],
                ValidateLifetime = true,
                ClockSkew = TimeSpan.Zero,
                RequireExpirationTime = true,


            };
            var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);


            //若是第三個參數,是ClaimTypes.Role,上面集合的每一個元素的Name爲角色名稱,若是ClaimTypes.Name,即上面集合的每一個元素的Name爲用戶名
            var permissionRequirement = new PermissionRequirement(              
                ClaimTypes.Role,
                audienceConfig["Issuer"],
                audienceConfig["Audience"],
                signingCredentials,
                expiration: TimeSpan.FromSeconds(1000000)//設置Token過時時間
                );


            services.AddAuthorization(options =>
            {
                options.AddPolicy("Permission", policy => policy.AddRequirements(permissionRequirement));
            }).
            AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, o =>
            {
                //不使用https
                o.RequireHttpsMetadata = false;
                o.TokenValidationParameters = tokenValidationParameters;


            });
            //注入受權Handler
            services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
            services.AddSingleton(permissionRequirement);
        }
    }
}

Query.cs

using HotChocolate;
using HotChocolate.Data;
using HotChocolate.Types;
using System;
using System.Linq;
using System.Security.Claims;


namespace GraphQLDemo02
{       
    public class Query
    {
        [UseDbContext(typeof(AdventureWorks2016Context))]
        [UseOffsetPaging]
        [UseProjection]
        [UseFiltering]
        [UseSorting]
        public IQueryable<Product> GetProducts([ScopedService] AdventureWorks2016Context context)
        {
            return context.Products;
        }


        [UseDbContext(typeof(AdventureWorks2016Context))]
        [UsePaging]
        [UseProjection]
        [UseFiltering]
        [UseSorting]
        public IQueryable<Person> GetPersons([ScopedService] AdventureWorks2016Context context)
        {
            return context.People;
        }
        public TokenModel Login(string username, string password, [Service] PermissionRequirement requirement)
        {
            Console.WriteLine(username);
            var isValidated = username == "gsw" && password == "111111";
            if (!isValidated)
            {
                return new TokenModel()
                {
                    Result = false,
                    Message = "認證失敗"
                };
            }
            else
            {
                //若是是基於用戶的受權策略,這裏要添加用戶;若是是基於角色的受權策略,這裏要添加角色
                var claims = new Claim[] {
                    new Claim(ClaimTypes.Name, username),
                    new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(200000).ToString())
                };


                var token = JwtToken.BuildJwtToken(claims, requirement);
                return new TokenModel()
                {
                    Result = true,
                    Data = token.access_token
                };
            }
        }    
    }
}

HotChocklate的驗證是經過在實體類或實體類的屬性上加自定義策略來對數據進行權限控制,因此下面的例子是加在實體類上,所有屬性進行受權驗證。

Product.cs

[HotChocolate.AspNetCore.Authorization.Authorize(Policy = "Permission")]
    public partial class Product
    {
       //此處略n多行
    }

Person.cs

[Authorize(Policy = "Permission")]
    public partial class Person
    {
      //此處略n多行
    }

運行結果:

未驗證

登陸

拿到token後,在header上追加驗證token信息,再次訪問,成功獲取數據

相關文章
相關標籤/搜索