34 changed files with 0 additions and 1916 deletions
@ -1,3 +0,0 @@ |
|||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|
||||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|
||||
</Weavers> |
|
||||
@ -1,30 +0,0 @@ |
|||||
<?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> |
|
||||
@ -1,19 +0,0 @@ |
|||||
<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> |
|
||||
@ -1,8 +0,0 @@ |
|||||
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"; |
|
||||
} |
|
||||
@ -1,19 +0,0 @@ |
|||||
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(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,31 +0,0 @@ |
|||||
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"; |
|
||||
} |
|
||||
} |
|
||||
@ -1,175 +0,0 @@ |
|||||
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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,47 +0,0 @@ |
|||||
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"); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,63 +0,0 @@ |
|||||
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<QQConnectOAuthOptions> configureOptions) |
|
||||
{ |
|
||||
return builder |
|
||||
.AddQQConnect( |
|
||||
AbpIdentityServerQQConsts.AuthenticationScheme, |
|
||||
configureOptions); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// </summary>
|
|
||||
public static AuthenticationBuilder AddQQConnect( |
|
||||
this AuthenticationBuilder builder, |
|
||||
string authenticationScheme, |
|
||||
Action<QQConnectOAuthOptions> configureOptions) |
|
||||
{ |
|
||||
return builder |
|
||||
.AddQQConnect( |
|
||||
authenticationScheme, |
|
||||
AbpIdentityServerQQConsts.DisplayName, |
|
||||
configureOptions); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// </summary>
|
|
||||
public static AuthenticationBuilder AddQQConnect( |
|
||||
this AuthenticationBuilder builder, |
|
||||
string authenticationScheme, |
|
||||
string displayName, |
|
||||
Action<QQConnectOAuthOptions> configureOptions) |
|
||||
{ |
|
||||
return builder |
|
||||
.AddOAuth<QQConnectOAuthOptions, QQConnectOAuthHandler>( |
|
||||
authenticationScheme, |
|
||||
displayName, |
|
||||
configureOptions); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,3 +0,0 @@ |
|||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|
||||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|
||||
</Weavers> |
|
||||
@ -1,30 +0,0 @@ |
|||||
<?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> |
|
||||
@ -1,26 +0,0 @@ |
|||||
<Project Sdk="Microsoft.NET.Sdk"> |
|
||||
|
|
||||
<Import Project="..\..\..\common.props" /> |
|
||||
<Import Project="..\..\..\configureawait.props" /> |
|
||||
|
|
||||
<PropertyGroup> |
|
||||
<TargetFramework>net6.0</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="$(VoloAbpPackageVersion)" /> |
|
||||
</ItemGroup> |
|
||||
|
|
||||
<ItemGroup> |
|
||||
<ProjectReference Include="..\..\wechat\LINGYUN.Abp.Identity.WeChat\LINGYUN.Abp.Identity.WeChat.csproj" /> |
|
||||
<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> |
|
||||
@ -1,56 +0,0 @@ |
|||||
using LINGYUN.Abp.Identity.WeChat; |
|
||||
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(AbpIdentityWeChatModule), |
|
||||
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(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,9 +0,0 @@ |
|||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace LINGYUN.Abp.IdentityServer |
|
||||
{ |
|
||||
public interface IWeChatResourceDataSeeder |
|
||||
{ |
|
||||
Task CreateStandardResourcesAsync(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,12 +0,0 @@ |
|||||
{ |
|
||||
"culture": "en", |
|
||||
"texts": { |
|
||||
"MiniProgramAuthorizationDisabledMessage": "Applet authorization is not enabled for the application", |
|
||||
"OfficialAuthorizationDisabledMessage": "Official authorization is not enabled for the application", |
|
||||
"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,12 +0,0 @@ |
|||||
{ |
|
||||
"culture": "zh-Hans", |
|
||||
"texts": { |
|
||||
"MiniProgramAuthorizationDisabledMessage": "应用程序未开放小程序授权", |
|
||||
"OfficialAuthorizationDisabledMessage": "应用程序未开放公众平台授权", |
|
||||
"SelfRegistrationDisabledMessage": "应用程序未开放注册,请联系管理员添加新用户.", |
|
||||
"InvalidGrant:GrantTypeInvalid": "不被允许的授权类型!", |
|
||||
"InvalidGrant:WeChatTokenInvalid": "微信认证失败!", |
|
||||
"InvalidGrant:WeChatCodeNotFound": "微信登录时获取的 code 为空或不存在!", |
|
||||
"InvalidGrant:WeChatNotRegister": "用户微信账号未绑定!" |
|
||||
} |
|
||||
} |
|
||||
@ -1,75 +0,0 @@ |
|||||
using IdentityServer4.Models; |
|
||||
using IdentityServer4.Services; |
|
||||
using IdentityServer4.Validation; |
|
||||
using LINGYUN.Abp.WeChat.Localization; |
|
||||
using LINGYUN.Abp.WeChat.MiniProgram; |
|
||||
using LINGYUN.Abp.WeChat.MiniProgram.Features; |
|
||||
using LINGYUN.Abp.WeChat.OpenId; |
|
||||
using Microsoft.AspNetCore.Identity; |
|
||||
using Microsoft.Extensions.Localization; |
|
||||
using System.Threading.Tasks; |
|
||||
using Volo.Abp.Features; |
|
||||
using Volo.Abp.Identity; |
|
||||
using Volo.Abp.IdentityServer.Localization; |
|
||||
using IdentityUser = Volo.Abp.Identity.IdentityUser; |
|
||||
|
|
||||
namespace LINGYUN.Abp.IdentityServer.WeChat.MiniProgram |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// 对于小程序绑定用户的扩展授权验证器
|
|
||||
/// </summary>
|
|
||||
public class WeChatMiniProgramGrantValidator : WeChatGrantValidator |
|
||||
{ |
|
||||
public override string GrantType => AbpWeChatMiniProgramConsts.GrantType; |
|
||||
|
|
||||
public override string LoginProvider => AbpWeChatMiniProgramConsts.ProviderName; |
|
||||
|
|
||||
public override string AuthenticationMethod => AbpWeChatMiniProgramConsts.AuthenticationMethod; |
|
||||
|
|
||||
protected AbpWeChatMiniProgramOptionsFactory MiniProgramOptionsFactory { get; } |
|
||||
|
|
||||
protected IFeatureChecker FeatureChecker => ServiceProvider.LazyGetRequiredService<IFeatureChecker>(); |
|
||||
|
|
||||
public WeChatMiniProgramGrantValidator( |
|
||||
IEventService eventService, |
|
||||
IWeChatOpenIdFinder weChatOpenIdFinder, |
|
||||
UserManager<IdentityUser> userManager, |
|
||||
IIdentityUserRepository userRepository, |
|
||||
IdentitySecurityLogManager identitySecurityLogManager, |
|
||||
IStringLocalizer<Volo.Abp.Identity.Localization.IdentityResource> identityLocalizer, |
|
||||
IStringLocalizer<AbpIdentityServerResource> identityServerLocalizer, |
|
||||
IStringLocalizer<WeChatResource> wechatLocalizer, |
|
||||
AbpWeChatMiniProgramOptionsFactory miniProgramOptionsFactory) |
|
||||
: base( |
|
||||
eventService, |
|
||||
weChatOpenIdFinder, |
|
||||
userManager, |
|
||||
userRepository, |
|
||||
identitySecurityLogManager, |
|
||||
wechatLocalizer, |
|
||||
identityLocalizer, |
|
||||
identityServerLocalizer) |
|
||||
{ |
|
||||
MiniProgramOptionsFactory = miniProgramOptionsFactory; |
|
||||
} |
|
||||
|
|
||||
protected override async Task<bool> CheckFeatureAsync(ExtensionGrantValidationContext context) |
|
||||
{ |
|
||||
if (!await FeatureChecker.IsEnabledAsync(WeChatMiniProgramFeatures.EnableAuthorization)) |
|
||||
{ |
|
||||
context.Result = new GrantValidationResult( |
|
||||
TokenRequestErrors.InvalidGrant, |
|
||||
WeChatLocalizer["MiniProgramAuthorizationDisabledMessage"]); |
|
||||
return false; |
|
||||
} |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
protected override async Task<WeChatOpenId> FindOpenIdAsync(string code) |
|
||||
{ |
|
||||
var options = await MiniProgramOptionsFactory.CreateAsync(); |
|
||||
|
|
||||
return await WeChatOpenIdFinder.FindAsync(code, options.AppId, options.AppSecret); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,67 +0,0 @@ |
|||||
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); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[UnitOfWork] |
|
||||
public override Task<bool> IsUserActiveAsync(IdentityUser user) |
|
||||
{ |
|
||||
return Task.FromResult(user.IsActive); |
|
||||
} |
|
||||
|
|
||||
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,75 +0,0 @@ |
|||||
using IdentityServer4.Models; |
|
||||
using IdentityServer4.Services; |
|
||||
using IdentityServer4.Validation; |
|
||||
using LINGYUN.Abp.WeChat.Localization; |
|
||||
using LINGYUN.Abp.WeChat.Official; |
|
||||
using LINGYUN.Abp.WeChat.Official.Features; |
|
||||
using LINGYUN.Abp.WeChat.OpenId; |
|
||||
using Microsoft.AspNetCore.Identity; |
|
||||
using Microsoft.Extensions.Localization; |
|
||||
using System.Threading.Tasks; |
|
||||
using Volo.Abp.Features; |
|
||||
using Volo.Abp.Identity; |
|
||||
using Volo.Abp.IdentityServer.Localization; |
|
||||
using IdentityUser = Volo.Abp.Identity.IdentityUser; |
|
||||
|
|
||||
namespace LINGYUN.Abp.IdentityServer.WeChat.Official |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// 对于公众平台绑定用户的扩展授权验证器
|
|
||||
/// </summary>
|
|
||||
public class WeChatOfficialGrantValidator : WeChatGrantValidator |
|
||||
{ |
|
||||
public override string GrantType => AbpWeChatOfficialConsts.GrantType; |
|
||||
|
|
||||
public override string LoginProvider => AbpWeChatOfficialConsts.ProviderName; |
|
||||
|
|
||||
public override string AuthenticationMethod => AbpWeChatOfficialConsts.AuthenticationMethod; |
|
||||
|
|
||||
protected AbpWeChatOfficialOptionsFactory WeChatOfficialOptionsFactory { get; } |
|
||||
|
|
||||
protected IFeatureChecker FeatureChecker => ServiceProvider.LazyGetRequiredService<IFeatureChecker>(); |
|
||||
|
|
||||
public WeChatOfficialGrantValidator( |
|
||||
IEventService eventService, |
|
||||
IWeChatOpenIdFinder weChatOpenIdFinder, |
|
||||
UserManager<IdentityUser> userManager, |
|
||||
IIdentityUserRepository userRepository, |
|
||||
IdentitySecurityLogManager identitySecurityLogManager, |
|
||||
IStringLocalizer<Volo.Abp.Identity.Localization.IdentityResource> identityLocalizer, |
|
||||
IStringLocalizer<AbpIdentityServerResource> identityServerLocalizer, |
|
||||
IStringLocalizer<WeChatResource> wechatLocalizer, |
|
||||
AbpWeChatOfficialOptionsFactory weChatOfficialOptionsFactory) |
|
||||
: base( |
|
||||
eventService, |
|
||||
weChatOpenIdFinder, |
|
||||
userManager, |
|
||||
userRepository, |
|
||||
identitySecurityLogManager, |
|
||||
wechatLocalizer, |
|
||||
identityLocalizer, |
|
||||
identityServerLocalizer) |
|
||||
{ |
|
||||
WeChatOfficialOptionsFactory = weChatOfficialOptionsFactory; |
|
||||
} |
|
||||
|
|
||||
protected override async Task<bool> CheckFeatureAsync(ExtensionGrantValidationContext context) |
|
||||
{ |
|
||||
if (!await FeatureChecker.IsEnabledAsync(WeChatOfficialFeatures.EnableAuthorization)) |
|
||||
{ |
|
||||
context.Result = new GrantValidationResult( |
|
||||
TokenRequestErrors.InvalidGrant, |
|
||||
WeChatLocalizer["OfficialAuthorizationDisabledMessage"]); |
|
||||
return false; |
|
||||
} |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
protected override async Task<WeChatOpenId> FindOpenIdAsync(string code) |
|
||||
{ |
|
||||
var options = await WeChatOfficialOptionsFactory.CreateAsync(); |
|
||||
|
|
||||
return await WeChatOpenIdFinder.FindAsync(code, options.AppId, options.AppSecret); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,71 +0,0 @@ |
|||||
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.ProviderName; |
|
||||
/// <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"; |
|
||||
} |
|
||||
} |
|
||||
@ -1,77 +0,0 @@ |
|||||
using LINGYUN.Abp.WeChat.Official; |
|
||||
using Microsoft.AspNetCore.Http; |
|
||||
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 AbpWeChatOfficialOptionsFactory WeChatOfficialOptionsFactory { get; } |
|
||||
public WeChatOfficialSignatureMiddleware( |
|
||||
AbpWeChatOfficialOptionsFactory weChatOfficialOptionsFactory) |
|
||||
{ |
|
||||
WeChatOfficialOptionsFactory = weChatOfficialOptionsFactory; |
|
||||
} |
|
||||
|
|
||||
public async Task InvokeAsync(HttpContext context, RequestDelegate next) |
|
||||
{ |
|
||||
if (context.Request.Path.HasValue) |
|
||||
{ |
|
||||
var options = await WeChatOfficialOptionsFactory.CreateAsync(); |
|
||||
|
|
||||
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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,242 +0,0 @@ |
|||||
using IdentityModel; |
|
||||
using IdentityServer4.Events; |
|
||||
using IdentityServer4.Models; |
|
||||
using IdentityServer4.Services; |
|
||||
using IdentityServer4.Validation; |
|
||||
using LINGYUN.Abp.WeChat; |
|
||||
using LINGYUN.Abp.WeChat.Localization; |
|
||||
using LINGYUN.Abp.WeChat.OpenId; |
|
||||
using LINGYUN.Abp.WeChat.Security.Claims; |
|
||||
using LINGYUN.Abp.WeChat.Settings; |
|
||||
using Microsoft.AspNetCore.Identity; |
|
||||
using Microsoft.Extensions.Localization; |
|
||||
using Microsoft.Extensions.Logging; |
|
||||
using Microsoft.Extensions.Logging.Abstractions; |
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Security.Claims; |
|
||||
using System.Threading.Tasks; |
|
||||
using Volo.Abp.DependencyInjection; |
|
||||
using Volo.Abp.Guids; |
|
||||
using Volo.Abp.Identity; |
|
||||
using Volo.Abp.IdentityServer; |
|
||||
using Volo.Abp.IdentityServer.Localization; |
|
||||
using Volo.Abp.MultiTenancy; |
|
||||
using Volo.Abp.Security.Claims; |
|
||||
using Volo.Abp.Settings; |
|
||||
using Volo.Abp.Uow; |
|
||||
|
|
||||
using IdentityResource = Volo.Abp.Identity.Localization.IdentityResource; |
|
||||
using IdentityUser = Volo.Abp.Identity.IdentityUser; |
|
||||
|
|
||||
namespace LINGYUN.Abp.IdentityServer.WeChat |
|
||||
{ |
|
||||
public abstract class WeChatGrantValidator : IExtensionGrantValidator |
|
||||
{ |
|
||||
public abstract string GrantType { get; } |
|
||||
public abstract string LoginProvider { get; } |
|
||||
public abstract string AuthenticationMethod { get; } |
|
||||
|
|
||||
public ILogger Logger { protected get; set; } |
|
||||
public IAbpLazyServiceProvider ServiceProvider { protected get; set; } |
|
||||
|
|
||||
protected IEventService EventService { get; } |
|
||||
protected IWeChatOpenIdFinder WeChatOpenIdFinder { get; } |
|
||||
protected IIdentityUserRepository UserRepository { get; } |
|
||||
protected IdentitySecurityLogManager IdentitySecurityLogManager { get; } |
|
||||
protected UserManager<IdentityUser> UserManager { get; } |
|
||||
protected IStringLocalizer<WeChatResource> WeChatLocalizer { get; } |
|
||||
protected IStringLocalizer<IdentityResource> IdentityLocalizer { get; } |
|
||||
protected IStringLocalizer<AbpIdentityServerResource> IdentityServerLocalizer { get; } |
|
||||
|
|
||||
public WeChatGrantValidator( |
|
||||
IEventService eventService, |
|
||||
IWeChatOpenIdFinder weChatOpenIdFinder, |
|
||||
UserManager<IdentityUser> userManager, |
|
||||
IIdentityUserRepository userRepository, |
|
||||
IdentitySecurityLogManager identitySecurityLogManager, |
|
||||
IStringLocalizer<WeChatResource> wechatLocalizer, |
|
||||
IStringLocalizer<IdentityResource> identityLocalizer, |
|
||||
IStringLocalizer<AbpIdentityServerResource> identityServerLocalizer) |
|
||||
{ |
|
||||
EventService = eventService; |
|
||||
UserManager = userManager; |
|
||||
UserRepository = userRepository; |
|
||||
IdentitySecurityLogManager = identitySecurityLogManager; |
|
||||
WeChatOpenIdFinder = weChatOpenIdFinder; |
|
||||
WeChatLocalizer = wechatLocalizer; |
|
||||
IdentityLocalizer = identityLocalizer; |
|
||||
IdentityServerLocalizer = identityServerLocalizer; |
|
||||
|
|
||||
Logger = NullLogger<WeChatGrantValidator>.Instance; |
|
||||
} |
|
||||
|
|
||||
protected abstract Task<WeChatOpenId> FindOpenIdAsync(string code); |
|
||||
|
|
||||
[UnitOfWork] |
|
||||
public async Task ValidateAsync(ExtensionGrantValidationContext context) |
|
||||
{ |
|
||||
if (!await CheckFeatureAsync(context)) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
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, IdentityServerLocalizer["InvalidGrant:GrantTypeInvalid"]); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
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, IdentityServerLocalizer["InvalidGrant:WeChatCodeNotFound"]); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
WeChatOpenId wechatOpenId; |
|
||||
try |
|
||||
{ |
|
||||
wechatOpenId = await FindOpenIdAsync(wechatCode); |
|
||||
} |
|
||||
catch(AbpWeChatException e) |
|
||||
{ |
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, e.Message); |
|
||||
return; |
|
||||
} |
|
||||
var currentUser = await UserManager.FindByLoginAsync(LoginProvider, wechatOpenId.OpenId); |
|
||||
if (currentUser == null) |
|
||||
{ |
|
||||
// 检查是否允许自注册
|
|
||||
var settingProvider = ServiceProvider.LazyGetRequiredService<ISettingProvider>(); |
|
||||
// TODO 检查启用用户注册是否有必要引用账户模块
|
|
||||
if (!await settingProvider.IsTrueAsync("Abp.Account.IsSelfRegistrationEnabled") || |
|
||||
!await settingProvider.IsTrueAsync(WeChatSettingNames.EnabledQuickLogin)) |
|
||||
{ |
|
||||
Logger.LogWarning("Invalid grant type: wechat openid not register", wechatOpenId.OpenId); |
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, IdentityServerLocalizer["InvalidGrant:WeChatNotRegister"]); |
|
||||
return; |
|
||||
} |
|
||||
var guiGenerator = ServiceProvider.LazyGetRequiredService<IGuidGenerator>(); |
|
||||
var currentTenant = ServiceProvider.LazyGetRequiredService<ICurrentTenant>(); |
|
||||
var userName = "wxid-" + wechatOpenId.OpenId.ToMd5().ToLower(); |
|
||||
var userEmail = $"{userName}@{currentTenant.Name ?? "default"}.io"; |
|
||||
currentUser = new IdentityUser(guiGenerator.Create(), userName, userEmail, currentTenant.Id); |
|
||||
(await UserManager.CreateAsync(currentUser)).CheckErrors(); |
|
||||
(await UserManager.AddLoginAsync( |
|
||||
currentUser, |
|
||||
new UserLoginInfo( |
|
||||
LoginProvider, |
|
||||
wechatOpenId.OpenId, |
|
||||
AbpWeChatGlobalConsts.DisplayName))).CheckErrors(); |
|
||||
} |
|
||||
|
|
||||
// 检查是否已锁定
|
|
||||
if (await UserManager.IsLockedOutAsync(currentUser)) |
|
||||
{ |
|
||||
Logger.LogInformation("Authentication failed for username: {username}, reason: locked out", currentUser.UserName); |
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, IdentityLocalizer["Volo.Abp.Identity:UserLockedOut"]); |
|
||||
|
|
||||
await SaveSecurityLogAsync(context, currentUser, wechatOpenId, IdentityServerSecurityLogActionConsts.LoginLockedout); |
|
||||
|
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
await EventService.RaiseAsync(new UserLoginSuccessEvent(currentUser.UserName, wechatOpenId.OpenId, null)); |
|
||||
|
|
||||
// 登录之后需要更新安全令牌
|
|
||||
(await UserManager.UpdateSecurityStampAsync(currentUser)).CheckErrors(); |
|
||||
|
|
||||
await SetSuccessResultAsync(context, currentUser, wechatOpenId); |
|
||||
} |
|
||||
|
|
||||
protected virtual Task<bool> CheckFeatureAsync(ExtensionGrantValidationContext context) |
|
||||
{ |
|
||||
return Task.FromResult(true); |
|
||||
} |
|
||||
|
|
||||
protected async virtual Task SetSuccessResultAsync(ExtensionGrantValidationContext context, IdentityUser user, WeChatOpenId wechatOpenId) |
|
||||
{ |
|
||||
var sub = await UserManager.GetUserIdAsync(user); |
|
||||
|
|
||||
Logger.LogInformation("Credentials validated for username: {username}", user.UserName); |
|
||||
|
|
||||
var additionalClaims = new List<Claim>(); |
|
||||
|
|
||||
await AddCustomClaimsAsync(additionalClaims, user, wechatOpenId, context); |
|
||||
|
|
||||
context.Result = new GrantValidationResult( |
|
||||
sub, |
|
||||
AuthenticationMethod, |
|
||||
additionalClaims.ToArray() |
|
||||
); |
|
||||
|
|
||||
await SaveSecurityLogAsync( |
|
||||
context, |
|
||||
user, |
|
||||
wechatOpenId, |
|
||||
IdentityServerSecurityLogActionConsts.LoginSucceeded); |
|
||||
} |
|
||||
|
|
||||
protected async virtual Task SaveSecurityLogAsync( |
|
||||
ExtensionGrantValidationContext context, |
|
||||
IdentityUser user, |
|
||||
WeChatOpenId wechatOpenId, |
|
||||
string action) |
|
||||
{ |
|
||||
var logContext = new IdentitySecurityLogContext |
|
||||
{ |
|
||||
Identity = IdentityServerSecurityLogIdentityConsts.IdentityServer, |
|
||||
Action = action, |
|
||||
UserName = user.UserName, |
|
||||
ClientId = await FindClientIdAsync(context) |
|
||||
}; |
|
||||
logContext.WithProperty("GrantType", GrantType); |
|
||||
logContext.WithProperty("Provider", LoginProvider); |
|
||||
logContext.WithProperty("Method", AuthenticationMethod); |
|
||||
|
|
||||
await IdentitySecurityLogManager.SaveAsync(logContext); |
|
||||
} |
|
||||
|
|
||||
protected virtual Task<string> FindClientIdAsync(ExtensionGrantValidationContext context) |
|
||||
{ |
|
||||
return Task.FromResult(context.Request?.Client?.ClientId); |
|
||||
} |
|
||||
|
|
||||
protected virtual Task AddCustomClaimsAsync( |
|
||||
List<Claim> customClaims, |
|
||||
IdentityUser user, |
|
||||
WeChatOpenId wechatOpenId, |
|
||||
ExtensionGrantValidationContext context) |
|
||||
{ |
|
||||
if (user.TenantId.HasValue) |
|
||||
{ |
|
||||
customClaims.Add( |
|
||||
new Claim( |
|
||||
AbpClaimTypes.TenantId, |
|
||||
user.TenantId?.ToString() |
|
||||
) |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
customClaims.Add( |
|
||||
new Claim( |
|
||||
AbpWeChatClaimTypes.OpenId, |
|
||||
wechatOpenId.OpenId)); |
|
||||
|
|
||||
if (!wechatOpenId.UnionId.IsNullOrWhiteSpace()) |
|
||||
{ |
|
||||
customClaims.Add( |
|
||||
new Claim( |
|
||||
AbpWeChatClaimTypes.UnionId, |
|
||||
wechatOpenId.UnionId)); |
|
||||
} |
|
||||
|
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,86 +0,0 @@ |
|||||
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 async virtual 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 async virtual Task AddIdentityResourceIfNotExistsAsync(IdentityServer4.Models.IdentityResource resource) |
|
||||
{ |
|
||||
if (await IdentityResourceRepository.CheckNameExistAsync(resource.Name)) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
await IdentityResourceRepository.InsertAsync( |
|
||||
new IdentityResource( |
|
||||
GuidGenerator.Create(), |
|
||||
resource |
|
||||
) |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
protected async virtual Task AddClaimTypeIfNotExistsAsync(string claimType) |
|
||||
{ |
|
||||
if (await ClaimTypeRepository.AnyAsync(claimType)) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
await ClaimTypeRepository.InsertAsync( |
|
||||
new IdentityClaimType( |
|
||||
GuidGenerator.Create(), |
|
||||
claimType, |
|
||||
isStatic: true |
|
||||
) |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,336 +0,0 @@ |
|||||
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 AbpWeChatOfficialOptionsFactory WeChatOfficialOptionsFactory { get; } |
|
||||
public WeChatOfficialOAuthHandler( |
|
||||
IDistributedCache<WeChatOfficialStateCacheItem> cache, |
|
||||
IOptionsMonitor<WeChatOfficialOAuthOptions> options, |
|
||||
AbpWeChatOfficialOptionsFactory weChatOfficialOptionsFactory, |
|
||||
ILoggerFactory logger, |
|
||||
UrlEncoder encoder, |
|
||||
ISystemClock clock) |
|
||||
: base(options, logger, encoder, clock) |
|
||||
{ |
|
||||
Cache = cache; |
|
||||
WeChatOfficialOptionsFactory = weChatOfficialOptionsFactory; |
|
||||
} |
|
||||
|
|
||||
protected override async Task InitializeHandlerAsync() |
|
||||
{ |
|
||||
var weChatOfficialOptions = await WeChatOfficialOptionsFactory.CreateAsync(); |
|
||||
|
|
||||
// 用配置项重写
|
|
||||
Options.ClientId = weChatOfficialOptions.AppId; |
|
||||
Options.ClientSecret = weChatOfficialOptions.AppSecret; |
|
||||
|
|
||||
await 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.ToString().ToMd5(), 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之后再进行缓存
|
|
||||
// 注: 默认的State对于微信来说太长(微信只支持128位长度的State),因此巧妙的利用CorrelationId的MD5值来替代State
|
|
||||
// MD5转换防止直接通过CorrelationId干些别的事情...
|
|
||||
var state = properties.Items[".xsrf"]; |
|
||||
|
|
||||
var stateToken = Options.StateDataFormat.Protect(properties); |
|
||||
var stateCacheKey = WeChatOfficialStateCacheItem.CalculateCacheKey(state.ToMd5(), 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.ToMd5()}"; |
|
||||
|
|
||||
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.ToString().ToMd5(), 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,47 +0,0 @@ |
|||||
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() |
|
||||
{ |
|
||||
// 用于防止初始化错误,会在OAuthHandler.InitializeHandlerAsync中进行重写
|
|
||||
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")); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,18 +0,0 @@ |
|||||
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"}"; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,65 +0,0 @@ |
|||||
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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,22 +0,0 @@ |
|||||
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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue