52 changed files with 1171 additions and 345 deletions
@ -1,12 +0,0 @@ |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"DisplayName:TenantCloud.SmsSetting": "Sms Setting", |
|||
"DisplayName:AppId": "App Id", |
|||
"Description:AppId": "AppId applied on Tencent Cloud SMS control platform.", |
|||
"DisplayName:DefaultSignName": "Default Sign Name", |
|||
"Description:DefaultSignName": "Default signname if no signname is specified in the SMS message body", |
|||
"DisplayName:DefaultTemplateId": "Default Template Id", |
|||
"Description:DefaultTemplateId": "Default template id if the Template Id number is not specified in the SMS message body." |
|||
} |
|||
} |
|||
@ -1,12 +0,0 @@ |
|||
{ |
|||
"culture": "zh-Hans", |
|||
"texts": { |
|||
"DisplayName:TenantCloud.SmsSetting": "短信设置", |
|||
"DisplayName:AppId": "应用Id", |
|||
"Description:AppId": "在腾讯云短信控制平台申请的应用标识", |
|||
"DisplayName:DefaultSignName": "默认签名", |
|||
"Description:DefaultSignName": "当短信消息体未指定签名时的默认签名", |
|||
"DisplayName:DefaultTemplateId": "默认模板", |
|||
"Description:DefaultTemplateId": "当短信消息体未指定模板号时的默认模板标识" |
|||
} |
|||
} |
|||
@ -1,23 +0,0 @@ |
|||
using LINGYUN.Abp.Tencent.Settings; |
|||
|
|||
namespace LINGYUN.Abp.Sms.Tencent.Settings |
|||
{ |
|||
public static class TencentCloudSmsSettingNames |
|||
{ |
|||
public const string Prefix = TencentCloudSettingNames.Prefix + ".Sms"; |
|||
|
|||
/// <summary>
|
|||
/// 短信 SdkAppId
|
|||
/// 在 短信控制台 添加应用后生成的实际 SdkAppId,示例如1400006666。
|
|||
/// </summary>
|
|||
public const string AppId = Prefix + ".Domain"; |
|||
/// <summary>
|
|||
/// 短信签名内容
|
|||
/// </summary>
|
|||
public const string DefaultSignName = Prefix + ".DefaultSignName"; |
|||
/// <summary>
|
|||
/// 默认短信模板 ID
|
|||
/// </summary>
|
|||
public const string DefaultTemplateId = Prefix + ".DefaultTemplateId"; |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,19 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\common.props" /> |
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<EmbeddedResource Include="LINGYUN\Abp\Tencent\QQ\Localization\*.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.Tencent\LINGYUN.Abp.Tencent.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,30 @@ |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.Tencent.QQ; |
|||
|
|||
public class AbpTencentQQCacheItem |
|||
{ |
|||
public const string CacheKeyFormat = "pn:{0},pk:{1}"; |
|||
public string AppId { get; set; } |
|||
public string AppKey { get; set; } |
|||
public bool IsMobile { get; set; } |
|||
public AbpTencentQQCacheItem() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public AbpTencentQQCacheItem( |
|||
string appId, |
|||
string appKey, |
|||
bool isMobile = false) |
|||
{ |
|||
AppId = appId; |
|||
AppKey = appKey; |
|||
IsMobile = isMobile; |
|||
} |
|||
|
|||
public static string CalculateCacheKey(Guid? tenantId = null) |
|||
{ |
|||
return string.Format(CacheKeyFormat, tenantId.HasValue ? tenantId.Value.ToString() : "global", "tenant-qq"); |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using LINGYUN.Abp.Tencent.Localization; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
|
|||
namespace LINGYUN.Abp.Tencent.QQ; |
|||
|
|||
[DependsOn(typeof(AbpTencentCloudModule))] |
|||
public class AbpTencentQQModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<AbpTencentQQModule>(); |
|||
}); |
|||
|
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources |
|||
.Get<TencentCloudResource>() |
|||
.AddVirtualJson("/LINGYUN/Abp/Tencent/QQ/Localization"); |
|||
}); |
|||
|
|||
context.Services.AddAbpDynamicOptions<AbpTencentQQOptions, AbpTencentQQOptionsManager>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
namespace LINGYUN.Abp.Tencent.QQ; |
|||
|
|||
public class AbpTencentQQOptions |
|||
{ |
|||
/// <summary>
|
|||
/// 在QQ互联上申请的AppId
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// see: https://wiki.connect.qq.com/%e5%87%86%e5%a4%87%e5%b7%a5%e4%bd%9c_oauth2-0
|
|||
/// </remarks>
|
|||
public string AppId { get; set; } |
|||
/// <summary>
|
|||
/// 在QQ互联上申请的AppKey
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// see: https://wiki.connect.qq.com/%e5%87%86%e5%a4%87%e5%b7%a5%e4%bd%9c_oauth2-0
|
|||
/// </remarks>
|
|||
public string AppKey { get; set; } |
|||
/// <summary>
|
|||
/// 是否移动端样式
|
|||
/// </summary>
|
|||
public bool IsMobile { get; set; } |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using Microsoft.Extensions.Options; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.Tencent.QQ |
|||
{ |
|||
public class AbpTencentQQOptionsFactory : ITransientDependency |
|||
{ |
|||
protected IOptions<AbpTencentQQOptions> Options { get; } |
|||
|
|||
public AbpTencentQQOptionsFactory( |
|||
IOptions<AbpTencentQQOptions> options) |
|||
{ |
|||
Options = options; |
|||
} |
|||
|
|||
public virtual async Task<AbpTencentQQOptions> CreateAsync() |
|||
{ |
|||
await Options.SetAsync(); |
|||
|
|||
return Options.Value; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
using LINGYUN.Abp.Tencent.QQ.Settings; |
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Options; |
|||
using Volo.Abp.Settings; |
|||
|
|||
namespace LINGYUN.Abp.Tencent.QQ; |
|||
|
|||
public class AbpTencentQQOptionsManager : AbpDynamicOptionsManager<AbpTencentQQOptions> |
|||
{ |
|||
protected IMemoryCache TencentCache { get; } |
|||
protected ICurrentTenant CurrentTenant { get; } |
|||
protected ISettingProvider SettingProvider { get; } |
|||
public AbpTencentQQOptionsManager( |
|||
IMemoryCache tencentCache, |
|||
ICurrentTenant currentTenant, |
|||
ISettingProvider settingProvider, |
|||
IOptionsFactory<AbpTencentQQOptions> factory) |
|||
: base(factory) |
|||
{ |
|||
TencentCache = tencentCache; |
|||
CurrentTenant = currentTenant; |
|||
SettingProvider = settingProvider; |
|||
} |
|||
|
|||
protected override async Task OverrideOptionsAsync(string name, AbpTencentQQOptions options) |
|||
{ |
|||
var cacheItem = await GetCacheItemAsync(); |
|||
|
|||
options.AppId = cacheItem.AppId; |
|||
options.AppKey = cacheItem.AppKey; |
|||
options.IsMobile = cacheItem.IsMobile; |
|||
} |
|||
|
|||
protected virtual async Task<AbpTencentQQCacheItem> GetCacheItemAsync() |
|||
{ |
|||
var cacheKey = AbpTencentQQCacheItem.CalculateCacheKey(CurrentTenant.Id); |
|||
|
|||
var cacheItem = await TencentCache.GetOrCreateAsync( |
|||
cacheKey, |
|||
async (cache) => |
|||
{ |
|||
var appId = await SettingProvider.GetOrNullAsync(TencentQQSettingNames.QQConnect.AppId); |
|||
var appKey = await SettingProvider.GetOrNullAsync(TencentQQSettingNames.QQConnect.AppKey); |
|||
var isMobile = await SettingProvider.IsTrueAsync(TencentQQSettingNames.QQConnect.IsMobile); |
|||
|
|||
cache.SetAbsoluteExpiration(TimeSpan.FromMinutes(2d)); |
|||
|
|||
return new AbpTencentQQCacheItem(appId, appKey, isMobile); |
|||
}); |
|||
|
|||
return cacheItem; |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"DisplayName:TenantCloud.QQConnect": "QQ Connect", |
|||
"Description:TenantCloud.QQConnect": "Access QQ Internet open platform", |
|||
"DisplayName:QQConnect.AppId": "AppId", |
|||
"Description:QQConnect.AppId": "The application identification necessary to access QQ login can be applied at QQ Interconnection Management Center.", |
|||
"DisplayName:QQConnect.AppKey": "AppKey", |
|||
"Description:QQConnect.AppKey": "When accessing user resources, it is used to verify the validity of the application and apply for it in QQ Interconnection Management Center.", |
|||
"DisplayName:QQConnect.IsMobile": "Whether to move the style", |
|||
"Description:QQConnect.IsMobile": "The style used for display is displayed as the style under PC by default." |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
{ |
|||
"culture": "zh-Hans", |
|||
"texts": { |
|||
"DisplayName:TenantCloud.QQConnect": "QQ互联", |
|||
"Description:TenantCloud.QQConnect": "接入QQ互联开放平台", |
|||
"DisplayName:QQConnect.AppId": "应用的唯一标识", |
|||
"Description:QQConnect.AppId": "接入QQ登录所必须的应用标识, 在QQ互联管理中心申请.", |
|||
"DisplayName:QQConnect.AppKey": "AppId对应的密钥", |
|||
"Description:QQConnect.AppKey": "访问用户资源时用来验证应用的合法性, 在QQ互联管理中心申请.", |
|||
"DisplayName:QQConnect.IsMobile": "是否移动端样式", |
|||
"Description:QQConnect.IsMobile": "用于展示的样式, 默认展示为PC下的样式." |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using LINGYUN.Abp.Tencent.Settings; |
|||
|
|||
namespace LINGYUN.Abp.Tencent.QQ.Settings |
|||
{ |
|||
public class TencentQQSettingNames |
|||
{ |
|||
public static class QQConnect |
|||
{ |
|||
private const string Prefix = TencentCloudSettingNames.Prefix + ".QQConnect"; |
|||
|
|||
public const string AppId = Prefix + ".AppId"; |
|||
public const string AppKey = Prefix + ".AppKey"; |
|||
public const string IsMobile = Prefix + ".IsMobile"; |
|||
} |
|||
} |
|||
} |
|||
@ -1,43 +0,0 @@ |
|||
using LINGYUN.Abp.Aliyun.Settings; |
|||
|
|||
namespace LINGYUN.Abp.Sms.Aliyun.Settings |
|||
{ |
|||
public static class AliyunSmsSettingNames |
|||
{ |
|||
/// <summary>
|
|||
/// 短信服务
|
|||
/// </summary>
|
|||
public class Sms |
|||
{ |
|||
public const string Prefix = AliyunSettingNames.Prefix + ".Sms"; |
|||
/// <summary>
|
|||
/// 阿里云sms服务域名
|
|||
/// </summary>
|
|||
public const string Domain = Prefix + ".Domain"; |
|||
/// <summary>
|
|||
/// 调用方法名称
|
|||
/// </summary>
|
|||
public const string ActionName = Prefix + ".ActionName"; |
|||
/// <summary>
|
|||
/// 默认版本号
|
|||
/// </summary>
|
|||
public const string Version = Prefix + ".Version"; |
|||
/// <summary>
|
|||
/// 默认签名
|
|||
/// </summary>
|
|||
public const string DefaultSignName = Prefix + ".DefaultSignName"; |
|||
/// <summary>
|
|||
/// 默认短信模板号
|
|||
/// </summary>
|
|||
public const string DefaultTemplateCode = Prefix + ".DefaultTemplateCode"; |
|||
/// <summary>
|
|||
/// 默认号码
|
|||
/// </summary>
|
|||
public const string DefaultPhoneNumber = Prefix + ".DefaultPhoneNumber"; |
|||
/// <summary>
|
|||
/// 展示错误给客户端
|
|||
/// </summary>
|
|||
public const string VisableErrorToClient = Prefix + ".VisableErrorToClient"; |
|||
} |
|||
} |
|||
} |
|||
@ -1,109 +0,0 @@ |
|||
using LINGYUN.Abp.Aliyun.Localization; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Settings; |
|||
|
|||
namespace LINGYUN.Abp.Sms.Aliyun.Settings |
|||
{ |
|||
public class AliyunSmsSettingProvider : SettingDefinitionProvider |
|||
{ |
|||
public override void Define(ISettingDefinitionContext context) |
|||
{ |
|||
context.Add(CreateAliyunSettings()); |
|||
} |
|||
|
|||
private SettingDefinition[] CreateAliyunSettings() |
|||
{ |
|||
return new SettingDefinition[] |
|||
{ |
|||
new SettingDefinition( |
|||
AliyunSmsSettingNames.Sms.ActionName, |
|||
defaultValue: "SendSms", |
|||
displayName: L("DisplayName:ActionName"), |
|||
description: L("Description:ActionName"), |
|||
isVisibleToClients: false |
|||
) |
|||
.WithProviders( |
|||
DefaultValueSettingValueProvider.ProviderName, |
|||
ConfigurationSettingValueProvider.ProviderName, |
|||
GlobalSettingValueProvider.ProviderName, |
|||
TenantSettingValueProvider.ProviderName), |
|||
new SettingDefinition( |
|||
AliyunSmsSettingNames.Sms.DefaultSignName, |
|||
displayName: L("DisplayName:DefaultSignName"), |
|||
description: L("Description:DefaultSignName"), |
|||
isVisibleToClients: false, |
|||
isEncrypted: true |
|||
) |
|||
.WithProviders( |
|||
DefaultValueSettingValueProvider.ProviderName, |
|||
ConfigurationSettingValueProvider.ProviderName, |
|||
GlobalSettingValueProvider.ProviderName, |
|||
TenantSettingValueProvider.ProviderName), |
|||
new SettingDefinition( |
|||
AliyunSmsSettingNames.Sms.DefaultTemplateCode, |
|||
displayName: L("DisplayName:DefaultTemplateCode"), |
|||
description: L("Description:DefaultTemplateCode"), |
|||
isVisibleToClients: false, |
|||
isEncrypted: true |
|||
) |
|||
.WithProviders( |
|||
DefaultValueSettingValueProvider.ProviderName, |
|||
ConfigurationSettingValueProvider.ProviderName, |
|||
GlobalSettingValueProvider.ProviderName, |
|||
TenantSettingValueProvider.ProviderName), |
|||
new SettingDefinition( |
|||
AliyunSmsSettingNames.Sms.DefaultPhoneNumber, |
|||
displayName: L("DisplayName:DefaultPhoneNumber"), |
|||
description: L("Description:DefaultPhoneNumber"), |
|||
isVisibleToClients: false |
|||
) |
|||
.WithProviders( |
|||
DefaultValueSettingValueProvider.ProviderName, |
|||
ConfigurationSettingValueProvider.ProviderName, |
|||
GlobalSettingValueProvider.ProviderName, |
|||
TenantSettingValueProvider.ProviderName), |
|||
new SettingDefinition( |
|||
AliyunSmsSettingNames.Sms.Domain, |
|||
defaultValue: "dysmsapi.aliyuncs.com", |
|||
displayName: L("DisplayName:Domain"), |
|||
description: L("Description:Domain"), |
|||
isVisibleToClients: false |
|||
) |
|||
.WithProviders( |
|||
DefaultValueSettingValueProvider.ProviderName, |
|||
ConfigurationSettingValueProvider.ProviderName, |
|||
GlobalSettingValueProvider.ProviderName, |
|||
TenantSettingValueProvider.ProviderName), |
|||
new SettingDefinition( |
|||
AliyunSmsSettingNames.Sms.Version, |
|||
defaultValue: "2017-05-25", |
|||
displayName: L("DisplayName:Version"), |
|||
description: L("Description:Version"), |
|||
isVisibleToClients: false |
|||
) |
|||
.WithProviders( |
|||
DefaultValueSettingValueProvider.ProviderName, |
|||
ConfigurationSettingValueProvider.ProviderName, |
|||
GlobalSettingValueProvider.ProviderName, |
|||
TenantSettingValueProvider.ProviderName), |
|||
new SettingDefinition( |
|||
AliyunSmsSettingNames.Sms.VisableErrorToClient, |
|||
defaultValue: false.ToString(), |
|||
displayName: L("DisplayName:VisableErrorToClient"), |
|||
description: L("Description:VisableErrorToClient"), |
|||
isVisibleToClients: false |
|||
) |
|||
.WithProviders( |
|||
DefaultValueSettingValueProvider.ProviderName, |
|||
ConfigurationSettingValueProvider.ProviderName, |
|||
GlobalSettingValueProvider.ProviderName, |
|||
TenantSettingValueProvider.ProviderName) |
|||
}; |
|||
} |
|||
|
|||
private ILocalizableString L(string name) |
|||
{ |
|||
return LocalizableString.Create<AliyunResource>(name); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,19 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\common.props" /> |
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.IdentityServer.Domain" Version="$(VoloAbpPackageVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\cloud-tencent\LINGYUN.Abp.Tencent.QQ\LINGYUN.Abp.Tencent.QQ.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,8 @@ |
|||
namespace LINGYUN.Abp.IdentityServer.QQ; |
|||
|
|||
public static class AbpIdentityServerQQConsts |
|||
{ |
|||
public static string AuthenticationScheme { get; set; } = "QQ Connect"; |
|||
public static string DisplayName { get; set; } = "QQ Connect"; |
|||
public static string CallbackPath { get; set; } = "/signin-qq"; |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using LINGYUN.Abp.Tencent.QQ; |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.IdentityServer; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.IdentityServer.QQ; |
|||
|
|||
[DependsOn(typeof(AbpTencentQQModule))] |
|||
[DependsOn(typeof(AbpIdentityServerDomainModule))] |
|||
public class AbpIdentityServerQQModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services |
|||
.AddAuthentication() |
|||
.AddQQConnect(); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
namespace LINGYUN.Abp.WeChat.Security.Claims |
|||
{ |
|||
/// <summary>
|
|||
/// QQ互联身份类型,可以像 <see cref="Volo.Abp.Security.Claims.AbpClaimTypes"/> 自行配置
|
|||
/// <br />
|
|||
/// See: <see cref="https://wiki.connect.qq.com/get_user_info"/>
|
|||
/// </summary>
|
|||
public class AbpQQClaimTypes |
|||
{ |
|||
/// <summary>
|
|||
/// 用户的唯一标识
|
|||
/// </summary>
|
|||
public static string OpenId { get; set; } = "qq-openid"; // 可变更
|
|||
/// <summary>
|
|||
/// 用户昵称
|
|||
/// </summary>
|
|||
public static string NickName { get; set; } = "nickname"; |
|||
/// <summary>
|
|||
/// 性别。 如果获取不到则默认返回"男"
|
|||
/// </summary>
|
|||
public static string Gender { get; set; } = "gender"; |
|||
/// <summary>
|
|||
/// 用户头像, 取自字段: figureurl_qq_1
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 根据QQ互联文档, 40x40的头像是一定会存在的, 只取40x40的头像
|
|||
/// see: https://wiki.connect.qq.com/get_user_info
|
|||
/// </remarks>
|
|||
public static string AvatarUrl { get; set; } = "avatar"; |
|||
} |
|||
} |
|||
@ -0,0 +1,175 @@ |
|||
using LINGYUN.Abp.Tencent.QQ; |
|||
using LINGYUN.Abp.WeChat.Security.Claims; |
|||
using Microsoft.AspNetCore.Authentication.OAuth; |
|||
using Microsoft.AspNetCore.WebUtilities; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Net.Http; |
|||
using System.Security.Claims; |
|||
using System.Text.Encodings.Web; |
|||
using System.Text.Json; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Microsoft.AspNetCore.Authentication.QQ |
|||
{ |
|||
/// <summary>
|
|||
/// QQ互联实现
|
|||
/// </summary>
|
|||
public class QQConnectOAuthHandler : OAuthHandler<QQConnectOAuthOptions> |
|||
{ |
|||
protected AbpTencentQQOptionsFactory TencentQQOptionsFactory { get; } |
|||
public QQConnectOAuthHandler( |
|||
IOptionsMonitor<QQConnectOAuthOptions> options, |
|||
AbpTencentQQOptionsFactory tencentQQOptionsFactory, |
|||
ILoggerFactory logger, |
|||
UrlEncoder encoder, |
|||
ISystemClock clock) |
|||
: base(options, logger, encoder, clock) |
|||
{ |
|||
TencentQQOptionsFactory = tencentQQOptionsFactory; |
|||
} |
|||
|
|||
protected override async Task InitializeHandlerAsync() |
|||
{ |
|||
var options = await TencentQQOptionsFactory.CreateAsync(); |
|||
|
|||
// 用配置项重写
|
|||
Options.ClientId = options.AppId; |
|||
Options.ClientSecret = options.AppKey; |
|||
Options.IsMobile = options.IsMobile; |
|||
|
|||
await base.InitializeHandlerAsync(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 构建用户授权地址
|
|||
/// </summary>
|
|||
protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri) |
|||
{ |
|||
var challengeUrl = base.BuildChallengeUrl(properties, redirectUri); |
|||
if (Options.IsMobile) |
|||
{ |
|||
challengeUrl += "&display=mobile"; |
|||
} |
|||
return challengeUrl; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// code换取access_token
|
|||
/// </summary>
|
|||
protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(OAuthCodeExchangeContext context) |
|||
{ |
|||
var address = QueryHelpers.AddQueryString(Options.TokenEndpoint, new Dictionary<string, string>() |
|||
{ |
|||
{ "client_id", Options.ClientId }, |
|||
{ "redirect_uri", context.RedirectUri }, |
|||
{ "client_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<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) |
|||
{ |
|||
var openIdEndpoint = Options.OpenIdEndpoint + "?access_token=" + tokens.AccessToken + "&fmt=json"; |
|||
var openIdResponse = await Backchannel.GetAsync(openIdEndpoint, Context.RequestAborted); |
|||
openIdResponse.EnsureSuccessStatusCode(); |
|||
|
|||
var openIdPayload = JsonDocument.Parse(await openIdResponse.Content.ReadAsStringAsync()); |
|||
var openId = openIdPayload.GetRootString("openid"); |
|||
|
|||
identity.AddClaim(new Claim(AbpQQClaimTypes.OpenId, openId, ClaimValueTypes.String, Options.ClaimsIssuer)); |
|||
|
|||
var address = QueryHelpers.AddQueryString(Options.UserInformationEndpoint, new Dictionary<string, string> |
|||
{ |
|||
{"oauth_consumer_key", Options.ClientId}, |
|||
{"access_token", tokens.AccessToken}, |
|||
{"openid", 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 userInfoPayload = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); |
|||
var errorCode = userInfoPayload.GetRootString("ret"); |
|||
if (!"0".Equals(errorCode)) |
|||
{ |
|||
// See: https://wiki.connect.qq.com/%e5%85%ac%e5%85%b1%e8%bf%94%e5%9b%9e%e7%a0%81%e8%af%b4%e6%98%8e
|
|||
Logger.LogError("An error occurred while retrieving the user profile: the remote server " + |
|||
"returned code {Code} response with message: {Message}.", |
|||
errorCode, |
|||
userInfoPayload.GetRootString("msg")); |
|||
|
|||
throw new HttpRequestException("An error occurred while retrieving user information."); |
|||
} |
|||
|
|||
var nickName = userInfoPayload.GetRootString("nickname"); |
|||
if (!nickName.IsNullOrWhiteSpace()) |
|||
{ |
|||
identity.AddClaim(new Claim(AbpQQClaimTypes.NickName, nickName, ClaimValueTypes.String, Options.ClaimsIssuer)); |
|||
} |
|||
var gender = userInfoPayload.GetRootString("gender"); |
|||
if (!gender.IsNullOrWhiteSpace()) |
|||
{ |
|||
identity.AddClaim(new Claim(AbpQQClaimTypes.Gender, gender, ClaimValueTypes.String, Options.ClaimsIssuer)); |
|||
} |
|||
var avatarUrl = userInfoPayload.GetRootString("figureurl_qq_1"); |
|||
if (!avatarUrl.IsNullOrWhiteSpace()) |
|||
{ |
|||
identity.AddClaim(new Claim(AbpQQClaimTypes.AvatarUrl, avatarUrl, ClaimValueTypes.String, Options.ClaimsIssuer)); |
|||
} |
|||
|
|||
var context = new OAuthCreatingTicketContext( |
|||
new ClaimsPrincipal(identity), |
|||
properties, |
|||
Context, |
|||
Scheme, |
|||
Options, |
|||
Backchannel, |
|||
tokens, |
|||
userInfoPayload.RootElement); |
|||
|
|||
context.RunClaimActions(); |
|||
|
|||
await Events.CreatingTicket(context); |
|||
|
|||
return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
using LINGYUN.Abp.IdentityServer.QQ; |
|||
using LINGYUN.Abp.WeChat.Security.Claims; |
|||
using Microsoft.AspNetCore.Authentication.OAuth; |
|||
using Microsoft.AspNetCore.Http; |
|||
using System.Security.Claims; |
|||
|
|||
namespace Microsoft.AspNetCore.Authentication.QQ |
|||
{ |
|||
public class QQConnectOAuthOptions : OAuthOptions |
|||
{ |
|||
/// <summary>
|
|||
/// 是否移动端样式
|
|||
/// </summary>
|
|||
public bool IsMobile { get; set; } |
|||
/// <summary>
|
|||
/// 获取用户OpenID_OAuth2.0
|
|||
/// </summary>
|
|||
public string OpenIdEndpoint { get; set; } |
|||
|
|||
public QQConnectOAuthOptions() |
|||
{ |
|||
// 用于防止初始化错误,会在OAuthHandler.InitializeHandlerAsync中进行重写
|
|||
ClientId = "QQConnect"; |
|||
ClientSecret = "QQConnect"; |
|||
|
|||
ClaimsIssuer = "connect.qq.com"; |
|||
CallbackPath = new PathString(AbpIdentityServerQQConsts.CallbackPath); |
|||
|
|||
AuthorizationEndpoint = "https://graph.qq.com/oauth2.0/authorize"; |
|||
TokenEndpoint = "https://graph.qq.com/oauth2.0/token"; |
|||
OpenIdEndpoint = "https://graph.qq.com/oauth2.0/me"; |
|||
UserInformationEndpoint = "https://graph.qq.com/user/get_user_info"; |
|||
|
|||
Scope.Add("get_user_info"); |
|||
|
|||
// 这个原始的属性一定要写进去,框架关联判断是否绑定QQ
|
|||
ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "openid"); |
|||
ClaimActions.MapJsonKey(ClaimTypes.Name, "nickname"); |
|||
|
|||
// 把自定义的身份标识写进令牌
|
|||
ClaimActions.MapJsonKey(AbpQQClaimTypes.OpenId, "openid"); |
|||
ClaimActions.MapJsonKey(AbpQQClaimTypes.NickName, "nickname"); |
|||
ClaimActions.MapJsonKey(AbpQQClaimTypes.Gender, "gender"); |
|||
ClaimActions.MapJsonKey(AbpQQClaimTypes.AvatarUrl, "figureurl_qq_1"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
using LINGYUN.Abp.IdentityServer.QQ; |
|||
using Microsoft.AspNetCore.Authentication.QQ; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System; |
|||
|
|||
namespace Microsoft.AspNetCore.Authentication |
|||
{ |
|||
public static class QQAuthenticationExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// </summary>
|
|||
public static AuthenticationBuilder AddQQConnect( |
|||
this AuthenticationBuilder builder) |
|||
{ |
|||
return builder |
|||
.AddQQConnect( |
|||
AbpIdentityServerQQConsts.AuthenticationScheme, |
|||
AbpIdentityServerQQConsts.DisplayName, |
|||
options => { }); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// </summary>
|
|||
public static AuthenticationBuilder AddQQConnect( |
|||
this AuthenticationBuilder builder, |
|||
Action<QQOAuthOptions> configureOptions) |
|||
{ |
|||
return builder |
|||
.AddQQConnect( |
|||
AbpIdentityServerQQConsts.AuthenticationScheme, |
|||
configureOptions); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// </summary>
|
|||
public static AuthenticationBuilder AddQQConnect( |
|||
this AuthenticationBuilder builder, |
|||
string authenticationScheme, |
|||
Action<QQOAuthOptions> configureOptions) |
|||
{ |
|||
return builder |
|||
.AddQQConnect( |
|||
authenticationScheme, |
|||
AbpIdentityServerQQConsts.DisplayName, |
|||
configureOptions); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// </summary>
|
|||
public static AuthenticationBuilder AddQQConnect( |
|||
this AuthenticationBuilder builder, |
|||
string authenticationScheme, |
|||
string displayName, |
|||
Action<QQOAuthOptions> configureOptions) |
|||
{ |
|||
return builder |
|||
.AddOAuth<QQOAuthOptions, QQOAuthHandler>( |
|||
authenticationScheme, |
|||
displayName, |
|||
configureOptions); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue