GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。
——出自 https://graphql.cn
由于HotChocklate是是基于asp.net core框架,所以授权策略与原生的asp.net core mvc项目大同小异,都是通过固定角色,自定义策略等方式来进行的。下面的例子就是通过一个自定义策略的例子来进行的。
并且授权是在实体类和实体类的属性上的,而不是像之前在Controller的Action上,对应的是一个api的url。
看实例了解详情:
添加Nuget包
HotChocolate.AspNetCore
HotChocolate.Data
HotChocolate.Data.EntityFramework
HotChocolate.AspNetCore.Authorization
权限类
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 =>{//不使用httpso.RequireHttpsMetadata = false;o.TokenValidationParameters = tokenValidationParameters;});//注入授权Handlerservices.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信息,再次访问,成功获取数据