69 changed files with 2489 additions and 3096 deletions
@ -1,28 +0,0 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netcoreapp3.1</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<None Remove="LINGYUN\Abp\IdentityServer\Localization\WeChatValidator\en.json" /> |
|||
<None Remove="LINGYUN\Abp\IdentityServer\Localization\WeChatValidator\zh-Hans.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<EmbeddedResource Include="LINGYUN\Abp\IdentityServer\Localization\WeChatValidator\en.json" /> |
|||
<EmbeddedResource Include="LINGYUN\Abp\IdentityServer\Localization\WeChatValidator\zh-Hans.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.IdentityServer.Domain" Version="4.4.0" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Folder Include="Microsoft\DependencyInjection\" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -1,57 +0,0 @@ |
|||
using LINGYUN.Abp.IdentityServer.AspNetIdentity; |
|||
using LINGYUN.Abp.IdentityServer.WeChatValidator; |
|||
using LINGYUN.Abp.WeChat.Authorization; |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using Microsoft.Extensions.Configuration; |
|||
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 |
|||
{ |
|||
[DependsOn( |
|||
typeof(AbpWeChatAuthorizationModule), |
|||
typeof(AbpIdentityServerDomainModule))] |
|||
public class AbpIdentityServerWeChatValidatorModule : AbpModule |
|||
{ |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var configuration = context.Services.GetConfiguration(); |
|||
|
|||
PreConfigure<IIdentityServerBuilder>(builder => |
|||
{ |
|||
builder.AddProfileService<AbpWeChatProfileServicee>(); |
|||
builder.AddExtensionGrantValidator<WeChatTokenGrantValidator>(); |
|||
}); |
|||
} |
|||
|
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var configuration = context.Services.GetConfiguration(); |
|||
|
|||
Configure<WeChatSignatureOptions>(configuration.GetSection("WeChat:Signature")); |
|||
|
|||
context.Services |
|||
.AddAuthentication() |
|||
.AddWeChat(options => // 加入微信认证登录
|
|||
{ |
|||
configuration.GetSection("WeChat:Auth")?.Bind(options); |
|||
}); |
|||
|
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<AbpIdentityServerWeChatValidatorModule>(); |
|||
}); |
|||
|
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources |
|||
.Get<AbpIdentityServerResource>() |
|||
.AddVirtualJson("/LINGYUN/Abp/IdentityServer/Localization/WeChatValidator"); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -1,62 +0,0 @@ |
|||
using IdentityServer4.AspNetIdentity; |
|||
using IdentityServer4.Models; |
|||
using LINGYUN.Abp.WeChat.Authorization; |
|||
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.Security.Claims; |
|||
using Volo.Abp.Uow; |
|||
using IdentityUser = Volo.Abp.Identity.IdentityUser; |
|||
|
|||
namespace LINGYUN.Abp.IdentityServer.AspNetIdentity |
|||
{ |
|||
public class AbpWeChatProfileServicee : ProfileService<IdentityUser> |
|||
{ |
|||
protected ICurrentTenant CurrentTenant { get; } |
|||
public AbpWeChatProfileServicee( |
|||
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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.IdentityServer |
|||
{ |
|||
public interface IWeChatResourceDataSeeder |
|||
{ |
|||
Task CreateStandardResourcesAsync(); |
|||
} |
|||
} |
|||
@ -1,10 +0,0 @@ |
|||
{ |
|||
"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!" |
|||
} |
|||
} |
|||
@ -1,10 +0,0 @@ |
|||
{ |
|||
"culture": "zh-Hans", |
|||
"texts": { |
|||
"SelfRegistrationDisabledMessage": "应用程序未开放注册,请联系管理员添加新用户.", |
|||
"InvalidGrant:GrantTypeInvalid": "不被允许的授权类型!", |
|||
"InvalidGrant:WeChatTokenInvalid": "微信认证失败!", |
|||
"InvalidGrant:WeChatCodeNotFound": "微信登录时获取的 code 为空或不存在!", |
|||
"InvalidGrant:WeChatNotRegister": "用户微信账号未绑定!" |
|||
} |
|||
} |
|||
@ -1,85 +0,0 @@ |
|||
using LINGYUN.Abp.WeChat.Authorization; |
|||
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( |
|||
AbpWeChatAuthorizationConsts.ProfileKey, |
|||
AbpWeChatAuthorizationConsts.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 |
|||
) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -1,74 +0,0 @@ |
|||
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 |
|||
{ |
|||
public class WeChatSignatureMiddleware : IMiddleware, ITransientDependency |
|||
{ |
|||
protected WeChatSignatureOptions Options { get; } |
|||
public WeChatSignatureMiddleware(IOptions<WeChatSignatureOptions> 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.RequestPath)) |
|||
{ |
|||
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; |
|||
} |
|||
} |
|||
} |
|||
@ -1,16 +0,0 @@ |
|||
namespace LINGYUN.Abp.IdentityServer |
|||
{ |
|||
public class WeChatSignatureOptions |
|||
{ |
|||
/// <summary>
|
|||
/// 微信服务器请求路径
|
|||
/// 填写在微信开发者中心配置的地址
|
|||
/// </summary>
|
|||
public string RequestPath { get; set; } |
|||
/// <summary>
|
|||
/// 微信服务器请求token
|
|||
/// 填写在微信开发者中心配置的token
|
|||
/// </summary>
|
|||
public string Token { get; set; } |
|||
} |
|||
} |
|||
@ -1,111 +0,0 @@ |
|||
using IdentityModel; |
|||
using IdentityServer4.Events; |
|||
using IdentityServer4.Models; |
|||
using IdentityServer4.Services; |
|||
using IdentityServer4.Validation; |
|||
using LINGYUN.Abp.WeChat.Authorization; |
|||
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.WeChatValidator |
|||
{ |
|||
public class WeChatTokenGrantValidator : IExtensionGrantValidator |
|||
{ |
|||
protected ILogger<WeChatTokenGrantValidator> Logger { get; } |
|||
protected AbpWeChatAuthorizationOptions 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 WeChatTokenGrantValidator( |
|||
IEventService eventService, |
|||
IWeChatOpenIdFinder weChatOpenIdFinder, |
|||
IHttpClientFactory httpClientFactory, |
|||
UserManager<IdentityUser> userManager, |
|||
IIdentityUserRepository userRepository, |
|||
SignInManager<IdentityUser> signInManager, |
|||
IStringLocalizer<AbpIdentityServerResource> stringLocalizer, |
|||
PhoneNumberTokenProvider<IdentityUser> phoneNumberTokenProvider, |
|||
IOptions<AbpWeChatAuthorizationOptions> options, |
|||
ILogger<WeChatTokenGrantValidator> 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 => WeChatValidatorConsts.WeChatValidatorGrantTypeName; |
|||
|
|||
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(WeChatValidatorConsts.WeChatValidatorTokenName); |
|||
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); |
|||
var currentUser = await UserManager.FindByLoginAsync(AbpWeChatAuthorizationConsts.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, |
|||
WeChatValidatorConsts.AuthenticationMethods.BasedWeChatAuthentication, additionalClaims.ToArray()); |
|||
} |
|||
} |
|||
} |
|||
@ -1,16 +0,0 @@ |
|||
namespace LINGYUN.Abp.IdentityServer.WeChatValidator |
|||
{ |
|||
public class WeChatValidatorConsts |
|||
{ |
|||
public const string WeChatValidatorClientName = "WeChatValidator"; |
|||
|
|||
public const string WeChatValidatorGrantTypeName = "wechat"; |
|||
|
|||
public const string WeChatValidatorTokenName = "code"; |
|||
|
|||
public class AuthenticationMethods |
|||
{ |
|||
public const string BasedWeChatAuthentication = "wca"; |
|||
} |
|||
} |
|||
} |
|||
@ -1,316 +0,0 @@ |
|||
using LINGYUN.Abp.WeChat.Authorization; |
|||
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 |
|||
{ |
|||
public class WeChatAuthenticationHandler : OAuthHandler<WeChatAuthenticationOptions> |
|||
{ |
|||
protected IDistributedCache<WeChatAuthenticationStateCacheItem> Cache { get; } |
|||
public WeChatAuthenticationHandler( |
|||
IDistributedCache<WeChatAuthenticationStateCacheItem> cache, |
|||
IOptionsMonitor<WeChatAuthenticationOptions> options, |
|||
ILoggerFactory logger, |
|||
UrlEncoder encoder, |
|||
ISystemClock clock) |
|||
: base(options, logger, encoder, clock) |
|||
{ |
|||
Cache = cache; |
|||
} |
|||
|
|||
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 = WeChatAuthenticationStateCacheItem.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 = WeChatAuthenticationStateCacheItem.CalculateCacheKey(state, null); |
|||
|
|||
await Cache |
|||
.SetAsync( |
|||
stateCacheKey, |
|||
new WeChatAuthenticationStateCacheItem(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 |
|||
? AbpWeChatAuthorizationConsts.UserInfoScope |
|||
: FormatScope(); |
|||
|
|||
var endPoint = isWeChatBrewserRequest |
|||
? Options.AuthorizationEndpoint |
|||
: AbpWeChatAuthorizationConsts.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 = WeChatAuthenticationStateCacheItem.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); |
|||
} |
|||
} |
|||
} |
|||
@ -1,54 +0,0 @@ |
|||
using LINGYUN.Abp.WeChat.Authorization; |
|||
using Microsoft.AspNetCore.Authentication.OAuth; |
|||
using Microsoft.AspNetCore.Http; |
|||
using System.Security.Claims; |
|||
using System.Text.Json; |
|||
|
|||
namespace Microsoft.AspNetCore.Authentication.WeChat |
|||
{ |
|||
public class WeChatAuthenticationOptions : OAuthOptions |
|||
{ |
|||
public string AppId |
|||
{ |
|||
get => ClientId; |
|||
set => ClientId = value; |
|||
} |
|||
|
|||
public string AppSecret |
|||
{ |
|||
get => ClientSecret; |
|||
set => ClientSecret = value; |
|||
} |
|||
|
|||
public WeChatAuthenticationOptions() |
|||
{ |
|||
ClaimsIssuer = AbpWeChatAuthorizationConsts.ProviderKey; |
|||
CallbackPath = new PathString(AbpWeChatAuthorizationConsts.CallbackPath); |
|||
|
|||
AuthorizationEndpoint = AbpWeChatAuthorizationConsts.AuthorizationEndpoint; |
|||
TokenEndpoint = AbpWeChatAuthorizationConsts.TokenEndpoint; |
|||
UserInformationEndpoint = AbpWeChatAuthorizationConsts.UserInformationEndpoint; |
|||
|
|||
Scope.Add(AbpWeChatAuthorizationConsts.LoginScope); |
|||
Scope.Add(AbpWeChatAuthorizationConsts.UserInfoScope); |
|||
|
|||
// 这个原始的属性一定要写进去,框架与UserLogin.ProviderKey进行关联判断是否绑定微信
|
|||
ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "openid"); |
|||
ClaimActions.MapJsonKey(ClaimTypes.Name, "nickname"); |
|||
|
|||
// 把自定义的身份标识写进令牌
|
|||
ClaimActions.MapJsonKey(AbpWeChatClaimTypes.OpenId, "openid"); |
|||
ClaimActions.MapJsonKey(AbpWeChatClaimTypes.UnionId, "unionid"); // TODO: 可用作tenant对比?
|
|||
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")); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -1,18 +0,0 @@ |
|||
namespace Microsoft.AspNetCore.Authentication.WeChat |
|||
{ |
|||
public class WeChatAuthenticationStateCacheItem |
|||
{ |
|||
public string State { get; set; } |
|||
|
|||
public WeChatAuthenticationStateCacheItem() { } |
|||
public WeChatAuthenticationStateCacheItem(string state) |
|||
{ |
|||
State = state; |
|||
} |
|||
|
|||
public static string CalculateCacheKey(string correlationId, string purpose) |
|||
{ |
|||
return $"ci:{correlationId};p:{purpose ?? "null"}"; |
|||
} |
|||
} |
|||
} |
|||
@ -1,64 +0,0 @@ |
|||
using LINGYUN.Abp.WeChat.Authorization; |
|||
using Microsoft.AspNetCore.Authentication.WeChat; |
|||
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( |
|||
AbpWeChatAuthorizationConsts.AuthenticationScheme, |
|||
AbpWeChatAuthorizationConsts.DisplayName, |
|||
options => { }); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// </summary>
|
|||
public static AuthenticationBuilder AddWeChat( |
|||
this AuthenticationBuilder builder, |
|||
Action<WeChatAuthenticationOptions> configureOptions) |
|||
{ |
|||
return builder |
|||
.AddWeChat( |
|||
AbpWeChatAuthorizationConsts.AuthenticationScheme, |
|||
AbpWeChatAuthorizationConsts.DisplayName, |
|||
configureOptions); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// </summary>
|
|||
public static AuthenticationBuilder AddWeChat( |
|||
this AuthenticationBuilder builder, |
|||
string authenticationScheme, |
|||
Action<WeChatAuthenticationOptions> configureOptions) |
|||
{ |
|||
return builder |
|||
.AddWeChat( |
|||
authenticationScheme, |
|||
AbpWeChatAuthorizationConsts.DisplayName, |
|||
configureOptions); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// </summary>
|
|||
public static AuthenticationBuilder AddWeChat( |
|||
this AuthenticationBuilder builder, |
|||
string authenticationScheme, |
|||
string displayName, |
|||
Action<WeChatAuthenticationOptions> configureOptions) |
|||
{ |
|||
return builder |
|||
.AddOAuth<WeChatAuthenticationOptions, WeChatAuthenticationHandler>( |
|||
authenticationScheme, |
|||
displayName, |
|||
configureOptions); |
|||
} |
|||
} |
|||
} |
|||
@ -1,22 +0,0 @@ |
|||
using LINGYUN.Abp.IdentityServer; |
|||
|
|||
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<WeChatSignatureMiddleware>(); |
|||
return builder; |
|||
} |
|||
} |
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
# LINGYUN.Abp.IdentityServer.WeChatValidator |
|||
|
|||
废弃模块,模块层次不清晰,微信有多端平台,不同平台授权规则不一致 |
|||
|
|||
#### 注意 |
|||
|
|||
|
|||
|
|||
## 配置使用 |
|||
|
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpIdentityServerWeChatValidatorModule))] |
|||
public class YouProjectModule : AbpModule |
|||
{ |
|||
// other |
|||
} |
|||
@ -1,16 +0,0 @@ |
|||
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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,63 +0,0 @@ |
|||
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,19 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Identity.Domain" Version="4.4.0" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.WeChat\LINGYUN.Abp.WeChat.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
|
|||
</Project> |
|||
@ -0,0 +1,13 @@ |
|||
using LINGYUN.Abp.WeChat; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.Identity.WeChat |
|||
{ |
|||
[DependsOn( |
|||
typeof(AbpWeChatModule), |
|||
typeof(AbpIdentityDomainModule))] |
|||
public class AbpIdentityWeChatModule : AbpModule |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Ddd.Application.Contracts" Version="4.4.0" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,11 @@ |
|||
using Volo.Abp.Application; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.WeChat |
|||
{ |
|||
[DependsOn( |
|||
typeof(AbpDddApplicationContractsModule))] |
|||
public class AbpWeChatApplicationContractsModule : AbpModule |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
namespace LINGYUN.Abp.WeChat.Crypto |
|||
{ |
|||
/// <summary>
|
|||
/// 性别
|
|||
/// </summary>
|
|||
public enum Gender |
|||
{ |
|||
/// <summary>
|
|||
/// 未知
|
|||
/// </summary>
|
|||
None = 0, |
|||
/// <summary>
|
|||
/// 男性
|
|||
/// </summary>
|
|||
Man = 1, |
|||
/// <summary>
|
|||
/// 女性
|
|||
/// </summary>
|
|||
Women = 2 |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
namespace LINGYUN.Abp.WeChat.Crypto |
|||
{ |
|||
public class GetUserInfoInput |
|||
{ |
|||
public string EncryptedData { get; set; } |
|||
public string IV { get; set; } |
|||
public string Code { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
namespace LINGYUN.Abp.WeChat.Crypto |
|||
{ |
|||
public class UserInfoDto |
|||
{ |
|||
/// <summary>
|
|||
/// 用户昵称
|
|||
/// </summary>
|
|||
public string NickName { get; set; } |
|||
/// <summary>
|
|||
/// 用户头像图片的 URL。
|
|||
/// URL 最后一个数值代表正方形头像大小(有 0、46、64、96、132 数值可选,0 代表 640x640 的正方形头像,46 表示 46x46 的正方形头像,剩余数值以此类推。默认132),
|
|||
/// 用户没有头像时该项为空。
|
|||
/// 若用户更换头像,原有头像 URL 将失效。
|
|||
/// </summary>
|
|||
public string AvatarUrl { get; set; } |
|||
/// <summary>
|
|||
/// 性别
|
|||
/// </summary>
|
|||
public Gender Gender { get; set; } |
|||
/// <summary>
|
|||
/// 用户所在国家
|
|||
/// </summary>
|
|||
public string Country { get; set; } |
|||
/// <summary>
|
|||
/// 用户所在省份
|
|||
/// </summary>
|
|||
public string Province { get; set; } |
|||
/// <summary>
|
|||
/// 用户所在城市
|
|||
/// </summary>
|
|||
public string City { get; set; } |
|||
/// <summary>
|
|||
/// 显示 country,province,city 所用的语言
|
|||
/// </summary>
|
|||
public string Language { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Crypto |
|||
{ |
|||
public interface ICryptoAppService : IApplicationService |
|||
{ |
|||
Task<UserInfoDto> GetUserInfoAsync(GetUserInfoInput input); |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace LINGYUN.Abp.WeChat |
|||
{ |
|||
public static class WeChatRemoteServiceConsts |
|||
{ |
|||
public const string RemoteServiceName = "AbpWeChat"; |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Ddd.Application" Version="4.4.0" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.WeChat.Application.Contracts\LINGYUN.Abp.WeChat.Application.Contracts.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.WeChat.MiniProgram\LINGYUN.Abp.WeChat.MiniProgram.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,14 @@ |
|||
using LINGYUN.Abp.WeChat.MiniProgram; |
|||
using Volo.Abp.Application; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.WeChat |
|||
{ |
|||
[DependsOn( |
|||
typeof(AbpWeChatMiniProgramModule), |
|||
typeof(AbpWeChatApplicationContractsModule), |
|||
typeof(AbpDddApplicationModule))] |
|||
public class AbpWeChatApplicationModule : AbpModule |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
using LINGYUN.Abp.WeChat.MiniProgram; |
|||
using LINGYUN.Abp.WeChat.OpenId; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Json; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Crypto |
|||
{ |
|||
[Authorize] |
|||
public class CryptoAppService : WeChatApplicationServiceBase, ICryptoAppService |
|||
{ |
|||
protected IJsonSerializer JsonSerializer { get; } |
|||
protected IWeChatOpenIdFinder OpenIdFinder { get; } |
|||
protected IWeChatCryptoService WeChatCryptoService { get; } |
|||
protected AbpWeChatMiniProgramOptionsFactory MiniProgramOptionsFactory { get; } |
|||
|
|||
public CryptoAppService( |
|||
IJsonSerializer jsonSerializer, |
|||
IWeChatOpenIdFinder openIdFinder, |
|||
IWeChatCryptoService weChatCryptoService, |
|||
AbpWeChatMiniProgramOptionsFactory miniProgramOptionsFactory) |
|||
{ |
|||
JsonSerializer = jsonSerializer; |
|||
OpenIdFinder = openIdFinder; |
|||
WeChatCryptoService = weChatCryptoService; |
|||
MiniProgramOptionsFactory = miniProgramOptionsFactory; |
|||
} |
|||
|
|||
public virtual async Task<UserInfoDto> GetUserInfoAsync(GetUserInfoInput input) |
|||
{ |
|||
var options = await MiniProgramOptionsFactory.CreateAsync(); |
|||
WeChatOpenId weChatOpenId = input.Code.IsNullOrWhiteSpace() |
|||
? await OpenIdFinder.FindAsync(options.AppId) |
|||
: await OpenIdFinder.FindAsync(input.Code, options.AppId, options.AppSecret); |
|||
|
|||
var decryptedData = WeChatCryptoService.Decrypt(input.EncryptedData, input.IV, weChatOpenId.SessionKey); |
|||
|
|||
return JsonSerializer.Deserialize<UserInfoDto>(decryptedData); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using LINGYUN.Abp.WeChat.Localization; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace LINGYUN.Abp.WeChat |
|||
{ |
|||
public abstract class WeChatApplicationServiceBase : ApplicationService |
|||
{ |
|||
protected WeChatApplicationServiceBase() |
|||
{ |
|||
LocalizationResource = typeof(WeChatResource); |
|||
ObjectMapperContext = typeof(AbpWeChatApplicationModule); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net5.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.AspNetCore.Mvc" Version="4.4.0" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,12 @@ |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.WeChat |
|||
{ |
|||
[DependsOn( |
|||
typeof(AbpWeChatApplicationContractsModule), |
|||
typeof(AbpAspNetCoreMvcModule))] |
|||
public class AbpWeChatHttpApiModule : AbpModule |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Crypto |
|||
{ |
|||
[RemoteService(Name = WeChatRemoteServiceConsts.RemoteServiceName)] |
|||
[Area("account")] |
|||
[ApiVersion("1.0")] |
|||
[Route("api/wechat")] |
|||
public class CryptoController : AbpController, ICryptoAppService |
|||
{ |
|||
private readonly ICryptoAppService _service; |
|||
|
|||
public CryptoController( |
|||
ICryptoAppService service) |
|||
{ |
|||
_service = service; |
|||
} |
|||
|
|||
[HttpGet] |
|||
[Route("getUserInfo")] |
|||
public virtual async Task<UserInfoDto> GetUserInfoAsync(GetUserInfoInput input) |
|||
{ |
|||
return await _service.GetUserInfoAsync(input); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace LINGYUN.Abp.WeChat.Crypto |
|||
{ |
|||
public interface IWeChatCryptoService |
|||
{ |
|||
string Decrypt(string encryptedData, string iv, string sessionKey); |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
using System; |
|||
using System.Security.Cryptography; |
|||
using System.Text; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Crypto |
|||
{ |
|||
public class WeChatCryptoService : IWeChatCryptoService, ITransientDependency |
|||
{ |
|||
public virtual string Decrypt(string encryptedData, string iv, string sessionKey) |
|||
{ |
|||
using var aes = new AesCryptoServiceProvider(); |
|||
aes.Mode = CipherMode.CBC; |
|||
aes.BlockSize = 128; |
|||
// 对称解密使用的算法为 AES-128-CBC,数据采用PKCS#7填充。
|
|||
aes.Padding = PaddingMode.PKCS7; |
|||
|
|||
//格式化待处理字符串
|
|||
// 对称解密的目标密文为 Base64_Decode(encryptedData)。
|
|||
byte[] byte_encryptedData = Convert.FromBase64String(encryptedData); |
|||
// 对称解密算法初始向量 为Base64_Decode(iv),其中iv由数据接口返回。
|
|||
byte[] byte_iv = Convert.FromBase64String(iv); |
|||
// 对称解密秘钥 aeskey = Base64_Decode(session_key), aeskey 是16字节。
|
|||
byte[] byte_sessionKey = Convert.FromBase64String(sessionKey); |
|||
|
|||
//根据设置好的数据生成解密器实例
|
|||
using var transform = aes.CreateDecryptor(byte_iv, byte_sessionKey); |
|||
//解密
|
|||
byte[] final = transform.TransformFinalBlock(byte_encryptedData, 0, byte_encryptedData.Length); |
|||
|
|||
//生成结果
|
|||
string result = Encoding.UTF8.GetString(final); |
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using LINGYUN.Abp.WeChat.Localization; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Settings; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Settings |
|||
{ |
|||
public class WeChatSettingDefinitionProvider : SettingDefinitionProvider |
|||
{ |
|||
public override void Define(ISettingDefinitionContext context) |
|||
{ |
|||
context.Add( |
|||
new SettingDefinition( |
|||
WeChatSettingNames.EnabledQuickLogin, |
|||
// 默认启用
|
|||
true.ToString(), |
|||
L("DisplayName:WeChat.EnabledQuickLogin"), |
|||
L("Description:WeChat.EnabledQuickLogin"), |
|||
isVisibleToClients: true, |
|||
isEncrypted: false) |
|||
); |
|||
} |
|||
|
|||
protected ILocalizableString L(string name) |
|||
{ |
|||
return LocalizableString.Create<WeChatResource>(name); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue