107 changed files with 3151 additions and 181 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,24 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netcoreapp3.1</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<EmbeddedResource Include="LINGYUN\Abp\IdentityServer\WeChat\Localization\en.json" /> |
||||
|
<EmbeddedResource Include="LINGYUN\Abp\IdentityServer\WeChat\Localization\zh-Hans.json" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Volo.Abp.IdentityServer.Domain" Version="3.3.0" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\wechat\LINGYUN.Abp.WeChat.MiniProgram\LINGYUN.Abp.WeChat.MiniProgram.csproj" /> |
||||
|
<ProjectReference Include="..\..\wechat\LINGYUN.Abp.WeChat.Official\LINGYUN.Abp.WeChat.Official.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,54 @@ |
|||||
|
using LINGYUN.Abp.IdentityServer.WeChat.MiniProgram; |
||||
|
using LINGYUN.Abp.IdentityServer.WeChat.Official; |
||||
|
using LINGYUN.Abp.WeChat.MiniProgram; |
||||
|
using LINGYUN.Abp.WeChat.Official; |
||||
|
using Microsoft.AspNetCore.Authentication; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Volo.Abp.IdentityServer; |
||||
|
using Volo.Abp.IdentityServer.Localization; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.VirtualFileSystem; |
||||
|
|
||||
|
namespace LINGYUN.Abp.IdentityServer.WeChat |
||||
|
{ |
||||
|
[DependsOn( |
||||
|
typeof(AbpWeChatOfficialModule), |
||||
|
typeof(AbpWeChatMiniProgramModule), |
||||
|
typeof(AbpIdentityServerDomainModule))] |
||||
|
public class AbpIdentityServerWeChatModule : AbpModule |
||||
|
{ |
||||
|
public override void PreConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
var configuration = context.Services.GetConfiguration(); |
||||
|
|
||||
|
PreConfigure<IIdentityServerBuilder>(builder => |
||||
|
{ |
||||
|
builder.AddProfileService<WeChatMiniProgramProfileService>(); |
||||
|
|
||||
|
// TODO: 两个类型不通用配置项,不然只需要一个
|
||||
|
builder.AddExtensionGrantValidator<WeChatMiniProgramGrantValidator>(); |
||||
|
builder.AddExtensionGrantValidator<WeChatOfficialGrantValidator>(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
Configure<AbpVirtualFileSystemOptions>(options => |
||||
|
{ |
||||
|
options.FileSets.AddEmbedded<AbpIdentityServerWeChatModule>(); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpLocalizationOptions>(options => |
||||
|
{ |
||||
|
options.Resources |
||||
|
.Get<AbpIdentityServerResource>() |
||||
|
.AddVirtualJson("/LINGYUN/Abp/IdentityServer/WeChat/Localization"); |
||||
|
}); |
||||
|
|
||||
|
context.Services |
||||
|
.AddAuthentication() |
||||
|
.AddWeChat(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace LINGYUN.Abp.IdentityServer |
||||
|
{ |
||||
|
public interface IWeChatResourceDataSeeder |
||||
|
{ |
||||
|
Task CreateStandardResourcesAsync(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"culture": "en", |
||||
|
"texts": { |
||||
|
"SelfRegistrationDisabledMessage": "Self-registration is disabled for this application. Please contact the application administrator to register a new user.", |
||||
|
"InvalidGrant:GrantTypeInvalid": "The type of authorization that is not allowed!", |
||||
|
"InvalidGrant:WeChatTokenInvalid": "WeChat authentication failed!", |
||||
|
"InvalidGrant:WeChatCodeNotFound": "The code obtained when WeChat is logged in is empty or does not exist!", |
||||
|
"InvalidGrant:WeChatNotRegister": "User WeChat account not registed!" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"culture": "zh-Hans", |
||||
|
"texts": { |
||||
|
"SelfRegistrationDisabledMessage": "应用程序未开放注册,请联系管理员添加新用户.", |
||||
|
"InvalidGrant:GrantTypeInvalid": "不被允许的授权类型!", |
||||
|
"InvalidGrant:WeChatTokenInvalid": "微信认证失败!", |
||||
|
"InvalidGrant:WeChatCodeNotFound": "微信登录时获取的 code 为空或不存在!", |
||||
|
"InvalidGrant:WeChatNotRegister": "用户微信账号未绑定!" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,118 @@ |
|||||
|
using IdentityModel; |
||||
|
using IdentityServer4.Events; |
||||
|
using IdentityServer4.Models; |
||||
|
using IdentityServer4.Services; |
||||
|
using IdentityServer4.Validation; |
||||
|
using LINGYUN.Abp.WeChat; |
||||
|
using LINGYUN.Abp.WeChat.MiniProgram; |
||||
|
using LINGYUN.Abp.WeChat.OpenId; |
||||
|
using LINGYUN.Abp.WeChat.Security.Claims; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Microsoft.Extensions.Localization; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Net.Http; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Identity; |
||||
|
using Volo.Abp.IdentityServer.Localization; |
||||
|
using Volo.Abp.Security.Claims; |
||||
|
using IdentityUser = Volo.Abp.Identity.IdentityUser; |
||||
|
|
||||
|
namespace LINGYUN.Abp.IdentityServer.WeChat.MiniProgram |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 对于小程序绑定用户的扩展授权验证器
|
||||
|
/// </summary>
|
||||
|
public class WeChatMiniProgramGrantValidator : IExtensionGrantValidator |
||||
|
{ |
||||
|
protected ILogger<WeChatMiniProgramGrantValidator> Logger { get; } |
||||
|
protected AbpWeChatMiniProgramOptions Options { get; } |
||||
|
protected IHttpClientFactory HttpClientFactory{ get; } |
||||
|
protected IEventService EventService { get; } |
||||
|
protected IWeChatOpenIdFinder WeChatOpenIdFinder { get; } |
||||
|
protected IIdentityUserRepository UserRepository { get; } |
||||
|
protected UserManager<IdentityUser> UserManager { get; } |
||||
|
protected SignInManager<IdentityUser> SignInManager { get; } |
||||
|
protected IStringLocalizer<AbpIdentityServerResource> Localizer { get; } |
||||
|
protected PhoneNumberTokenProvider<IdentityUser> PhoneNumberTokenProvider { get; } |
||||
|
|
||||
|
|
||||
|
public WeChatMiniProgramGrantValidator( |
||||
|
IEventService eventService, |
||||
|
IWeChatOpenIdFinder weChatOpenIdFinder, |
||||
|
IHttpClientFactory httpClientFactory, |
||||
|
UserManager<IdentityUser> userManager, |
||||
|
IIdentityUserRepository userRepository, |
||||
|
SignInManager<IdentityUser> signInManager, |
||||
|
IStringLocalizer<AbpIdentityServerResource> stringLocalizer, |
||||
|
PhoneNumberTokenProvider<IdentityUser> phoneNumberTokenProvider, |
||||
|
IOptions<AbpWeChatMiniProgramOptions> options, |
||||
|
ILogger<WeChatMiniProgramGrantValidator> logger) |
||||
|
{ |
||||
|
Logger = logger; |
||||
|
Options = options.Value; |
||||
|
|
||||
|
EventService = eventService; |
||||
|
UserManager = userManager; |
||||
|
SignInManager = signInManager; |
||||
|
Localizer = stringLocalizer; |
||||
|
UserRepository = userRepository; |
||||
|
WeChatOpenIdFinder = weChatOpenIdFinder; |
||||
|
HttpClientFactory = httpClientFactory; |
||||
|
PhoneNumberTokenProvider = phoneNumberTokenProvider; |
||||
|
} |
||||
|
|
||||
|
public string GrantType => AbpWeChatMiniProgramConsts.GrantType; |
||||
|
|
||||
|
public async Task ValidateAsync(ExtensionGrantValidationContext context) |
||||
|
{ |
||||
|
var raw = context.Request.Raw; |
||||
|
var credential = raw.Get(OidcConstants.TokenRequest.GrantType); |
||||
|
if (credential == null || !credential.Equals(GrantType)) |
||||
|
{ |
||||
|
Logger.LogWarning("Invalid grant type: not allowed"); |
||||
|
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, |
||||
|
Localizer["InvalidGrant:GrantTypeInvalid"]); |
||||
|
return; |
||||
|
} |
||||
|
// TODO: 统一命名规范, 微信认证传递的 code 改为 WeChatOpenIdConsts.WeCahtCodeKey
|
||||
|
var wechatCode = raw.Get(AbpWeChatGlobalConsts.TokenName); |
||||
|
if (wechatCode.IsNullOrWhiteSpace() || wechatCode.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
Logger.LogWarning("Invalid grant type: wechat code not found"); |
||||
|
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, |
||||
|
Localizer["InvalidGrant:WeChatCodeNotFound"]); |
||||
|
return; |
||||
|
} |
||||
|
var wechatOpenId = await WeChatOpenIdFinder.FindAsync(wechatCode, Options.AppId, Options.AppSecret); |
||||
|
// 如果存在 UnionId优先使用
|
||||
|
var currentUser = await UserManager.FindByLoginAsync(AbpWeChatMiniProgramConsts.ProviderKey, wechatOpenId.UnionId ?? wechatOpenId.OpenId); |
||||
|
if(currentUser == null) |
||||
|
{ |
||||
|
Logger.LogWarning("Invalid grant type: wechat openid: {0} not register", wechatOpenId.OpenId); |
||||
|
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, |
||||
|
Localizer["InvalidGrant:WeChatNotRegister"]); |
||||
|
return; |
||||
|
} |
||||
|
var sub = await UserManager.GetUserIdAsync(currentUser); |
||||
|
|
||||
|
var additionalClaims = new List<Claim>(); |
||||
|
if (currentUser.TenantId.HasValue) |
||||
|
{ |
||||
|
additionalClaims.Add(new Claim(AbpClaimTypes.TenantId, currentUser.TenantId?.ToString())); |
||||
|
} |
||||
|
additionalClaims.Add(new Claim(AbpWeChatClaimTypes.OpenId, wechatOpenId.OpenId)); |
||||
|
if (!wechatOpenId.UnionId.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
additionalClaims.Add(new Claim(AbpWeChatClaimTypes.UnionId, wechatOpenId.UnionId)); |
||||
|
} |
||||
|
|
||||
|
await EventService.RaiseAsync(new UserLoginSuccessEvent(currentUser.UserName, wechatOpenId.OpenId, null)); |
||||
|
context.Result = new GrantValidationResult(sub, |
||||
|
AbpWeChatMiniProgramConsts.AuthenticationMethod, additionalClaims.ToArray()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,61 @@ |
|||||
|
using IdentityServer4.AspNetIdentity; |
||||
|
using IdentityServer4.Models; |
||||
|
using LINGYUN.Abp.WeChat.Security.Claims; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using System.Linq; |
||||
|
using System.Security.Principal; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Identity; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
using Volo.Abp.Uow; |
||||
|
using IdentityUser = Volo.Abp.Identity.IdentityUser; |
||||
|
|
||||
|
namespace LINGYUN.Abp.IdentityServer.WeChat.MiniProgram |
||||
|
{ |
||||
|
public class WeChatMiniProgramProfileService : ProfileService<IdentityUser> |
||||
|
{ |
||||
|
protected ICurrentTenant CurrentTenant { get; } |
||||
|
public WeChatMiniProgramProfileService( |
||||
|
IdentityUserManager userManager, |
||||
|
IUserClaimsPrincipalFactory<IdentityUser> claimsFactory, |
||||
|
ICurrentTenant currentTenant) |
||||
|
: base(userManager, claimsFactory) |
||||
|
{ |
||||
|
CurrentTenant = currentTenant; |
||||
|
} |
||||
|
|
||||
|
[UnitOfWork] |
||||
|
public override async Task GetProfileDataAsync(ProfileDataRequestContext context) |
||||
|
{ |
||||
|
using (CurrentTenant.Change(context.Subject.FindTenantId())) |
||||
|
{ |
||||
|
await base.GetProfileDataAsync(context); |
||||
|
|
||||
|
// TODO: 可以从令牌获取openid, 安全性呢?
|
||||
|
TryAddWeChatClaim(context, AbpWeChatClaimTypes.OpenId); |
||||
|
TryAddWeChatClaim(context, AbpWeChatClaimTypes.UnionId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[UnitOfWork] |
||||
|
public override async Task IsActiveAsync(IsActiveContext context) |
||||
|
{ |
||||
|
using (CurrentTenant.Change(context.Subject.FindTenantId())) |
||||
|
{ |
||||
|
await base.IsActiveAsync(context); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual void TryAddWeChatClaim(ProfileDataRequestContext context, string weChatClaimType) |
||||
|
{ |
||||
|
if (context.RequestedClaimTypes.Any(rc => rc.Contains(weChatClaimType))) |
||||
|
{ |
||||
|
var weChatClaim = context.Subject.FindFirst(weChatClaimType); |
||||
|
if (weChatClaim != null) |
||||
|
{ |
||||
|
context.IssuedClaims.Add(weChatClaim); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,117 @@ |
|||||
|
using IdentityModel; |
||||
|
using IdentityServer4.Events; |
||||
|
using IdentityServer4.Models; |
||||
|
using IdentityServer4.Services; |
||||
|
using IdentityServer4.Validation; |
||||
|
using LINGYUN.Abp.WeChat; |
||||
|
using LINGYUN.Abp.WeChat.Official; |
||||
|
using LINGYUN.Abp.WeChat.OpenId; |
||||
|
using LINGYUN.Abp.WeChat.Security.Claims; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Microsoft.Extensions.Localization; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Net.Http; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Identity; |
||||
|
using Volo.Abp.IdentityServer.Localization; |
||||
|
using Volo.Abp.Security.Claims; |
||||
|
using IdentityUser = Volo.Abp.Identity.IdentityUser; |
||||
|
|
||||
|
namespace LINGYUN.Abp.IdentityServer.WeChat.Official |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 对于公众平台绑定用户的扩展授权验证器
|
||||
|
/// </summary>
|
||||
|
public class WeChatOfficialGrantValidator : IExtensionGrantValidator |
||||
|
{ |
||||
|
protected ILogger<WeChatOfficialGrantValidator> Logger { get; } |
||||
|
protected AbpWeChatOfficialOptions Options { get; } |
||||
|
protected IHttpClientFactory HttpClientFactory { get; } |
||||
|
protected IEventService EventService { get; } |
||||
|
protected IWeChatOpenIdFinder WeChatOpenIdFinder { get; } |
||||
|
protected IIdentityUserRepository UserRepository { get; } |
||||
|
protected UserManager<IdentityUser> UserManager { get; } |
||||
|
protected SignInManager<IdentityUser> SignInManager { get; } |
||||
|
protected IStringLocalizer<AbpIdentityServerResource> Localizer { get; } |
||||
|
protected PhoneNumberTokenProvider<IdentityUser> PhoneNumberTokenProvider { get; } |
||||
|
|
||||
|
|
||||
|
public WeChatOfficialGrantValidator( |
||||
|
IEventService eventService, |
||||
|
IWeChatOpenIdFinder weChatOpenIdFinder, |
||||
|
IHttpClientFactory httpClientFactory, |
||||
|
UserManager<IdentityUser> userManager, |
||||
|
IIdentityUserRepository userRepository, |
||||
|
SignInManager<IdentityUser> signInManager, |
||||
|
IStringLocalizer<AbpIdentityServerResource> stringLocalizer, |
||||
|
PhoneNumberTokenProvider<IdentityUser> phoneNumberTokenProvider, |
||||
|
IOptions<AbpWeChatOfficialOptions> options, |
||||
|
ILogger<WeChatOfficialGrantValidator> logger) |
||||
|
{ |
||||
|
Logger = logger; |
||||
|
Options = options.Value; |
||||
|
|
||||
|
EventService = eventService; |
||||
|
UserManager = userManager; |
||||
|
SignInManager = signInManager; |
||||
|
Localizer = stringLocalizer; |
||||
|
UserRepository = userRepository; |
||||
|
WeChatOpenIdFinder = weChatOpenIdFinder; |
||||
|
HttpClientFactory = httpClientFactory; |
||||
|
PhoneNumberTokenProvider = phoneNumberTokenProvider; |
||||
|
} |
||||
|
|
||||
|
public string GrantType => AbpWeChatOfficialConsts.GrantType; |
||||
|
|
||||
|
public async Task ValidateAsync(ExtensionGrantValidationContext context) |
||||
|
{ |
||||
|
var raw = context.Request.Raw; |
||||
|
var credential = raw.Get(OidcConstants.TokenRequest.GrantType); |
||||
|
if (credential == null || !credential.Equals(GrantType)) |
||||
|
{ |
||||
|
Logger.LogWarning("Invalid grant type: not allowed"); |
||||
|
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, |
||||
|
Localizer["InvalidGrant:GrantTypeInvalid"]); |
||||
|
return; |
||||
|
} |
||||
|
// TODO: 统一命名规范, 微信认证传递的 code 改为 WeChatOpenIdConsts.WeCahtCodeKey
|
||||
|
var wechatCode = raw.Get(AbpWeChatGlobalConsts.TokenName); |
||||
|
if (wechatCode.IsNullOrWhiteSpace() || wechatCode.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
Logger.LogWarning("Invalid grant type: wechat code not found"); |
||||
|
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, |
||||
|
Localizer["InvalidGrant:WeChatCodeNotFound"]); |
||||
|
return; |
||||
|
} |
||||
|
var wechatOpenId = await WeChatOpenIdFinder.FindAsync(wechatCode, Options.AppId, Options.AppSecret); |
||||
|
var currentUser = await UserManager.FindByLoginAsync(AbpWeChatOfficialConsts.ProviderKey, wechatOpenId.OpenId); |
||||
|
if (currentUser == null) |
||||
|
{ |
||||
|
Logger.LogWarning("Invalid grant type: wechat openid: {0} not register", wechatOpenId.OpenId); |
||||
|
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, |
||||
|
Localizer["InvalidGrant:WeChatNotRegister"]); |
||||
|
return; |
||||
|
} |
||||
|
var sub = await UserManager.GetUserIdAsync(currentUser); |
||||
|
|
||||
|
var additionalClaims = new List<Claim>(); |
||||
|
if (currentUser.TenantId.HasValue) |
||||
|
{ |
||||
|
additionalClaims.Add(new Claim(AbpClaimTypes.TenantId, currentUser.TenantId?.ToString())); |
||||
|
} |
||||
|
additionalClaims.Add(new Claim(AbpWeChatClaimTypes.OpenId, wechatOpenId.OpenId)); |
||||
|
if (!wechatOpenId.UnionId.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
additionalClaims.Add(new Claim(AbpWeChatClaimTypes.UnionId, wechatOpenId.UnionId)); |
||||
|
} |
||||
|
|
||||
|
await EventService.RaiseAsync(new UserLoginSuccessEvent(currentUser.UserName, wechatOpenId.OpenId, null)); |
||||
|
context.Result = new GrantValidationResult(sub, |
||||
|
AbpWeChatOfficialConsts.AuthenticationMethod, additionalClaims.ToArray()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,71 @@ |
|||||
|
using LINGYUN.Abp.WeChat; |
||||
|
using LINGYUN.Abp.WeChat.Official; |
||||
|
|
||||
|
namespace LINGYUN.Abp.IdentityServer.WeChat.Official |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 与微信公众号认证相关的静态(可变)常量
|
||||
|
/// </summary>
|
||||
|
public static class WeChatOfficialOAuthConsts |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 微信个人信息标识
|
||||
|
/// </summary>
|
||||
|
public static string ProfileKey { get; set; } = "wechat.profile"; |
||||
|
/// <summary>
|
||||
|
/// 微信提供者标识
|
||||
|
/// </summary>
|
||||
|
public static string ProviderKey => AbpWeChatOfficialConsts.ProviderKey; |
||||
|
/// <summary>
|
||||
|
/// 微信提供者显示名称
|
||||
|
/// </summary>
|
||||
|
public static string DisplayName => AbpWeChatGlobalConsts.DisplayName; |
||||
|
/// <summary>
|
||||
|
/// 回调地址
|
||||
|
/// </summary>
|
||||
|
public static string CallbackPath { get; set; } = "/signin-wechat"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 微信客户端外的网页登录
|
||||
|
/// </summary>
|
||||
|
public const string QrConnectEndpoint = "https://open.weixin.qq.com/connect/qrconnect"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 微信客户端内的网页登录
|
||||
|
/// </summary>
|
||||
|
public const string AuthorizationEndpoint = "https://open.weixin.qq.com/connect/oauth2/authorize"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 用户允许授权后通过返回的code换取access_token地址
|
||||
|
/// </summary>
|
||||
|
public const string TokenEndpoint = "https://api.weixin.qq.com/sns/oauth2/access_token"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 使用access_token获取用户个人信息地址
|
||||
|
/// </summary>
|
||||
|
public const string UserInformationEndpoint = "https://api.weixin.qq.com/sns/userinfo"; |
||||
|
/// <summary>
|
||||
|
/// 弹出授权页面,可通过openid拿到昵称、性别、所在地。
|
||||
|
/// 并且, 即使在未关注的情况下,只要用户授权,也能获取其信息
|
||||
|
/// <br />
|
||||
|
/// <br />
|
||||
|
/// 详询: <see cref="https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html"/>
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// 以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。
|
||||
|
/// 但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息
|
||||
|
/// </remarks>
|
||||
|
public const string UserInfoScope = "snsapi_userinfo"; |
||||
|
/// <summary>
|
||||
|
/// 不弹出授权页面,直接跳转,只能获取用户openid
|
||||
|
/// <br />
|
||||
|
/// <br />
|
||||
|
/// 详询: <see cref="https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html"/>
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// 以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。
|
||||
|
/// 用户感知的就是直接进入了回调页(往往是业务页面)
|
||||
|
/// </remarks>
|
||||
|
public const string LoginScope = "snsapi_login"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,75 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Official; |
||||
|
using Microsoft.AspNetCore.Http; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using System; |
||||
|
using System.Collections; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace LINGYUN.Abp.IdentityServer.WeChat.Official |
||||
|
{ |
||||
|
public class WeChatOfficialSignatureMiddleware : IMiddleware, ITransientDependency |
||||
|
{ |
||||
|
protected AbpWeChatOfficialOptions Options { get; } |
||||
|
public WeChatOfficialSignatureMiddleware(IOptions<AbpWeChatOfficialOptions> options) |
||||
|
{ |
||||
|
Options = options.Value; |
||||
|
} |
||||
|
|
||||
|
public async Task InvokeAsync(HttpContext context, RequestDelegate next) |
||||
|
{ |
||||
|
if (context.Request.Path.HasValue) |
||||
|
{ |
||||
|
var requestPath = context.Request.Path.Value; |
||||
|
// 访问地址是否与定义的地址匹配
|
||||
|
if (requestPath.Equals(Options.Url)) |
||||
|
{ |
||||
|
var timestamp = context.Request.Query["timestamp"]; |
||||
|
var nonce = context.Request.Query["nonce"]; |
||||
|
var signature = context.Request.Query["signature"]; |
||||
|
var echostr = context.Request.Query["echostr"]; |
||||
|
// 验证消息合法性
|
||||
|
var check = CheckWeChatSignature(Options.Token, timestamp, nonce, signature); |
||||
|
if (check) |
||||
|
{ |
||||
|
// 验证通过需要把微信服务器传递的字符原封不动传回
|
||||
|
await context.Response.WriteAsync(echostr); |
||||
|
return; |
||||
|
} |
||||
|
// 微信消息验证不通过
|
||||
|
throw new AbpException("Invalid wechat signature"); |
||||
|
} |
||||
|
} |
||||
|
// 不属于微信的消息进入下一个中间件
|
||||
|
await next(context); |
||||
|
} |
||||
|
|
||||
|
protected bool CheckWeChatSignature(string token, string timestamp, string nonce, string signature) |
||||
|
{ |
||||
|
var al = new ArrayList |
||||
|
{ |
||||
|
token, |
||||
|
timestamp, |
||||
|
nonce |
||||
|
}; |
||||
|
// step1 排序
|
||||
|
al.Sort(); |
||||
|
string signatureStr = string.Empty; |
||||
|
// step2 拼接
|
||||
|
for (int i = 0; i < al.Count; i++) |
||||
|
{ |
||||
|
signatureStr += al[i]; |
||||
|
} |
||||
|
// step3 SHA1加密
|
||||
|
byte[] bytes_out = signatureStr.Sha1(); |
||||
|
string result = BitConverter.ToString(bytes_out).Replace("-", ""); |
||||
|
// step4 比对
|
||||
|
if (result.Equals(signature, StringComparison.CurrentCultureIgnoreCase)) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,86 @@ |
|||||
|
using LINGYUN.Abp.IdentityServer.WeChat.Official; |
||||
|
using LINGYUN.Abp.WeChat.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Guids; |
||||
|
using Volo.Abp.Identity; |
||||
|
using Volo.Abp.IdentityServer.IdentityResources; |
||||
|
|
||||
|
namespace LINGYUN.Abp.IdentityServer |
||||
|
{ |
||||
|
public class WeChatResourceDataSeeder : IWeChatResourceDataSeeder, ITransientDependency |
||||
|
{ |
||||
|
protected IIdentityClaimTypeRepository ClaimTypeRepository { get; } |
||||
|
protected IIdentityResourceRepository IdentityResourceRepository { get; } |
||||
|
protected IGuidGenerator GuidGenerator { get; } |
||||
|
|
||||
|
public WeChatResourceDataSeeder( |
||||
|
IIdentityResourceRepository identityResourceRepository, |
||||
|
IGuidGenerator guidGenerator, |
||||
|
IIdentityClaimTypeRepository claimTypeRepository) |
||||
|
{ |
||||
|
IdentityResourceRepository = identityResourceRepository; |
||||
|
GuidGenerator = guidGenerator; |
||||
|
ClaimTypeRepository = claimTypeRepository; |
||||
|
} |
||||
|
|
||||
|
public virtual async Task CreateStandardResourcesAsync() |
||||
|
{ |
||||
|
var wechatClaimTypes = new string[] |
||||
|
{ |
||||
|
AbpWeChatClaimTypes.AvatarUrl, |
||||
|
AbpWeChatClaimTypes.City, |
||||
|
AbpWeChatClaimTypes.Country, |
||||
|
AbpWeChatClaimTypes.NickName, |
||||
|
AbpWeChatClaimTypes.OpenId, |
||||
|
AbpWeChatClaimTypes.Privilege, |
||||
|
AbpWeChatClaimTypes.Province, |
||||
|
AbpWeChatClaimTypes.Sex, |
||||
|
AbpWeChatClaimTypes.UnionId |
||||
|
}; |
||||
|
|
||||
|
var wechatResource = new IdentityServer4.Models.IdentityResource( |
||||
|
WeChatOfficialOAuthConsts.ProfileKey, |
||||
|
WeChatOfficialOAuthConsts.DisplayName, |
||||
|
wechatClaimTypes); |
||||
|
|
||||
|
foreach (var claimType in wechatClaimTypes) |
||||
|
{ |
||||
|
await AddClaimTypeIfNotExistsAsync(claimType); |
||||
|
} |
||||
|
|
||||
|
await AddIdentityResourceIfNotExistsAsync(wechatResource); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task AddIdentityResourceIfNotExistsAsync(IdentityServer4.Models.IdentityResource resource) |
||||
|
{ |
||||
|
if (await IdentityResourceRepository.CheckNameExistAsync(resource.Name)) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await IdentityResourceRepository.InsertAsync( |
||||
|
new IdentityResource( |
||||
|
GuidGenerator.Create(), |
||||
|
resource |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task AddClaimTypeIfNotExistsAsync(string claimType) |
||||
|
{ |
||||
|
if (await ClaimTypeRepository.AnyAsync(claimType)) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await ClaimTypeRepository.InsertAsync( |
||||
|
new IdentityClaimType( |
||||
|
GuidGenerator.Create(), |
||||
|
claimType, |
||||
|
isStatic: true |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,331 @@ |
|||||
|
using LINGYUN.Abp.IdentityServer.WeChat.Official; |
||||
|
using LINGYUN.Abp.WeChat.Official; |
||||
|
using Microsoft.AspNetCore.Authentication.OAuth; |
||||
|
using Microsoft.AspNetCore.WebUtilities; |
||||
|
using Microsoft.Extensions.Caching.Distributed; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Microsoft.Extensions.Primitives; |
||||
|
using Microsoft.Net.Http.Headers; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Globalization; |
||||
|
using System.Net.Http; |
||||
|
using System.Security.Claims; |
||||
|
using System.Text; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using System.Text.Json; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Caching; |
||||
|
|
||||
|
namespace Microsoft.AspNetCore.Authentication.WeChat.Official |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 网页授权只有公众平台的实现
|
||||
|
/// </summary>
|
||||
|
public class WeChatOfficialOAuthHandler : OAuthHandler<WeChatOfficialOAuthOptions> |
||||
|
{ |
||||
|
protected IDistributedCache<WeChatOfficialStateCacheItem> Cache { get; } |
||||
|
protected AbpWeChatOfficialOptions WeChatOfficialOptions { get; } |
||||
|
public WeChatOfficialOAuthHandler( |
||||
|
IDistributedCache<WeChatOfficialStateCacheItem> cache, |
||||
|
IOptionsMonitor<WeChatOfficialOAuthOptions> options, |
||||
|
IOptions<AbpWeChatOfficialOptions> weChatOfficialOptions, |
||||
|
ILoggerFactory logger, |
||||
|
UrlEncoder encoder, |
||||
|
ISystemClock clock) |
||||
|
: base(options, logger, encoder, clock) |
||||
|
{ |
||||
|
Cache = cache; |
||||
|
WeChatOfficialOptions = weChatOfficialOptions.Value; |
||||
|
} |
||||
|
|
||||
|
protected override Task InitializeHandlerAsync() |
||||
|
{ |
||||
|
// 用配置项重写
|
||||
|
Options.ClientId = WeChatOfficialOptions.AppId; |
||||
|
Options.ClientSecret = WeChatOfficialOptions.AppSecret; |
||||
|
return base.InitializeHandlerAsync(); |
||||
|
} |
||||
|
|
||||
|
protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) |
||||
|
{ |
||||
|
var address = QueryHelpers.AddQueryString(Options.UserInformationEndpoint, new Dictionary<string, string> |
||||
|
{ |
||||
|
["access_token"] = tokens.AccessToken, |
||||
|
["openid"] = tokens.Response.GetRootString("openid") |
||||
|
}); |
||||
|
|
||||
|
var response = await Backchannel.GetAsync(address); |
||||
|
if (!response.IsSuccessStatusCode) |
||||
|
{ |
||||
|
Logger.LogError("An error occurred while retrieving the user profile: the remote server " + |
||||
|
"returned a {Status} response with the following payload: {Headers} {Body}.", |
||||
|
/* Status: */ response.StatusCode, |
||||
|
/* Headers: */ response.Headers.ToString(), |
||||
|
/* Body: */ await response.Content.ReadAsStringAsync()); |
||||
|
|
||||
|
throw new HttpRequestException("An error occurred while retrieving user information."); |
||||
|
} |
||||
|
|
||||
|
var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); |
||||
|
if (!string.IsNullOrEmpty(payload.GetRootString("errcode"))) |
||||
|
{ |
||||
|
Logger.LogError("An error occurred while retrieving the user profile: the remote server " + |
||||
|
"returned a {Status} response with the following payload: {Headers} {Body}.", |
||||
|
/* Status: */ response.StatusCode, |
||||
|
/* Headers: */ response.Headers.ToString(), |
||||
|
/* Body: */ await response.Content.ReadAsStringAsync()); |
||||
|
|
||||
|
throw new HttpRequestException("An error occurred while retrieving user information."); |
||||
|
} |
||||
|
|
||||
|
var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement); |
||||
|
context.RunClaimActions(); |
||||
|
|
||||
|
await Events.CreatingTicket(context); |
||||
|
|
||||
|
// TODO: 此处通过唯一的 CorrelationId, 将 properties生成的State缓存删除
|
||||
|
var state = Request.Query["state"]; |
||||
|
|
||||
|
var stateCacheKey = WeChatOfficialStateCacheItem.CalculateCacheKey(state, null); |
||||
|
await Cache.RemoveAsync(stateCacheKey, token: Context.RequestAborted); |
||||
|
|
||||
|
return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// code换取access_token
|
||||
|
/// </summary>
|
||||
|
protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(OAuthCodeExchangeContext context) |
||||
|
{ |
||||
|
var address = QueryHelpers.AddQueryString(Options.TokenEndpoint, new Dictionary<string, string>() |
||||
|
{ |
||||
|
["appid"] = Options.ClientId, |
||||
|
["secret"] = Options.ClientSecret, |
||||
|
["code"] = context.Code, |
||||
|
["grant_type"] = "authorization_code" |
||||
|
}); |
||||
|
|
||||
|
var response = await Backchannel.GetAsync(address); |
||||
|
if (!response.IsSuccessStatusCode) |
||||
|
{ |
||||
|
Logger.LogError("An error occurred while retrieving an access token: the remote server " + |
||||
|
"returned a {Status} response with the following payload: {Headers} {Body}.", |
||||
|
/* Status: */ response.StatusCode, |
||||
|
/* Headers: */ response.Headers.ToString(), |
||||
|
/* Body: */ await response.Content.ReadAsStringAsync()); |
||||
|
|
||||
|
return OAuthTokenResponse.Failed(new Exception("An error occurred while retrieving an access token.")); |
||||
|
} |
||||
|
|
||||
|
var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); |
||||
|
if (!string.IsNullOrEmpty(payload.GetRootString("errcode"))) |
||||
|
{ |
||||
|
Logger.LogError("An error occurred while retrieving an access token: the remote server " + |
||||
|
"returned a {Status} response with the following payload: {Headers} {Body}.", |
||||
|
/* Status: */ response.StatusCode, |
||||
|
/* Headers: */ response.Headers.ToString(), |
||||
|
/* Body: */ await response.Content.ReadAsStringAsync()); |
||||
|
|
||||
|
return OAuthTokenResponse.Failed(new Exception("An error occurred while retrieving an access token.")); |
||||
|
} |
||||
|
return OAuthTokenResponse.Success(payload); |
||||
|
} |
||||
|
|
||||
|
protected override async Task HandleChallengeAsync(AuthenticationProperties properties) |
||||
|
{ |
||||
|
await base.HandleChallengeAsync(properties); |
||||
|
|
||||
|
// TODO: 此处已经生成唯一的 CorrelationId, 可以借此将 properties生成State之后再进行缓存
|
||||
|
var state = properties.Items[".xsrf"]; |
||||
|
|
||||
|
var stateToken = Options.StateDataFormat.Protect(properties); |
||||
|
var stateCacheKey = WeChatOfficialStateCacheItem.CalculateCacheKey(state, null); |
||||
|
|
||||
|
await Cache |
||||
|
.SetAsync( |
||||
|
stateCacheKey, |
||||
|
new WeChatOfficialStateCacheItem(stateToken), |
||||
|
new DistributedCacheEntryOptions |
||||
|
{ |
||||
|
AbsoluteExpiration = Clock.UtcNow.AddMinutes(2) // TODO: 设定2分钟过期?
|
||||
|
}, |
||||
|
token: Context.RequestAborted); |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// 构建用户授权地址
|
||||
|
/// </summary>
|
||||
|
protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri) |
||||
|
{ |
||||
|
var state = properties.Items[".xsrf"]; |
||||
|
|
||||
|
var isWeChatBrewserRequest = IsWeChatBrowser(); |
||||
|
|
||||
|
var scope = isWeChatBrewserRequest |
||||
|
? WeChatOfficialOAuthConsts.UserInfoScope |
||||
|
: FormatScope(); |
||||
|
|
||||
|
var endPoint = isWeChatBrewserRequest |
||||
|
? Options.AuthorizationEndpoint |
||||
|
: WeChatOfficialOAuthConsts.QrConnectEndpoint; |
||||
|
|
||||
|
var challengeUrl = QueryHelpers.AddQueryString(endPoint, new Dictionary<string, string> |
||||
|
{ |
||||
|
["appid"] = Options.ClientId, |
||||
|
["redirect_uri"] = redirectUri, |
||||
|
["response_type"] = "code" |
||||
|
}); |
||||
|
|
||||
|
challengeUrl += $"&scope={scope}&state={state}"; |
||||
|
|
||||
|
return challengeUrl; |
||||
|
} |
||||
|
|
||||
|
protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync() |
||||
|
{ |
||||
|
var query = Request.Query; |
||||
|
|
||||
|
// TODO: 此处借用唯一的 CorrelationId, 将 properties生成的State缓存取出,进行解密
|
||||
|
var state = query["state"]; |
||||
|
|
||||
|
var stateCacheKey = WeChatOfficialStateCacheItem.CalculateCacheKey(state, null); |
||||
|
var stateCacheItem = await Cache.GetAsync(stateCacheKey, token: Context.RequestAborted); |
||||
|
|
||||
|
var properties = Options.StateDataFormat.Unprotect(stateCacheItem.State); |
||||
|
|
||||
|
if (properties == null) |
||||
|
{ |
||||
|
return HandleRequestResult.Fail("The oauth state was missing or invalid."); |
||||
|
} |
||||
|
|
||||
|
// OAuth2 10.12 CSRF
|
||||
|
if (!ValidateCorrelationId(properties)) |
||||
|
{ |
||||
|
return HandleRequestResult.Fail("Correlation failed.", properties); |
||||
|
} |
||||
|
|
||||
|
var error = query["error"]; |
||||
|
if (!StringValues.IsNullOrEmpty(error)) |
||||
|
{ |
||||
|
// Note: access_denied errors are special protocol errors indicating the user didn't
|
||||
|
// approve the authorization demand requested by the remote authorization server.
|
||||
|
// Since it's a frequent scenario (that is not caused by incorrect configuration),
|
||||
|
// denied errors are handled differently using HandleAccessDeniedErrorAsync().
|
||||
|
// Visit https://tools.ietf.org/html/rfc6749#section-4.1.2.1 for more information.
|
||||
|
var errorDescription = query["error_description"]; |
||||
|
var errorUri = query["error_uri"]; |
||||
|
if (StringValues.Equals(error, "access_denied")) |
||||
|
{ |
||||
|
var result = await HandleAccessDeniedErrorAsync(properties); |
||||
|
if (!result.None) |
||||
|
{ |
||||
|
return result; |
||||
|
} |
||||
|
var deniedEx = new Exception("Access was denied by the resource owner or by the remote server."); |
||||
|
deniedEx.Data["error"] = error.ToString(); |
||||
|
deniedEx.Data["error_description"] = errorDescription.ToString(); |
||||
|
deniedEx.Data["error_uri"] = errorUri.ToString(); |
||||
|
|
||||
|
return HandleRequestResult.Fail(deniedEx, properties); |
||||
|
} |
||||
|
|
||||
|
var failureMessage = new StringBuilder(); |
||||
|
failureMessage.Append(error); |
||||
|
if (!StringValues.IsNullOrEmpty(errorDescription)) |
||||
|
{ |
||||
|
failureMessage.Append(";Description=").Append(errorDescription); |
||||
|
} |
||||
|
if (!StringValues.IsNullOrEmpty(errorUri)) |
||||
|
{ |
||||
|
failureMessage.Append(";Uri=").Append(errorUri); |
||||
|
} |
||||
|
|
||||
|
var ex = new Exception(failureMessage.ToString()); |
||||
|
ex.Data["error"] = error.ToString(); |
||||
|
ex.Data["error_description"] = errorDescription.ToString(); |
||||
|
ex.Data["error_uri"] = errorUri.ToString(); |
||||
|
|
||||
|
return HandleRequestResult.Fail(ex, properties); |
||||
|
} |
||||
|
|
||||
|
var code = query["code"]; |
||||
|
|
||||
|
if (StringValues.IsNullOrEmpty(code)) |
||||
|
{ |
||||
|
return HandleRequestResult.Fail("Code was not found.", properties); |
||||
|
} |
||||
|
|
||||
|
var codeExchangeContext = new OAuthCodeExchangeContext(properties, code, BuildRedirectUri(Options.CallbackPath)); |
||||
|
using var tokens = await ExchangeCodeAsync(codeExchangeContext); |
||||
|
|
||||
|
if (tokens.Error != null) |
||||
|
{ |
||||
|
return HandleRequestResult.Fail(tokens.Error, properties); |
||||
|
} |
||||
|
|
||||
|
if (string.IsNullOrEmpty(tokens.AccessToken)) |
||||
|
{ |
||||
|
return HandleRequestResult.Fail("Failed to retrieve access token.", properties); |
||||
|
} |
||||
|
|
||||
|
var identity = new ClaimsIdentity(ClaimsIssuer); |
||||
|
|
||||
|
if (Options.SaveTokens) |
||||
|
{ |
||||
|
var authTokens = new List<AuthenticationToken>(); |
||||
|
|
||||
|
authTokens.Add(new AuthenticationToken { Name = "access_token", Value = tokens.AccessToken }); |
||||
|
if (!string.IsNullOrEmpty(tokens.RefreshToken)) |
||||
|
{ |
||||
|
authTokens.Add(new AuthenticationToken { Name = "refresh_token", Value = tokens.RefreshToken }); |
||||
|
} |
||||
|
|
||||
|
if (!string.IsNullOrEmpty(tokens.TokenType)) |
||||
|
{ |
||||
|
authTokens.Add(new AuthenticationToken { Name = "token_type", Value = tokens.TokenType }); |
||||
|
} |
||||
|
|
||||
|
if (!string.IsNullOrEmpty(tokens.ExpiresIn)) |
||||
|
{ |
||||
|
int value; |
||||
|
if (int.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value)) |
||||
|
{ |
||||
|
// https://www.w3.org/TR/xmlschema-2/#dateTime
|
||||
|
// https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx
|
||||
|
var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value); |
||||
|
authTokens.Add(new AuthenticationToken |
||||
|
{ |
||||
|
Name = "expires_at", |
||||
|
Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
properties.StoreTokens(authTokens); |
||||
|
} |
||||
|
|
||||
|
var ticket = await CreateTicketAsync(identity, properties, tokens); |
||||
|
if (ticket != null) |
||||
|
{ |
||||
|
return HandleRequestResult.Success(ticket); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return HandleRequestResult.Fail("Failed to retrieve user information from remote server.", properties); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override string FormatScope() |
||||
|
{ |
||||
|
return string.Join(",", Options.Scope); |
||||
|
} |
||||
|
|
||||
|
protected virtual bool IsWeChatBrowser() |
||||
|
{ |
||||
|
var userAgent = Request.Headers[HeaderNames.UserAgent].ToString(); |
||||
|
|
||||
|
return userAgent.Contains("micromessenger", StringComparison.InvariantCultureIgnoreCase); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
using LINGYUN.Abp.IdentityServer.WeChat.Official; |
||||
|
using LINGYUN.Abp.WeChat.Security.Claims; |
||||
|
using Microsoft.AspNetCore.Authentication.OAuth; |
||||
|
using Microsoft.AspNetCore.Http; |
||||
|
using System.Security.Claims; |
||||
|
using System.Text.Json; |
||||
|
|
||||
|
namespace Microsoft.AspNetCore.Authentication.WeChat.Official |
||||
|
{ |
||||
|
public class WeChatOfficialOAuthOptions : OAuthOptions |
||||
|
{ |
||||
|
public WeChatOfficialOAuthOptions() |
||||
|
{ |
||||
|
// 用于防止初始化错误
|
||||
|
ClientId = "WeChatOfficial"; |
||||
|
ClientSecret = "WeChatOfficial"; |
||||
|
|
||||
|
ClaimsIssuer = WeChatOfficialOAuthConsts.ProviderKey; |
||||
|
CallbackPath = new PathString(WeChatOfficialOAuthConsts.CallbackPath); |
||||
|
|
||||
|
AuthorizationEndpoint = WeChatOfficialOAuthConsts.AuthorizationEndpoint; |
||||
|
TokenEndpoint = WeChatOfficialOAuthConsts.TokenEndpoint; |
||||
|
UserInformationEndpoint = WeChatOfficialOAuthConsts.UserInformationEndpoint; |
||||
|
|
||||
|
Scope.Add(WeChatOfficialOAuthConsts.LoginScope); |
||||
|
Scope.Add(WeChatOfficialOAuthConsts.UserInfoScope); |
||||
|
|
||||
|
// 这个原始的属性一定要写进去,框架与UserLogin.ProviderKey进行关联判断是否绑定微信
|
||||
|
ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "openid"); |
||||
|
ClaimActions.MapJsonKey(ClaimTypes.Name, "nickname"); |
||||
|
|
||||
|
// 把自定义的身份标识写进令牌
|
||||
|
ClaimActions.MapJsonKey(AbpWeChatClaimTypes.OpenId, "openid"); |
||||
|
ClaimActions.MapJsonKey(AbpWeChatClaimTypes.UnionId, "unionid");// 公众号如果与小程序关联,这个可以用上
|
||||
|
ClaimActions.MapJsonKey(AbpWeChatClaimTypes.NickName, "nickname"); |
||||
|
ClaimActions.MapJsonKey(AbpWeChatClaimTypes.Sex, "sex", ClaimValueTypes.Integer); |
||||
|
ClaimActions.MapJsonKey(AbpWeChatClaimTypes.Country, "country"); |
||||
|
ClaimActions.MapJsonKey(AbpWeChatClaimTypes.Province, "province"); |
||||
|
ClaimActions.MapJsonKey(AbpWeChatClaimTypes.City, "city"); |
||||
|
ClaimActions.MapJsonKey(AbpWeChatClaimTypes.AvatarUrl, "headimgurl"); |
||||
|
ClaimActions.MapCustomJson(AbpWeChatClaimTypes.Privilege, user => |
||||
|
{ |
||||
|
return string.Join(",", user.GetStrings("privilege")); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
namespace Microsoft.AspNetCore.Authentication.WeChat.Official |
||||
|
{ |
||||
|
public class WeChatOfficialStateCacheItem |
||||
|
{ |
||||
|
public string State { get; set; } |
||||
|
|
||||
|
public WeChatOfficialStateCacheItem() { } |
||||
|
public WeChatOfficialStateCacheItem(string state) |
||||
|
{ |
||||
|
State = state; |
||||
|
} |
||||
|
|
||||
|
public static string CalculateCacheKey(string correlationId, string purpose) |
||||
|
{ |
||||
|
return $"ci:{correlationId};p:{purpose ?? "null"}"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,65 @@ |
|||||
|
using LINGYUN.Abp.IdentityServer.WeChat.Official; |
||||
|
using LINGYUN.Abp.WeChat; |
||||
|
using Microsoft.AspNetCore.Authentication.WeChat.Official; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using System; |
||||
|
|
||||
|
namespace Microsoft.AspNetCore.Authentication |
||||
|
{ |
||||
|
public static class WeChatAuthenticationExtensions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// </summary>
|
||||
|
public static AuthenticationBuilder AddWeChat( |
||||
|
this AuthenticationBuilder builder) |
||||
|
{ |
||||
|
return builder |
||||
|
.AddWeChat( |
||||
|
AbpWeChatGlobalConsts.AuthenticationScheme, |
||||
|
AbpWeChatGlobalConsts.DisplayName, |
||||
|
options => { }); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// </summary>
|
||||
|
public static AuthenticationBuilder AddWeChat( |
||||
|
this AuthenticationBuilder builder, |
||||
|
Action<WeChatOfficialOAuthOptions> configureOptions) |
||||
|
{ |
||||
|
return builder |
||||
|
.AddWeChat( |
||||
|
AbpWeChatGlobalConsts.AuthenticationScheme, |
||||
|
AbpWeChatGlobalConsts.DisplayName, |
||||
|
configureOptions); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// </summary>
|
||||
|
public static AuthenticationBuilder AddWeChat( |
||||
|
this AuthenticationBuilder builder, |
||||
|
string authenticationScheme, |
||||
|
Action<WeChatOfficialOAuthOptions> configureOptions) |
||||
|
{ |
||||
|
return builder |
||||
|
.AddWeChat( |
||||
|
authenticationScheme, |
||||
|
WeChatOfficialOAuthConsts.DisplayName, |
||||
|
configureOptions); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// </summary>
|
||||
|
public static AuthenticationBuilder AddWeChat( |
||||
|
this AuthenticationBuilder builder, |
||||
|
string authenticationScheme, |
||||
|
string displayName, |
||||
|
Action<WeChatOfficialOAuthOptions> configureOptions) |
||||
|
{ |
||||
|
return builder |
||||
|
.AddOAuth<WeChatOfficialOAuthOptions, WeChatOfficialOAuthHandler>( |
||||
|
authenticationScheme, |
||||
|
displayName, |
||||
|
configureOptions); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
using LINGYUN.Abp.IdentityServer.WeChat.Official; |
||||
|
|
||||
|
namespace Microsoft.AspNetCore.Builder |
||||
|
{ |
||||
|
public static class IdentityServerApplicationBuilderExtensions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 启用中间件可以处理微信服务器消息
|
||||
|
/// 用于验证消息是否来自于微信服务器
|
||||
|
/// </summary>
|
||||
|
/// <param name="builder"></param>
|
||||
|
/// <remarks>
|
||||
|
/// 也可以用Controller的形式来实现
|
||||
|
/// </remarks>
|
||||
|
/// <returns></returns>
|
||||
|
public static IApplicationBuilder UseWeChatSignature(this IApplicationBuilder builder) |
||||
|
{ |
||||
|
builder.UseMiddleware<WeChatOfficialSignatureMiddleware>(); |
||||
|
return builder; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
using System.Security.Cryptography; |
||||
|
|
||||
|
namespace System |
||||
|
{ |
||||
|
internal static class BytesExtensions |
||||
|
{ |
||||
|
public static byte[] Sha1(this byte[] data) |
||||
|
{ |
||||
|
using (var sha = SHA1.Create()) |
||||
|
{ |
||||
|
var hashBytes = sha.ComputeHash(data); |
||||
|
return hashBytes; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
using System.Security.Cryptography; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace System |
||||
|
{ |
||||
|
internal static class StringExtensions |
||||
|
{ |
||||
|
public static byte[] Sha1(this string str) |
||||
|
{ |
||||
|
using (var sha = SHA1.Create()) |
||||
|
{ |
||||
|
var hashBytes = sha.ComputeHash(Encoding.ASCII.GetBytes(str)); |
||||
|
return hashBytes; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace System.Text.Json |
||||
|
{ |
||||
|
internal static class JsonElementExtensions |
||||
|
{ |
||||
|
public static IEnumerable<string> GetRootStrings(this JsonDocument json, string key) |
||||
|
{ |
||||
|
return json.RootElement.GetStrings(key); |
||||
|
} |
||||
|
|
||||
|
public static IEnumerable<string> GetStrings(this JsonElement json, string key) |
||||
|
{ |
||||
|
var result = new List<string>(); |
||||
|
|
||||
|
if (json.TryGetProperty(key, out JsonElement property) && property.ValueKind == JsonValueKind.Array) |
||||
|
{ |
||||
|
foreach (var jsonProp in property.EnumerateArray()) |
||||
|
{ |
||||
|
result.Add(jsonProp.GetString()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
public static string GetRootString(this JsonDocument json, string key, string defaultValue = "") |
||||
|
{ |
||||
|
if (json.RootElement.TryGetProperty(key, out JsonElement property)) |
||||
|
{ |
||||
|
return property.GetString(); |
||||
|
} |
||||
|
return defaultValue; |
||||
|
} |
||||
|
|
||||
|
public static string GetString(this JsonElement json, string key, string defaultValue = "") |
||||
|
{ |
||||
|
if (json.TryGetProperty(key, out JsonElement property)) |
||||
|
{ |
||||
|
return property.GetString(); |
||||
|
} |
||||
|
return defaultValue; |
||||
|
} |
||||
|
|
||||
|
public static int GetRootInt32(this JsonDocument json, string key, int defaultValue = 0) |
||||
|
{ |
||||
|
if (json.RootElement.TryGetProperty(key, out JsonElement property) && property.TryGetInt32(out int value)) |
||||
|
{ |
||||
|
return value; |
||||
|
} |
||||
|
return defaultValue; |
||||
|
} |
||||
|
|
||||
|
public static int GetInt32(this JsonElement json, string key, int defaultValue = 0) |
||||
|
{ |
||||
|
if (json.TryGetProperty(key, out JsonElement property) && property.TryGetInt32(out int value)) |
||||
|
{ |
||||
|
return value; |
||||
|
} |
||||
|
return defaultValue; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
# LINGYUN.Abp.IdentityServer.WeChatValidator |
||||
|
|
||||
|
废弃模块,模块层次不清晰,微信有多端平台,不同平台授权规则不一致 |
||||
|
|
||||
|
#### 注意 |
||||
|
|
||||
|
|
||||
|
|
||||
|
## 配置使用 |
||||
|
|
||||
|
|
||||
|
```csharp |
||||
|
[DependsOn(typeof(AbpIdentityServerWeChatValidatorModule))] |
||||
|
public class YouProjectModule : AbpModule |
||||
|
{ |
||||
|
// other |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Authorization.Settings; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Options; |
||||
|
using Volo.Abp.Settings; |
||||
|
using Volo.Abp.Threading; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Authorization |
||||
|
{ |
||||
|
public class AbpWeChatAuthorizationOptionsFactory : AbpOptionsFactory<AbpWeChatAuthorizationOptions> |
||||
|
{ |
||||
|
protected ISettingProvider SettingProvider { get; } |
||||
|
|
||||
|
public AbpWeChatAuthorizationOptionsFactory( |
||||
|
ISettingProvider settingProvider, |
||||
|
IEnumerable<IConfigureOptions<AbpWeChatAuthorizationOptions>> setups, |
||||
|
IEnumerable<IPostConfigureOptions<AbpWeChatAuthorizationOptions>> postConfigures) |
||||
|
: base(setups, postConfigures) |
||||
|
{ |
||||
|
SettingProvider = settingProvider; |
||||
|
} |
||||
|
|
||||
|
public override AbpWeChatAuthorizationOptions Create(string name) |
||||
|
{ |
||||
|
var options = base.Create(name); |
||||
|
|
||||
|
OverrideOptions(options); |
||||
|
|
||||
|
return options; |
||||
|
} |
||||
|
|
||||
|
protected virtual void OverrideOptions(AbpWeChatAuthorizationOptions options) |
||||
|
{ |
||||
|
AsyncHelper.RunSync(() => OverrideOptionsAsync(options)); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task OverrideOptionsAsync(AbpWeChatAuthorizationOptions options) |
||||
|
{ |
||||
|
var appId = await SettingProvider.GetOrNullAsync(WeChatAuthorizationSettingNames.AppId); |
||||
|
var appSecret = await SettingProvider.GetOrNullAsync(WeChatAuthorizationSettingNames.AppSecret); |
||||
|
|
||||
|
options.AppId = appId ?? options.AppId; |
||||
|
options.AppSecret = appSecret ?? options.AppSecret; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,12 +0,0 @@ |
|||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace LINGYUN.Abp.WeChat.Authorization |
|
||||
{ |
|
||||
public interface IUserWeChatOpenIdFinder |
|
||||
{ |
|
||||
Task<string> FindByUserIdAsync(Guid userId); |
|
||||
|
|
||||
Task<string> FindByUserNameAsync(string userName); |
|
||||
} |
|
||||
} |
|
||||
@ -1,10 +0,0 @@ |
|||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace LINGYUN.Abp.WeChat.Authorization |
|
||||
{ |
|
||||
public interface IWeChatOpenIdFinder |
|
||||
{ |
|
||||
Task<WeChatOpenId> FindAsync(string code); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,11 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Settings; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Authorization.Settings |
||||
|
{ |
||||
|
public class WeChatAuthorizationSettingNames |
||||
|
{ |
||||
|
private const string Prefix = WeChatSettingNames.Prefix + ".Authorization"; |
||||
|
public static string AppId = Prefix + "." + nameof(AbpWeChatAuthorizationOptions.AppId); |
||||
|
public static string AppSecret = Prefix + "." + nameof(AbpWeChatAuthorizationOptions.AppSecret); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Localization; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.Settings; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Authorization.Settings |
||||
|
{ |
||||
|
public class WeChatAuthorizationSettingProvider : SettingDefinitionProvider |
||||
|
{ |
||||
|
public override void Define(ISettingDefinitionContext context) |
||||
|
{ |
||||
|
context.Add( |
||||
|
new SettingDefinition( |
||||
|
WeChatAuthorizationSettingNames.AppId, "", |
||||
|
L("DisplayName:WeChat.Auth.AppId"), |
||||
|
L("Description:WeChat.Auth.AppId"), |
||||
|
isVisibleToClients: true, |
||||
|
isEncrypted: true), |
||||
|
new SettingDefinition( |
||||
|
WeChatAuthorizationSettingNames.AppSecret, "", |
||||
|
L("DisplayName:WeChat.Auth.AppSecret"), |
||||
|
L("Description:WeChat.Auth.AppSecret"), |
||||
|
isVisibleToClients: true, |
||||
|
isEncrypted: true) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
protected ILocalizableString L(string name) |
||||
|
{ |
||||
|
return LocalizableString.Create<WeChatResource>(name); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
# LINGYUN.Abp.WeChat.Authorization |
||||
|
|
||||
|
废弃模块,模块层次不清晰,微信有多端平台,不同平台授权规则不一致 |
||||
|
|
||||
|
#### 注意 |
||||
|
|
||||
|
|
||||
|
|
||||
|
## 配置使用 |
||||
|
|
||||
|
|
||||
|
```csharp |
||||
|
[DependsOn(typeof(AbpWeChatAuthorizationModule))] |
||||
|
public class YouProjectModule : AbpModule |
||||
|
{ |
||||
|
// other |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<None Remove="LINGYUN\Abp\WeChat\MiniProgram\Localization\Resources\en.json" /> |
||||
|
<None Remove="LINGYUN\Abp\WeChat\MiniProgram\Localization\Resources\zh-Hans.json" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<EmbeddedResource Include="LINGYUN\Abp\WeChat\MiniProgram\Localization\Resources\en.json" /> |
||||
|
<EmbeddedResource Include="LINGYUN\Abp\WeChat\MiniProgram\Localization\Resources\zh-Hans.json" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.WeChat\LINGYUN.Abp.WeChat.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,21 @@ |
|||||
|
namespace LINGYUN.Abp.WeChat.MiniProgram |
||||
|
{ |
||||
|
public class AbpWeChatMiniProgramConsts |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 全局对应的Provider名称
|
||||
|
/// </summary>
|
||||
|
public static string ProviderKey { get; set; } = "WeChat.MiniProgram"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 微信小程序授权类型
|
||||
|
/// </summary>
|
||||
|
public static string GrantType { get; set; } = "wx-mp"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 微信小程序授权方法名称
|
||||
|
/// </summary>
|
||||
|
public static string AuthenticationMethod { get; set; } = "wma"; |
||||
|
public static string HttpClient { get; set; } = "Abp.WeChat.MiniProgram"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Localization; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.DependencyInjection.Extensions; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using System; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.Settings; |
||||
|
using Volo.Abp.VirtualFileSystem; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.MiniProgram |
||||
|
{ |
||||
|
[DependsOn( |
||||
|
typeof(AbpWeChatModule), |
||||
|
typeof(AbpSettingsModule))] |
||||
|
public class AbpWeChatMiniProgramModule : AbpModule |
||||
|
{ |
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
Configure<AbpVirtualFileSystemOptions>(options => |
||||
|
{ |
||||
|
options.FileSets.AddEmbedded<AbpWeChatMiniProgramModule>(); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpLocalizationOptions>(options => |
||||
|
{ |
||||
|
options.Resources |
||||
|
.Get<WeChatResource>() |
||||
|
.AddVirtualJson("/LINGYUN/Abp/WeChat/MiniProgram/Localization/Resources"); |
||||
|
}); |
||||
|
|
||||
|
context.Services.AddHttpClient(AbpWeChatMiniProgramConsts.HttpClient, options => |
||||
|
{ |
||||
|
options.BaseAddress = new Uri("https://api.weixin.qq.com"); |
||||
|
}); |
||||
|
|
||||
|
AddAbpWeChatMiniProgramOptionsFactory(context.Services); |
||||
|
} |
||||
|
|
||||
|
private static void AddAbpWeChatMiniProgramOptionsFactory(IServiceCollection services) |
||||
|
{ |
||||
|
services.Replace(ServiceDescriptor.Transient<IOptionsFactory<AbpWeChatMiniProgramOptions>, AbpWeChatMiniProgramOptionsFactory>()); |
||||
|
services.Replace(ServiceDescriptor.Scoped<IOptions<AbpWeChatMiniProgramOptions>, OptionsManager<AbpWeChatMiniProgramOptions>>()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
namespace LINGYUN.Abp.WeChat.MiniProgram |
||||
|
{ |
||||
|
public class AbpWeChatMiniProgramOptions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 小程序AppId
|
||||
|
/// </summary>
|
||||
|
public string AppId { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 小程序AppSecret
|
||||
|
/// </summary>
|
||||
|
public string AppSecret { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 小程序消息解密Token
|
||||
|
/// </summary>
|
||||
|
public string Token { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 小程序消息解密AESKey
|
||||
|
/// </summary>
|
||||
|
public string EncodingAESKey { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
using LINGYUN.Abp.WeChat.MiniProgram.Settings; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Options; |
||||
|
using Volo.Abp.Settings; |
||||
|
using Volo.Abp.Threading; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.MiniProgram |
||||
|
{ |
||||
|
public class AbpWeChatMiniProgramOptionsFactory : AbpOptionsFactory<AbpWeChatMiniProgramOptions> |
||||
|
{ |
||||
|
protected ISettingProvider SettingProvider { get; } |
||||
|
public AbpWeChatMiniProgramOptionsFactory( |
||||
|
ISettingProvider settingProvider, |
||||
|
IEnumerable<IConfigureOptions<AbpWeChatMiniProgramOptions>> setups, |
||||
|
IEnumerable<IPostConfigureOptions<AbpWeChatMiniProgramOptions>> postConfigures) |
||||
|
: base(setups, postConfigures) |
||||
|
{ |
||||
|
SettingProvider = settingProvider; |
||||
|
} |
||||
|
|
||||
|
public override AbpWeChatMiniProgramOptions Create(string name) |
||||
|
{ |
||||
|
var options = base.Create(name); |
||||
|
|
||||
|
OverrideOptions(options); |
||||
|
|
||||
|
return options; |
||||
|
} |
||||
|
|
||||
|
protected virtual void OverrideOptions(AbpWeChatMiniProgramOptions options) |
||||
|
{ |
||||
|
AsyncHelper.RunSync(() => OverrideOptionsAsync(options)); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task OverrideOptionsAsync(AbpWeChatMiniProgramOptions options) |
||||
|
{ |
||||
|
var appId = await SettingProvider.GetOrNullAsync(WeChatMiniProgramSettingNames.AppId); |
||||
|
var appSecret = await SettingProvider.GetOrNullAsync(WeChatMiniProgramSettingNames.AppSecret); |
||||
|
var token = await SettingProvider.GetOrNullAsync(WeChatMiniProgramSettingNames.Token); |
||||
|
var aesKey = await SettingProvider.GetOrNullAsync(WeChatMiniProgramSettingNames.EncodingAESKey); |
||||
|
|
||||
|
options.AppId = appId ?? options.AppId; |
||||
|
options.AppSecret = appSecret ?? options.AppSecret; |
||||
|
options.Token = token ?? options.Token; |
||||
|
options.EncodingAESKey = aesKey ?? options.EncodingAESKey; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
{ |
||||
|
"culture": "en", |
||||
|
"texts": { |
||||
|
"DisplayName:WeChat.MiniProgram": "WeChat Mini Program", |
||||
|
"Description:WeChat.MiniProgram": "WeChat Mini Program", |
||||
|
"DisplayName:WeChat.MiniProgram.AppId": "AppId", |
||||
|
"Description:WeChat.MiniProgram.AppId": "AppId,See:https://developers.weixin.qq.com/miniprogram/dev/framework/", |
||||
|
"DisplayName:WeChat.MiniProgram.AppSecret": "App Secret", |
||||
|
"Description:WeChat.MiniProgram.AppSecret": "App Secret,See:https://developers.weixin.qq.com/miniprogram/dev/framework/", |
||||
|
"DisplayName:WeChat.MiniProgram.Token": "Token", |
||||
|
"Description:WeChat.MiniProgram.Token": "Token,See:https://developers.weixin.qq.com/miniprogram/dev/framework/", |
||||
|
"DisplayName:WeChat.MiniProgram.EncodingAESKey": "Encoding AES Key", |
||||
|
"Description:WeChat.MiniProgram.EncodingAESKey": "Encoding AES Key,See:https://developers.weixin.qq.com/miniprogram/dev/framework/" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
{ |
||||
|
"culture": "zh-Hans", |
||||
|
"texts": { |
||||
|
"DisplayName:WeChat.MiniProgram": "微信小程序", |
||||
|
"Description:WeChat.MiniProgram": "微信小程序", |
||||
|
"DisplayName:WeChat.MiniProgram.AppId": "小程序AppId", |
||||
|
"Description:WeChat.MiniProgram.AppId": "微信小程序AppId,详情见:https://developers.weixin.qq.com/miniprogram/dev/framework/", |
||||
|
"DisplayName:WeChat.MiniProgram.AppSecret": "小程序AppSecret", |
||||
|
"Description:WeChat.MiniProgram.AppSecret": "微信小程序AppSecret,详情见:https://developers.weixin.qq.com/miniprogram/dev/framework/", |
||||
|
"DisplayName:WeChat.MiniProgram.Token": "小程序Token", |
||||
|
"Description:WeChat.MiniProgram.Token": "微信小程序Token,详情见:https://developers.weixin.qq.com/miniprogram/dev/framework/", |
||||
|
"DisplayName:WeChat.MiniProgram.EncodingAESKey": "小程序EncodingAESKey", |
||||
|
"Description:WeChat.MiniProgram.EncodingAESKey": "微信小程序EncodingAESKey,详情见:https://developers.weixin.qq.com/miniprogram/dev/framework/" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.MiniProgram.Messages |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 小程序模板消息
|
||||
|
/// 详情: https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// 暂时仅实现发送订阅消息
|
||||
|
/// </remarks>
|
||||
|
public interface ISubscribeMessager |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 发送订阅消息
|
||||
|
/// </summary>
|
||||
|
/// <param name="message"></param>
|
||||
|
/// <param name="cancellation"></param>
|
||||
|
/// <returns></returns>
|
||||
|
Task SendAsync(SubscribeMessage message, CancellationToken cancellation = default); |
||||
|
/// <summary>
|
||||
|
/// 发送订阅消息
|
||||
|
/// </summary>
|
||||
|
/// <param name="toUser">用户</param>
|
||||
|
/// <param name="templateId">模板</param>
|
||||
|
/// <param name="page">跳转页面</param>
|
||||
|
/// <param name="lang">语言</param>
|
||||
|
/// <param name="state">类型</param>
|
||||
|
/// <param name="data">数据</param>
|
||||
|
/// <param name="cancellation"></param>
|
||||
|
/// <returns></returns>
|
||||
|
Task SendAsync( |
||||
|
Guid toUser, |
||||
|
string templateId, |
||||
|
string page = "", |
||||
|
string lang = "zh_CN", |
||||
|
string state = "formal", |
||||
|
Dictionary<string, object> data = null, |
||||
|
CancellationToken cancellation = default); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
using Newtonsoft.Json; |
||||
|
using Volo.Abp; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.MiniProgram.Messages |
||||
|
{ |
||||
|
public class SubscribeMessageResponse |
||||
|
{ |
||||
|
[JsonProperty("errcode")] |
||||
|
public int ErrorCode { get; set; } |
||||
|
|
||||
|
[JsonProperty("errmsg")] |
||||
|
public string ErrorMessage { get; set; } |
||||
|
|
||||
|
public bool IsSuccessed => ErrorCode == 0; |
||||
|
|
||||
|
public void ThrowIfNotSuccess() |
||||
|
{ |
||||
|
if (ErrorCode != 0) |
||||
|
{ |
||||
|
throw new AbpException($"Send wechat weapp notification error:{ErrorMessage}"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,106 @@ |
|||||
|
using Newtonsoft.Json; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.MiniProgram.Messages |
||||
|
{ |
||||
|
public class SubscribeMessage |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 接收者(用户)的 openid
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("touser")] |
||||
|
public string ToUser { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 所需下发的订阅模板id
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("template_id")] |
||||
|
public string TemplateId { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 点击模板卡片后的跳转页面,仅限本小程序内的页面。
|
||||
|
/// 支持带参数,(示例index?foo=bar)。
|
||||
|
/// 该字段不填则模板无跳转
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("page")] |
||||
|
public string Page { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 跳转小程序类型:
|
||||
|
/// developer为开发版;trial为体验版;formal为正式版;
|
||||
|
/// 默认为正式版
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("miniprogram_state")] |
||||
|
public string MiniProgramState { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 进入小程序查看”的语言类型,
|
||||
|
/// 支持zh_CN(简体中文)、en_US(英文)、zh_HK(繁体中文)、zh_TW(繁体中文),
|
||||
|
/// 默认为zh_CN
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("lang")] |
||||
|
public string Lang { get; set; } = "zh_CN"; |
||||
|
/// <summary>
|
||||
|
/// 模板内容,
|
||||
|
/// 格式形如 { "key1": { "value": any }, "key2": { "value": any } }
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("data")] |
||||
|
public Dictionary<string, MessageData> Data { get; set; } = new Dictionary<string, MessageData>(); |
||||
|
|
||||
|
public SubscribeMessage() { } |
||||
|
public SubscribeMessage( |
||||
|
string openId, |
||||
|
string templateId, |
||||
|
string redirectPage = "", |
||||
|
string state = "formal", |
||||
|
string miniLang = "zh_CN") |
||||
|
{ |
||||
|
ToUser = openId; |
||||
|
TemplateId = templateId; |
||||
|
Page = redirectPage; |
||||
|
MiniProgramState = state; |
||||
|
Lang = miniLang; |
||||
|
} |
||||
|
|
||||
|
public SubscribeMessage WriteData(string prefix, string key, object value) |
||||
|
{ |
||||
|
// 只截取符合标记的数据
|
||||
|
if (key.StartsWith(prefix)) |
||||
|
{ |
||||
|
key = key.Replace(prefix, ""); |
||||
|
if (!Data.ContainsKey(key)) |
||||
|
{ |
||||
|
Data.Add(key, new MessageData(value)); |
||||
|
} |
||||
|
} |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
public SubscribeMessage WriteData(string prefix, IDictionary<string, object> setData) |
||||
|
{ |
||||
|
foreach (var kv in setData) |
||||
|
{ |
||||
|
WriteData(prefix, kv.Key, kv.Value); |
||||
|
} |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
public SubscribeMessage WriteData(IDictionary<string, object> setData) |
||||
|
{ |
||||
|
foreach (var kv in setData) |
||||
|
{ |
||||
|
if (!Data.ContainsKey(kv.Key)) |
||||
|
{ |
||||
|
Data.Add(kv.Key, new MessageData(kv.Value)); |
||||
|
} |
||||
|
} |
||||
|
return this; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class MessageData |
||||
|
{ |
||||
|
public object Value { get; } |
||||
|
|
||||
|
public MessageData(object value) |
||||
|
{ |
||||
|
Value = value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,119 @@ |
|||||
|
using LINGYUN.Abp.WeChat.OpenId; |
||||
|
using LINGYUN.Abp.WeChat.Token; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Extensions.Logging.Abstractions; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Net.Http; |
||||
|
using System.Text; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Json; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.MiniProgram.Messages |
||||
|
{ |
||||
|
public class SubscribeMessager : ISubscribeMessager, ITransientDependency |
||||
|
{ |
||||
|
public ILogger<SubscribeMessager> Logger { get; set; } |
||||
|
protected IHttpClientFactory HttpClientFactory { get; } |
||||
|
protected IJsonSerializer JsonSerializer { get; } |
||||
|
protected AbpWeChatMiniProgramOptions MiniProgramOptions { get; } |
||||
|
protected IWeChatTokenProvider WeChatTokenProvider { get; } |
||||
|
protected IUserWeChatOpenIdFinder UserWeChatOpenIdFinder { get; } |
||||
|
public SubscribeMessager( |
||||
|
IJsonSerializer jsonSerializer, |
||||
|
IHttpClientFactory httpClientFactory, |
||||
|
IWeChatTokenProvider weChatTokenProvider, |
||||
|
IUserWeChatOpenIdFinder userWeChatOpenIdFinder, |
||||
|
IOptions<AbpWeChatMiniProgramOptions> miniProgramOptions) |
||||
|
{ |
||||
|
JsonSerializer = jsonSerializer; |
||||
|
HttpClientFactory = httpClientFactory; |
||||
|
WeChatTokenProvider = weChatTokenProvider; |
||||
|
UserWeChatOpenIdFinder = userWeChatOpenIdFinder; |
||||
|
MiniProgramOptions = miniProgramOptions.Value; |
||||
|
|
||||
|
Logger = NullLogger<SubscribeMessager>.Instance; |
||||
|
} |
||||
|
|
||||
|
public virtual async Task SendAsync( |
||||
|
Guid toUser, |
||||
|
string templateId, |
||||
|
string page = "", |
||||
|
string lang = "zh_CN", |
||||
|
string state = "formal", |
||||
|
Dictionary<string, object> data = null, |
||||
|
CancellationToken cancellation = default) |
||||
|
{ |
||||
|
var openId = await UserWeChatOpenIdFinder.FindByUserIdAsync(toUser, AbpWeChatMiniProgramConsts.ProviderKey); |
||||
|
if (openId.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
Logger.LogWarning("Can not found openId, Unable to send WeChat message!"); |
||||
|
return; |
||||
|
} |
||||
|
var messageData = new SubscribeMessage(openId, templateId, page, state, lang); |
||||
|
if (data != null) |
||||
|
{ |
||||
|
messageData.WriteData(data); |
||||
|
} |
||||
|
await SendAsync(messageData, cancellation); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task SendAsync(SubscribeMessage message, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var weChatToken = await WeChatTokenProvider.GetTokenAsync(MiniProgramOptions.AppId, MiniProgramOptions.AppSecret, cancellationToken); |
||||
|
var requestParamters = new Dictionary<string, string> |
||||
|
{ |
||||
|
{ "access_token", weChatToken.AccessToken } |
||||
|
}; |
||||
|
var weChatSendNotificationUrl = "https://api.weixin.qq.com"; |
||||
|
var weChatSendNotificationPath = "/cgi-bin/message/subscribe/send"; |
||||
|
var requestUrl = BuildRequestUrl(weChatSendNotificationUrl, weChatSendNotificationPath, requestParamters); |
||||
|
var responseContent = await MakeRequestAndGetResultAsync(requestUrl, message, cancellationToken); |
||||
|
var response = JsonSerializer.Deserialize<SubscribeMessageResponse>(responseContent); |
||||
|
|
||||
|
if (!response.IsSuccessed) |
||||
|
{ |
||||
|
Logger.LogWarning("Send wechat we app subscribe message failed"); |
||||
|
Logger.LogWarning($"Error code: {response.ErrorCode}, message: {response.ErrorMessage}"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<string> MakeRequestAndGetResultAsync(string url, SubscribeMessage message, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var client = HttpClientFactory.CreateClient(AbpWeChatMiniProgramConsts.HttpClient); |
||||
|
var sendDataContent = JsonSerializer.Serialize(message); |
||||
|
var requestContent = new StringContent(sendDataContent); |
||||
|
var requestMessage = new HttpRequestMessage(HttpMethod.Post, url) |
||||
|
{ |
||||
|
Content = requestContent |
||||
|
}; |
||||
|
|
||||
|
var response = await client.SendAsync(requestMessage, cancellationToken); |
||||
|
if (!response.IsSuccessStatusCode) |
||||
|
{ |
||||
|
throw new AbpException($"WeChat send subscribe message http request service returns error! HttpStatusCode: {response.StatusCode}, ReasonPhrase: {response.ReasonPhrase}"); |
||||
|
} |
||||
|
var resultContent = await response.Content.ReadAsStringAsync(); |
||||
|
|
||||
|
return resultContent; |
||||
|
} |
||||
|
|
||||
|
protected virtual string BuildRequestUrl(string uri, string path, IDictionary<string, string> paramters) |
||||
|
{ |
||||
|
var requestUrlBuilder = new StringBuilder(128); |
||||
|
requestUrlBuilder.Append(uri); |
||||
|
requestUrlBuilder.Append(path).Append("?"); |
||||
|
foreach (var paramter in paramters) |
||||
|
{ |
||||
|
requestUrlBuilder.AppendFormat("{0}={1}", paramter.Key, paramter.Value); |
||||
|
requestUrlBuilder.Append("&"); |
||||
|
} |
||||
|
requestUrlBuilder.Remove(requestUrlBuilder.Length - 1, 1); |
||||
|
return requestUrlBuilder.ToString(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Localization; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.Settings; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.MiniProgram.Settings |
||||
|
{ |
||||
|
public class WeChatMiniProgramSettingDefinitionProvider : SettingDefinitionProvider |
||||
|
{ |
||||
|
public override void Define(ISettingDefinitionContext context) |
||||
|
{ |
||||
|
context.Add( |
||||
|
new SettingDefinition( |
||||
|
WeChatMiniProgramSettingNames.AppId, "", |
||||
|
L("DisplayName:WeChat.MiniProgram.AppId"), |
||||
|
L("Description:WeChat.MiniProgram.AppId"), |
||||
|
isVisibleToClients: true, |
||||
|
isEncrypted: true), |
||||
|
new SettingDefinition( |
||||
|
WeChatMiniProgramSettingNames.AppSecret, "", |
||||
|
L("DisplayName:WeChat.MiniProgram.AppSecret"), |
||||
|
L("Description:WeChat.MiniProgram.AppSecret"), |
||||
|
isVisibleToClients: true, |
||||
|
isEncrypted: true), |
||||
|
new SettingDefinition( |
||||
|
WeChatMiniProgramSettingNames.Token, "", |
||||
|
L("DisplayName:WeChat.MiniProgram.Token"), |
||||
|
L("Description:WeChat.MiniProgram.Token"), |
||||
|
isVisibleToClients: true, |
||||
|
isEncrypted: true), |
||||
|
new SettingDefinition( |
||||
|
WeChatMiniProgramSettingNames.EncodingAESKey, "", |
||||
|
L("DisplayName:WeChat.MiniProgram.EncodingAESKey"), |
||||
|
L("Description:WeChat.MiniProgram.EncodingAESKey"), |
||||
|
isVisibleToClients: true, |
||||
|
isEncrypted: true) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
protected ILocalizableString L(string name) |
||||
|
{ |
||||
|
return LocalizableString.Create<WeChatResource>(name); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Settings; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.MiniProgram.Settings |
||||
|
{ |
||||
|
public class WeChatMiniProgramSettingNames |
||||
|
{ |
||||
|
private const string Prefix = WeChatSettingNames.Prefix + ".MiniProgram"; |
||||
|
|
||||
|
public static string AppId = Prefix + "." + nameof(AbpWeChatMiniProgramOptions.AppId); |
||||
|
public static string AppSecret = Prefix + "." + nameof(AbpWeChatMiniProgramOptions.AppSecret); |
||||
|
public static string Token = Prefix + "." + nameof(AbpWeChatMiniProgramOptions.Token); |
||||
|
public static string EncodingAESKey = Prefix + "." + nameof(AbpWeChatMiniProgramOptions.EncodingAESKey); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
# LINGYUN.Abp.WeChat.MiniProgram |
||||
|
|
||||
|
微信小程序SDK集成,考虑是否需要集成[Senparc.Weixin SDK](https://github.com/JeffreySu/WeiXinMPSDK) |
||||
|
|
||||
|
大部分重写的模块都和官方模块名称保持一致,通过命名空间区分,主要是只改写了一小部分或者增加额外的功能 |
||||
|
如果大部分模块代码都重写,或者完全就是扩展模块,才会定义自己的名字 |
||||
|
|
||||
|
#### 注意 |
||||
|
|
||||
|
|
||||
|
|
||||
|
## 配置使用 |
||||
|
|
||||
|
|
||||
|
```csharp |
||||
|
[DependsOn(typeof(AbpWeChatMiniProgramModule))] |
||||
|
public class YouProjectModule : AbpModule |
||||
|
{ |
||||
|
// other |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<None Remove="LINGYUN\Abp\WeChat\Official\Localization\Resources\en.json" /> |
||||
|
<None Remove="LINGYUN\Abp\WeChat\Official\Localization\Resources\zh-Hans.json" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<EmbeddedResource Include="LINGYUN\Abp\WeChat\Official\Localization\Resources\en.json" /> |
||||
|
<EmbeddedResource Include="LINGYUN\Abp\WeChat\Official\Localization\Resources\zh-Hans.json" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.WeChat\LINGYUN.Abp.WeChat.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,22 @@ |
|||||
|
namespace LINGYUN.Abp.WeChat.Official |
||||
|
{ |
||||
|
public class AbpWeChatOfficialConsts |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 微信公众号全局对应的Provider名称
|
||||
|
/// </summary>
|
||||
|
public static string ProviderKey { get; set; } = "WeChat.Official"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 微信公众平台授权类型
|
||||
|
/// </summary>
|
||||
|
public static string GrantType { get; set; } = "wx-op"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 微信公众平台授权方法名称
|
||||
|
/// </summary>
|
||||
|
public static string AuthenticationMethod { get; set; } = "woa"; |
||||
|
|
||||
|
public static string HttpClient { get; set; } = "Abp.WeChat.Official"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Localization; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.DependencyInjection.Extensions; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.VirtualFileSystem; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Official |
||||
|
{ |
||||
|
[DependsOn( |
||||
|
typeof(AbpWeChatModule))] |
||||
|
public class AbpWeChatOfficialModule : AbpModule |
||||
|
{ |
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
Configure<AbpVirtualFileSystemOptions>(options => |
||||
|
{ |
||||
|
options.FileSets.AddEmbedded<AbpWeChatOfficialModule>(); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpLocalizationOptions>(options => |
||||
|
{ |
||||
|
options.Resources |
||||
|
.Get<WeChatResource>() |
||||
|
.AddVirtualJson("/LINGYUN/Abp/WeChat/Official/Localization/Resources"); |
||||
|
}); |
||||
|
|
||||
|
AddAbpWeChatOfficialOptionsFactory(context.Services); |
||||
|
} |
||||
|
|
||||
|
private static void AddAbpWeChatOfficialOptionsFactory(IServiceCollection services) |
||||
|
{ |
||||
|
services.Replace(ServiceDescriptor.Transient<IOptionsFactory<AbpWeChatOfficialOptions>, AbpWeChatOfficialOptionsFactory>()); |
||||
|
services.Replace(ServiceDescriptor.Scoped<IOptions<AbpWeChatOfficialOptions>, OptionsManager<AbpWeChatOfficialOptions>>()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
namespace LINGYUN.Abp.WeChat.Official |
||||
|
{ |
||||
|
public class AbpWeChatOfficialOptions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 公众号服务器消息Url
|
||||
|
/// </summary>
|
||||
|
public string Url { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 公众号AppId
|
||||
|
/// </summary>
|
||||
|
public string AppId { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 公众号AppSecret
|
||||
|
/// </summary>
|
||||
|
public string AppSecret { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 公众号消息解密Token
|
||||
|
/// </summary>
|
||||
|
public string Token { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 公众号消息解密AESKey
|
||||
|
/// </summary>
|
||||
|
public string EncodingAESKey { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Official.Settings; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Options; |
||||
|
using Volo.Abp.Settings; |
||||
|
using Volo.Abp.Threading; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Official |
||||
|
{ |
||||
|
public class AbpWeChatOfficialOptionsFactory : AbpOptionsFactory<AbpWeChatOfficialOptions> |
||||
|
{ |
||||
|
protected ISettingProvider SettingProvider { get; } |
||||
|
public AbpWeChatOfficialOptionsFactory( |
||||
|
ISettingProvider settingProvider, |
||||
|
IEnumerable<IConfigureOptions<AbpWeChatOfficialOptions>> setups, |
||||
|
IEnumerable<IPostConfigureOptions<AbpWeChatOfficialOptions>> postConfigures) |
||||
|
: base(setups, postConfigures) |
||||
|
{ |
||||
|
SettingProvider = settingProvider; |
||||
|
} |
||||
|
|
||||
|
public override AbpWeChatOfficialOptions Create(string name) |
||||
|
{ |
||||
|
var options = base.Create(name); |
||||
|
|
||||
|
OverrideOptions(options); |
||||
|
|
||||
|
return options; |
||||
|
} |
||||
|
|
||||
|
protected virtual void OverrideOptions(AbpWeChatOfficialOptions options) |
||||
|
{ |
||||
|
AsyncHelper.RunSync(() => OverrideOptionsAsync(options)); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task OverrideOptionsAsync(AbpWeChatOfficialOptions options) |
||||
|
{ |
||||
|
var appId = await SettingProvider.GetOrNullAsync(WeChatOfficialSettingNames.AppId); |
||||
|
var appSecret = await SettingProvider.GetOrNullAsync(WeChatOfficialSettingNames.AppSecret); |
||||
|
var url = await SettingProvider.GetOrNullAsync(WeChatOfficialSettingNames.Url); |
||||
|
var token = await SettingProvider.GetOrNullAsync(WeChatOfficialSettingNames.Token); |
||||
|
var aesKey = await SettingProvider.GetOrNullAsync(WeChatOfficialSettingNames.EncodingAESKey); |
||||
|
|
||||
|
options.AppId = appId ?? options.AppId; |
||||
|
options.AppSecret = appSecret ?? options.AppSecret; |
||||
|
options.Url = url ?? options.Url; |
||||
|
options.Token = token ?? options.Token; |
||||
|
options.EncodingAESKey = aesKey ?? options.EncodingAESKey; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
{ |
||||
|
"culture": "en", |
||||
|
"texts": { |
||||
|
"DisplayName:WeChat.Official": "WeChat Official", |
||||
|
"Description:WeChat.Official": "WeChat Official", |
||||
|
"DisplayName:WeChat.Official.AppId": "AppId", |
||||
|
"Description:WeChat.Official.AppId": "AppId,See:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html", |
||||
|
"DisplayName:WeChat.Official.AppSecret": "App Secret", |
||||
|
"Description:WeChat.Official.AppSecret": "App Secret,See:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html", |
||||
|
"DisplayName:WeChat.Official.Url": "Url", |
||||
|
"Description:WeChat.Official.Url": "Url,See:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html", |
||||
|
"DisplayName:WeChat.Official.Token": "Token", |
||||
|
"Description:WeChat.Official.Token": "Token,See:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html", |
||||
|
"DisplayName:WeChat.Official.EncodingAESKey": "Encoding AES Key", |
||||
|
"Description:WeChat.Official.EncodingAESKey": "Encoding AES Key,See:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
{ |
||||
|
"culture": "zh-Hans", |
||||
|
"texts": { |
||||
|
"DisplayName:WeChat.Official": "微信公众号", |
||||
|
"Description:WeChat.Official": "微信公众号", |
||||
|
"DisplayName:WeChat.Official.AppId": "公众号AppId", |
||||
|
"Description:WeChat.Official.AppId": "微信公众号AppId,详情见:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html", |
||||
|
"DisplayName:WeChat.Official.AppSecret": "公众号AppSecret", |
||||
|
"Description:WeChat.Official.AppSecret": "微信公众号AppSecret,详情见:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html", |
||||
|
"DisplayName:WeChat.Official.Url": "公众号Url", |
||||
|
"Description:WeChat.Official.Url": "微信公众号Url,详情见:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html", |
||||
|
"DisplayName:WeChat.Official.Token": "公众号Token", |
||||
|
"Description:WeChat.Official.Token": "微信公众号Token,详情见:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html", |
||||
|
"DisplayName:WeChat.Official.EncodingAESKey": "公众号EncodingAESKey", |
||||
|
"Description:WeChat.Official.EncodingAESKey": "微信公众号EncodingAESKey,详情见:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Localization; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.Settings; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Official.Settings |
||||
|
{ |
||||
|
public class WeChatOfficialSettingDefinitionProvider : SettingDefinitionProvider |
||||
|
{ |
||||
|
public override void Define(ISettingDefinitionContext context) |
||||
|
{ |
||||
|
context.Add( |
||||
|
new SettingDefinition( |
||||
|
WeChatOfficialSettingNames.AppId, "", |
||||
|
L("DisplayName:WeChat.Official.AppId"), |
||||
|
L("Description:WeChat.Official.AppId"), |
||||
|
isVisibleToClients: true, |
||||
|
isEncrypted: true), |
||||
|
new SettingDefinition( |
||||
|
WeChatOfficialSettingNames.AppSecret, "", |
||||
|
L("DisplayName:WeChat.Official.AppSecret"), |
||||
|
L("Description:WeChat.Official.AppSecret"), |
||||
|
isVisibleToClients: true, |
||||
|
isEncrypted: true), |
||||
|
new SettingDefinition( |
||||
|
WeChatOfficialSettingNames.Url, "", |
||||
|
L("DisplayName:WeChat.Official.Url"), |
||||
|
L("Description:WeChat.Official.Url"), |
||||
|
isVisibleToClients: true, |
||||
|
isEncrypted: false), |
||||
|
new SettingDefinition( |
||||
|
WeChatOfficialSettingNames.Token, "", |
||||
|
L("DisplayName:WeChat.Official.Token"), |
||||
|
L("Description:WeChat.Official.Token"), |
||||
|
isVisibleToClients: true, |
||||
|
isEncrypted: true), |
||||
|
new SettingDefinition( |
||||
|
WeChatOfficialSettingNames.EncodingAESKey, "", |
||||
|
L("DisplayName:WeChat.Official.EncodingAESKey"), |
||||
|
L("Description:WeChat.Official.EncodingAESKey"), |
||||
|
isVisibleToClients: true, |
||||
|
isEncrypted: true) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
protected ILocalizableString L(string name) |
||||
|
{ |
||||
|
return LocalizableString.Create<WeChatResource>(name); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Settings; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Official.Settings |
||||
|
{ |
||||
|
public class WeChatOfficialSettingNames |
||||
|
{ |
||||
|
private const string Prefix = WeChatSettingNames.Prefix + ".Official"; |
||||
|
|
||||
|
public static string AppId = Prefix + "." + nameof(AbpWeChatOfficialOptions.AppId); |
||||
|
public static string AppSecret = Prefix + "." + nameof(AbpWeChatOfficialOptions.AppSecret); |
||||
|
public static string Url = Prefix + "." + nameof(AbpWeChatOfficialOptions.Url); |
||||
|
public static string Token = Prefix + "." + nameof(AbpWeChatOfficialOptions.Token); |
||||
|
public static string EncodingAESKey = Prefix + "." + nameof(AbpWeChatOfficialOptions.EncodingAESKey); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
# LINGYUN.Abp.WeChat.Official |
||||
|
|
||||
|
微信公众号SDK集成,考虑是否需要集成[Senparc.Weixin SDK](https://github.com/JeffreySu/WeiXinMPSDK) |
||||
|
|
||||
|
大部分重写的模块都和官方模块名称保持一致,通过命名空间区分,主要是只改写了一小部分或者增加额外的功能 |
||||
|
如果大部分模块代码都重写,或者完全就是扩展模块,才会定义自己的名字 |
||||
|
|
||||
|
#### 注意 |
||||
|
|
||||
|
|
||||
|
|
||||
|
## 配置使用 |
||||
|
|
||||
|
|
||||
|
```csharp |
||||
|
[DependsOn(typeof(AbpWeChatOfficialModule))] |
||||
|
public class YouProjectModule : AbpModule |
||||
|
{ |
||||
|
// other |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netcoreapp3.1</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<None Remove="LINGYUN\Abp\WeChat\SettingManagement\Localization\Resources\en.json" /> |
||||
|
<None Remove="LINGYUN\Abp\WeChat\SettingManagement\Localization\Resources\zh-Hans.json" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<EmbeddedResource Include="LINGYUN\Abp\WeChat\SettingManagement\Localization\Resources\en.json" /> |
||||
|
<EmbeddedResource Include="LINGYUN\Abp\WeChat\SettingManagement\Localization\Resources\zh-Hans.json" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Volo.Abp.AspNetCore.Mvc" Version="3.3.0" /> |
||||
|
<PackageReference Include="Volo.Abp.SettingManagement.Domain" Version="3.3.0" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\settings\LINGYUN.Abp.SettingManagement.Application.Contracts\LINGYUN.Abp.SettingManagement.Application.Contracts.csproj" /> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.WeChat.MiniProgram\LINGYUN.Abp.WeChat.MiniProgram.csproj" /> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.WeChat.Official\LINGYUN.Abp.WeChat.Official.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,51 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Localization; |
||||
|
using LINGYUN.Abp.WeChat.MiniProgram; |
||||
|
using LINGYUN.Abp.WeChat.Official; |
||||
|
using Localization.Resources.AbpUi; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Volo.Abp.AspNetCore.Mvc; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.VirtualFileSystem; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.SettingManagement |
||||
|
{ |
||||
|
[DependsOn( |
||||
|
typeof(AbpWeChatOfficialModule), |
||||
|
typeof(AbpWeChatMiniProgramModule), |
||||
|
typeof(AbpAspNetCoreMvcModule))] |
||||
|
public class AbpWeChatSettingManagementModule : AbpModule |
||||
|
{ |
||||
|
public override void PreConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
PreConfigure<IMvcBuilder>(mvcBuilder => |
||||
|
{ |
||||
|
mvcBuilder.AddApplicationPartIfNotExists(typeof(AbpWeChatSettingManagementModule).Assembly); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
Configure<AbpVirtualFileSystemOptions>(options => |
||||
|
{ |
||||
|
options.FileSets.AddEmbedded<AbpWeChatSettingManagementModule>(); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpLocalizationOptions>(options => |
||||
|
{ |
||||
|
options.Resources |
||||
|
.Get<WeChatResource>() |
||||
|
.AddVirtualJson("/LINGYUN/Abp/WeChat/SettingManagement/Localization/Resources"); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpLocalizationOptions>(options => |
||||
|
{ |
||||
|
options.Resources |
||||
|
.Get<WeChatResource>() |
||||
|
.AddBaseTypes( |
||||
|
typeof(AbpUiResource) |
||||
|
); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
using LINGYUN.Abp.SettingManagement; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Application.Dtos; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.SettingManagement |
||||
|
{ |
||||
|
public interface IWeChatSettingAppService |
||||
|
{ |
||||
|
Task<ListResultDto<SettingGroupDto>> GetAllForGlobalAsync(); |
||||
|
|
||||
|
Task<ListResultDto<SettingGroupDto>> GetAllForCurrentTenantAsync(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"culture": "en", |
||||
|
"texts": { |
||||
|
"Permission:WeChat.Official": "Official", |
||||
|
"Permission:WeChat.MiniProgram": "Mini Program" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"culture": "zh-Hans", |
||||
|
"texts": { |
||||
|
"Permission:WeChat.Official": "公众平台", |
||||
|
"Permission:WeChat.MiniProgram": "小程序" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,116 @@ |
|||||
|
using LINGYUN.Abp.SettingManagement; |
||||
|
using LINGYUN.Abp.WeChat.Localization; |
||||
|
using LINGYUN.Abp.WeChat.MiniProgram.Settings; |
||||
|
using LINGYUN.Abp.WeChat.Official.Settings; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Application.Dtos; |
||||
|
using Volo.Abp.Application.Services; |
||||
|
using Volo.Abp.Authorization.Permissions; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
using Volo.Abp.SettingManagement; |
||||
|
using Volo.Abp.Settings; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.SettingManagement |
||||
|
{ |
||||
|
public class WeChatSettingAppService : ApplicationService, IWeChatSettingAppService |
||||
|
{ |
||||
|
protected ISettingManager SettingManager { get; } |
||||
|
protected IPermissionChecker PermissionChecker { get; } |
||||
|
protected ISettingDefinitionManager SettingDefinitionManager { get; } |
||||
|
|
||||
|
public WeChatSettingAppService( |
||||
|
ISettingManager settingManager, |
||||
|
IPermissionChecker permissionChecker, |
||||
|
ISettingDefinitionManager settingDefinitionManager) |
||||
|
{ |
||||
|
SettingManager = settingManager; |
||||
|
PermissionChecker = permissionChecker; |
||||
|
SettingDefinitionManager = settingDefinitionManager; |
||||
|
LocalizationResource = typeof(WeChatResource); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<ListResultDto<SettingGroupDto>> GetAllForCurrentTenantAsync() |
||||
|
{ |
||||
|
return await GetAllForProviderAsync(TenantSettingValueProvider.ProviderName, CurrentTenant.GetId().ToString()); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<ListResultDto<SettingGroupDto>> GetAllForGlobalAsync() |
||||
|
{ |
||||
|
return await GetAllForProviderAsync(GlobalSettingValueProvider.ProviderName, null); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<ListResultDto<SettingGroupDto>> GetAllForProviderAsync(string providerName, string providerKey) |
||||
|
{ |
||||
|
var settingGroups = new List<SettingGroupDto>(); |
||||
|
var wechatSettingGroup = new SettingGroupDto(L["DisplayName:WeChat"], L["Description:WeChat"]); |
||||
|
|
||||
|
// 无权限返回空结果,直接报错的话,网关聚合会抛出异常
|
||||
|
if (await PermissionChecker.IsGrantedAsync(WeChatSettingPermissionNames.Official)) |
||||
|
{ |
||||
|
#region 公众号
|
||||
|
|
||||
|
var officialSetting = wechatSettingGroup.AddSetting(L["DisplayName:WeChat.Official"], L["Description:WeChat.Official"]); |
||||
|
officialSetting.AddDetail( |
||||
|
SettingDefinitionManager.Get(WeChatOfficialSettingNames.AppId), |
||||
|
StringLocalizerFactory, |
||||
|
await SettingManager.GetOrNullAsync(WeChatOfficialSettingNames.AppId, providerName, providerKey), |
||||
|
ValueType.String); |
||||
|
officialSetting.AddDetail( |
||||
|
SettingDefinitionManager.Get(WeChatOfficialSettingNames.AppSecret), |
||||
|
StringLocalizerFactory, |
||||
|
await SettingManager.GetOrNullAsync(WeChatOfficialSettingNames.AppSecret, providerName, providerKey), |
||||
|
ValueType.String); |
||||
|
officialSetting.AddDetail( |
||||
|
SettingDefinitionManager.Get(WeChatOfficialSettingNames.Url), |
||||
|
StringLocalizerFactory, |
||||
|
await SettingManager.GetOrNullAsync(WeChatOfficialSettingNames.Url, providerName, providerKey), |
||||
|
ValueType.String); |
||||
|
officialSetting.AddDetail( |
||||
|
SettingDefinitionManager.Get(WeChatOfficialSettingNames.Token), |
||||
|
StringLocalizerFactory, |
||||
|
await SettingManager.GetOrNullAsync(WeChatOfficialSettingNames.Token, providerName, providerKey), |
||||
|
ValueType.String); |
||||
|
officialSetting.AddDetail( |
||||
|
SettingDefinitionManager.Get(WeChatOfficialSettingNames.EncodingAESKey), |
||||
|
StringLocalizerFactory, |
||||
|
await SettingManager.GetOrNullAsync(WeChatOfficialSettingNames.EncodingAESKey, providerName, providerKey), |
||||
|
ValueType.String); |
||||
|
|
||||
|
#endregion
|
||||
|
} |
||||
|
|
||||
|
if (await PermissionChecker.IsGrantedAsync(WeChatSettingPermissionNames.MiniProgram)) |
||||
|
{ |
||||
|
#region 小程序
|
||||
|
|
||||
|
var miniProgramSetting = wechatSettingGroup.AddSetting(L["DisplayName:WeChat.MiniProgram"], L["Description:WeChat.MiniProgram"]); |
||||
|
miniProgramSetting.AddDetail( |
||||
|
SettingDefinitionManager.Get(WeChatMiniProgramSettingNames.AppId), |
||||
|
StringLocalizerFactory, |
||||
|
await SettingManager.GetOrNullAsync(WeChatMiniProgramSettingNames.AppId, providerName, providerKey), |
||||
|
ValueType.String); |
||||
|
miniProgramSetting.AddDetail( |
||||
|
SettingDefinitionManager.Get(WeChatMiniProgramSettingNames.AppSecret), |
||||
|
StringLocalizerFactory, |
||||
|
await SettingManager.GetOrNullAsync(WeChatMiniProgramSettingNames.AppSecret, providerName, providerKey), |
||||
|
ValueType.String); |
||||
|
miniProgramSetting.AddDetail( |
||||
|
SettingDefinitionManager.Get(WeChatMiniProgramSettingNames.Token), |
||||
|
StringLocalizerFactory, |
||||
|
await SettingManager.GetOrNullAsync(WeChatMiniProgramSettingNames.Token, providerName, providerKey), |
||||
|
ValueType.String); |
||||
|
miniProgramSetting.AddDetail( |
||||
|
SettingDefinitionManager.Get(WeChatMiniProgramSettingNames.EncodingAESKey), |
||||
|
StringLocalizerFactory, |
||||
|
await SettingManager.GetOrNullAsync(WeChatMiniProgramSettingNames.EncodingAESKey, providerName, providerKey), |
||||
|
ValueType.String); |
||||
|
|
||||
|
#endregion
|
||||
|
} |
||||
|
|
||||
|
settingGroups.Add(wechatSettingGroup); |
||||
|
return new ListResultDto<SettingGroupDto>(settingGroups); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
using LINGYUN.Abp.SettingManagement; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp; |
||||
|
using Volo.Abp.Application.Dtos; |
||||
|
using Volo.Abp.AspNetCore.Mvc; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.SettingManagement |
||||
|
{ |
||||
|
[RemoteService(Name = AbpSettingManagementRemoteServiceConsts.RemoteServiceName)] |
||||
|
[Area("settingManagement")] |
||||
|
[Route("api/setting-management/wechat")] |
||||
|
public class WeChatSettingController : AbpController, IWeChatSettingAppService |
||||
|
{ |
||||
|
protected IWeChatSettingAppService WeChatSettingAppService { get; } |
||||
|
|
||||
|
public WeChatSettingController( |
||||
|
IWeChatSettingAppService weChatSettingAppService) |
||||
|
{ |
||||
|
WeChatSettingAppService = weChatSettingAppService; |
||||
|
} |
||||
|
|
||||
|
[HttpGet] |
||||
|
[Route("by-current-tenant")] |
||||
|
public virtual async Task<ListResultDto<SettingGroupDto>> GetAllForCurrentTenantAsync() |
||||
|
{ |
||||
|
return await WeChatSettingAppService.GetAllForCurrentTenantAsync(); |
||||
|
} |
||||
|
|
||||
|
[HttpGet] |
||||
|
[Route("by-global")] |
||||
|
public virtual async Task<ListResultDto<SettingGroupDto>> GetAllForGlobalAsync() |
||||
|
{ |
||||
|
return await WeChatSettingAppService.GetAllForGlobalAsync(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Localization; |
||||
|
using Volo.Abp.Authorization.Permissions; |
||||
|
using Volo.Abp.Localization; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.SettingManagement |
||||
|
{ |
||||
|
public class WeChatSettingPermissionDefinitionProvider : PermissionDefinitionProvider |
||||
|
{ |
||||
|
public override void Define(IPermissionDefinitionContext context) |
||||
|
{ |
||||
|
var wechatGroup = context.AddGroup( |
||||
|
WeChatSettingPermissionNames.GroupName, |
||||
|
L("Permission:WeChat")); |
||||
|
|
||||
|
wechatGroup.AddPermission( |
||||
|
WeChatSettingPermissionNames.Official, L("Permission:WeChat.Official")); |
||||
|
|
||||
|
wechatGroup.AddPermission( |
||||
|
WeChatSettingPermissionNames.MiniProgram, L("Permission:WeChat.MiniProgram")); |
||||
|
} |
||||
|
|
||||
|
protected LocalizableString L(string name) |
||||
|
{ |
||||
|
return LocalizableString.Create<WeChatResource>(name); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
namespace LINGYUN.Abp.WeChat.SettingManagement |
||||
|
{ |
||||
|
public class WeChatSettingPermissionNames |
||||
|
{ |
||||
|
public const string GroupName = "Abp.WeChat"; |
||||
|
|
||||
|
public const string Official = GroupName + ".Official"; |
||||
|
public const string MiniProgram = GroupName + ".MiniProgram"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
namespace LINGYUN.Abp.WeChat |
||||
|
{ |
||||
|
public class AbpWeChatGlobalConsts |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 微信授权名称
|
||||
|
/// </summary>
|
||||
|
public static string AuthenticationScheme { get; set; }= "WeChat"; |
||||
|
/// <summary>
|
||||
|
/// 微信授权Token参数名称
|
||||
|
/// </summary>
|
||||
|
public static string TokenName { get; set; }= "code"; |
||||
|
/// <summary>
|
||||
|
/// 微信授权显示名称
|
||||
|
/// </summary>
|
||||
|
public static string DisplayName { get; set; } = "WeChat"; |
||||
|
} |
||||
|
} |
||||
@ -1,6 +1,9 @@ |
|||||
{ |
{ |
||||
"culture": "en", |
"culture": "en", |
||||
"texts": { |
"texts": { |
||||
"Features:WeChat": "WeChat" |
"Features:WeChat": "WeChat", |
||||
|
"Permission:WeChat": "WeChat", |
||||
|
"DisplayName:WeChat": "WeChat", |
||||
|
"Description:WeChat": "WeChat" |
||||
} |
} |
||||
} |
} |
||||
@ -1,6 +1,9 @@ |
|||||
{ |
{ |
||||
"culture": "zh-Hans", |
"culture": "zh-Hans", |
||||
"texts": { |
"texts": { |
||||
"Features:WeChat": "微信开发" |
"Features:WeChat": "微信开发", |
||||
|
"Permission:WeChat": "微信开发", |
||||
|
"DisplayName:WeChat": "微信开发平台", |
||||
|
"Description:WeChat": "微信开发平台" |
||||
} |
} |
||||
} |
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.OpenId |
||||
|
{ |
||||
|
public interface IUserWeChatOpenIdFinder |
||||
|
{ |
||||
|
Task<string> FindByUserIdAsync(Guid userId, string provider); |
||||
|
|
||||
|
Task<string> FindByUserNameAsync(string userName, string provider); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.OpenId |
||||
|
{ |
||||
|
public interface IWeChatOpenIdFinder |
||||
|
{ |
||||
|
Task<WeChatOpenId> FindAsync(string code, string appId, string appSecret); |
||||
|
} |
||||
|
} |
||||
@ -1,4 +1,4 @@ |
|||||
namespace LINGYUN.Abp.WeChat.Authorization |
namespace LINGYUN.Abp.WeChat.OpenId |
||||
{ |
{ |
||||
public class WeChatOpenId |
public class WeChatOpenId |
||||
{ |
{ |
||||
@ -1,4 +1,4 @@ |
|||||
namespace LINGYUN.Abp.WeChat.Authorization |
namespace LINGYUN.Abp.WeChat.OpenId |
||||
{ |
{ |
||||
public class WeChatOpenIdRequest |
public class WeChatOpenIdRequest |
||||
{ |
{ |
||||
@ -0,0 +1,48 @@ |
|||||
|
namespace LINGYUN.Abp.WeChat.Security.Claims |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 微信认证身份类型,可以像 <see cref="Volo.Abp.Security.Claims.AbpClaimTypes"/> 自行配置
|
||||
|
/// <br />
|
||||
|
/// See: <see cref="https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html"/>
|
||||
|
/// </summary>
|
||||
|
public class AbpWeChatClaimTypes |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 用户的唯一标识
|
||||
|
/// </summary>
|
||||
|
public static string OpenId { get; set; } = "wx-openid"; // 可变更
|
||||
|
/// <summary>
|
||||
|
/// 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。
|
||||
|
/// </summary>
|
||||
|
public static string UnionId { get; set; } = "wx-unionid"; //可变更
|
||||
|
/// <summary>
|
||||
|
/// 用户昵称
|
||||
|
/// </summary>
|
||||
|
public static string NickName { get; set; } = "nickname"; |
||||
|
/// <summary>
|
||||
|
/// 用户的性别,值为1时是男性,值为2时是女性,值为0时是未知
|
||||
|
/// </summary>
|
||||
|
public static string Sex { get; set; } = "sex"; |
||||
|
/// <summary>
|
||||
|
/// 国家,如中国为CN
|
||||
|
/// </summary>
|
||||
|
public static string Country { get; set; } = "country"; |
||||
|
/// <summary>
|
||||
|
/// 用户个人资料填写的省份
|
||||
|
/// </summary>
|
||||
|
public static string Province { get; set; } = "province"; |
||||
|
/// <summary>
|
||||
|
/// 普通用户个人资料填写的城市
|
||||
|
/// </summary>
|
||||
|
public static string City { get; set; } = "city"; |
||||
|
/// <summary>
|
||||
|
/// 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空。
|
||||
|
/// 若用户更换头像,原有头像URL将失效。
|
||||
|
/// </summary>
|
||||
|
public static string AvatarUrl { get; set; } = "avatar"; |
||||
|
/// <summary>
|
||||
|
/// 用户特权信息,json 数组,如微信沃卡用户为(chinaunicom)
|
||||
|
/// </summary>
|
||||
|
public static string Privilege { get; set; } = "privilege"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
namespace LINGYUN.Abp.WeChat.Settings |
||||
|
{ |
||||
|
public static class WeChatSettingNames |
||||
|
{ |
||||
|
public const string Prefix = "Abp.WeChat"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Token |
||||
|
{ |
||||
|
public interface IWeChatTokenProvider |
||||
|
{ |
||||
|
Task<WeChatToken> GetTokenAsync(string appId, string appSecret, CancellationToken cancellationToken = default); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
namespace LINGYUN.Abp.WeChat.Token |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 微信令牌
|
||||
|
/// </summary>
|
||||
|
public class WeChatToken |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 访问令牌
|
||||
|
/// </summary>
|
||||
|
public string AccessToken { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 过期时间,单位(s)
|
||||
|
/// </summary>
|
||||
|
public int ExpiresIn { get; set; } |
||||
|
public WeChatToken() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
public WeChatToken(string token, int expiresIn) |
||||
|
{ |
||||
|
AccessToken = token; |
||||
|
ExpiresIn = expiresIn; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
namespace LINGYUN.Abp.WeChat.Token |
||||
|
{ |
||||
|
public class WeChatTokenCacheItem |
||||
|
{ |
||||
|
public string AppId { get; set; } |
||||
|
|
||||
|
public WeChatToken WeChatToken { get; set; } |
||||
|
public WeChatTokenCacheItem() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public WeChatTokenCacheItem(string appId, WeChatToken weChatToken) |
||||
|
{ |
||||
|
AppId = appId; |
||||
|
WeChatToken = weChatToken; |
||||
|
} |
||||
|
|
||||
|
public static string CalculateCacheKey(string provider, string appId) |
||||
|
{ |
||||
|
return "p:" + provider + ",o:" + appId; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
using Microsoft.Extensions.Caching.Distributed; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Extensions.Logging.Abstractions; |
||||
|
using System; |
||||
|
using System.Net.Http; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Caching; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Json; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Token |
||||
|
{ |
||||
|
public class WeChatTokenProvider : IWeChatTokenProvider, ISingletonDependency |
||||
|
{ |
||||
|
public ILogger<WeChatTokenProvider> Logger { get; set; } |
||||
|
protected IHttpClientFactory HttpClientFactory { get; } |
||||
|
protected IJsonSerializer JsonSerializer { get; } |
||||
|
protected IDistributedCache<WeChatTokenCacheItem> Cache { get; } |
||||
|
public WeChatTokenProvider( |
||||
|
IJsonSerializer jsonSerializer, |
||||
|
IHttpClientFactory httpClientFactory, |
||||
|
IDistributedCache<WeChatTokenCacheItem> cache) |
||||
|
{ |
||||
|
JsonSerializer = jsonSerializer; |
||||
|
HttpClientFactory = httpClientFactory; |
||||
|
|
||||
|
Cache = cache; |
||||
|
|
||||
|
Logger = NullLogger<WeChatTokenProvider>.Instance; |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<WeChatToken> GetTokenAsync( |
||||
|
string appId, |
||||
|
string appSecret, |
||||
|
CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
return (await GetCacheItemAsync("WeChatToken", appId, appSecret, cancellationToken)).WeChatToken; |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<WeChatTokenCacheItem> GetCacheItemAsync( |
||||
|
string provider, |
||||
|
string appId, |
||||
|
string appSecret, |
||||
|
CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var cacheKey = WeChatTokenCacheItem.CalculateCacheKey(provider, appId); |
||||
|
|
||||
|
Logger.LogDebug($"WeChatTokenProvider.GetCacheItemAsync: {cacheKey}"); |
||||
|
|
||||
|
var cacheItem = await Cache.GetAsync(cacheKey, token: cancellationToken); |
||||
|
|
||||
|
if (cacheItem != null) |
||||
|
{ |
||||
|
Logger.LogDebug($"Found in the cache: {cacheKey}"); |
||||
|
return cacheItem; |
||||
|
} |
||||
|
|
||||
|
Logger.LogDebug($"Not found in the cache, getting from the httpClient: {cacheKey}"); |
||||
|
|
||||
|
var client = HttpClientFactory.CreateClient("WeChatRequestClient"); |
||||
|
|
||||
|
var request = new WeChatTokenRequest |
||||
|
{ |
||||
|
BaseUrl = client.BaseAddress.AbsoluteUri, |
||||
|
AppSecret = appSecret, |
||||
|
AppId = appId, |
||||
|
GrantType = "client_credential" |
||||
|
}; |
||||
|
|
||||
|
var response = await client.RequestWeChatCodeTokenAsync(request, cancellationToken); |
||||
|
var responseContent = await response.Content.ReadAsStringAsync(); |
||||
|
var weChatTokenResponse = JsonSerializer.Deserialize<WeChatTokenResponse>(responseContent); |
||||
|
var weChatToken = weChatTokenResponse.ToWeChatToken(); |
||||
|
cacheItem = new WeChatTokenCacheItem(appId, weChatToken); |
||||
|
|
||||
|
Logger.LogDebug($"Setting the cache item: {cacheKey}"); |
||||
|
|
||||
|
var cacheOptions = new DistributedCacheEntryOptions |
||||
|
{ |
||||
|
// 设置绝对过期时间为Token有效期剩余的二分钟
|
||||
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(weChatToken.ExpiresIn - 120) |
||||
|
}; |
||||
|
|
||||
|
await Cache.SetAsync(cacheKey, cacheItem, cacheOptions, token: cancellationToken); |
||||
|
|
||||
|
Logger.LogDebug($"Finished setting the cache item: {cacheKey}"); |
||||
|
|
||||
|
return cacheItem; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
namespace LINGYUN.Abp.WeChat.Token |
||||
|
{ |
||||
|
public class WeChatTokenRequest |
||||
|
{ |
||||
|
public string BaseUrl { get; set; } |
||||
|
public string GrantType { get; set; } |
||||
|
public string AppId { get; set; } |
||||
|
public string AppSecret { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
using Newtonsoft.Json; |
||||
|
using Volo.Abp; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Token |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 微信访问令牌返回对象
|
||||
|
/// </summary>
|
||||
|
public class WeChatTokenResponse |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 错误码
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("errcode")] |
||||
|
public int ErrorCode { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 错误消息
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("errmsg")] |
||||
|
public string ErrorMessage { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 访问令牌
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("access_token")] |
||||
|
public string AccessToken { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 过期时间,单位(s)
|
||||
|
/// </summary>
|
||||
|
[JsonProperty("expires_in")] |
||||
|
public int ExpiresIn { get; set; } |
||||
|
|
||||
|
public WeChatToken ToWeChatToken() |
||||
|
{ |
||||
|
if(ErrorCode != 0) |
||||
|
{ |
||||
|
throw new AbpException(ErrorMessage); |
||||
|
} |
||||
|
return new WeChatToken(AccessToken, ExpiresIn); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
using LINGYUN.Abp.WeChat.OpenId; |
||||
|
using LINGYUN.Abp.WeChat.Token; |
||||
|
using System.Text; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace System.Net.Http |
||||
|
{ |
||||
|
public static class HttpClientWeChatTokenRequestExtensions |
||||
|
{ |
||||
|
public static async Task<HttpResponseMessage> RequestWeChatCodeTokenAsync(this HttpMessageInvoker client, WeChatTokenRequest request, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var getResuestUrlBuilder = new StringBuilder(); |
||||
|
getResuestUrlBuilder.Append(request.BaseUrl); |
||||
|
getResuestUrlBuilder.Append("cgi-bin/token"); |
||||
|
getResuestUrlBuilder.Append("?grant_type=client_credential"); |
||||
|
getResuestUrlBuilder.AppendFormat("&appid={0}", request.AppId); |
||||
|
getResuestUrlBuilder.AppendFormat("&secret={0}", request.AppSecret); |
||||
|
|
||||
|
var getRequest = new HttpRequestMessage(HttpMethod.Get, getResuestUrlBuilder.ToString()); |
||||
|
HttpResponseMessage httpResponse; |
||||
|
|
||||
|
httpResponse = await client.SendAsync(getRequest, cancellationToken).ConfigureAwait(false); |
||||
|
|
||||
|
return httpResponse; |
||||
|
} |
||||
|
|
||||
|
public static async Task<HttpResponseMessage> RequestWeChatOpenIdAsync(this HttpMessageInvoker client, WeChatOpenIdRequest request, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var getResuestUrlBuiilder = new StringBuilder(); |
||||
|
getResuestUrlBuiilder.Append(request.BaseUrl); |
||||
|
getResuestUrlBuiilder.Append("sns/jscode2session"); |
||||
|
getResuestUrlBuiilder.AppendFormat("?appid={0}", request.AppId); |
||||
|
getResuestUrlBuiilder.AppendFormat("&secret={0}", request.Secret); |
||||
|
getResuestUrlBuiilder.AppendFormat("&js_code={0}", request.Code); |
||||
|
getResuestUrlBuiilder.Append("&grant_type=authorization_code"); |
||||
|
|
||||
|
var getRequest = new HttpRequestMessage(HttpMethod.Get, getResuestUrlBuiilder.ToString()); |
||||
|
HttpResponseMessage httpResponse; |
||||
|
|
||||
|
httpResponse = await client.SendAsync(getRequest, cancellationToken).ConfigureAwait(false); |
||||
|
|
||||
|
return httpResponse; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue