32 changed files with 843 additions and 115 deletions
@ -0,0 +1,9 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace LINGYUN.Abp.IdentityServer |
||||
|
{ |
||||
|
public interface IWeChatResourceDataSeeder |
||||
|
{ |
||||
|
Task CreateStandardResourcesAsync(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,85 @@ |
|||||
|
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 |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,316 @@ |
|||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
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")); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
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"}"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
using System.Security.Cryptography; |
||||
|
|
||||
|
namespace System |
||||
|
{ |
||||
|
internal static class BytesExtensions |
||||
|
{ |
||||
|
public static byte[] Sha1(this byte[] data) |
||||
|
{ |
||||
|
using (var sha = SHA1.Create()) |
||||
|
{ |
||||
|
var hashBytes = sha.ComputeHash(data); |
||||
|
return hashBytes; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
using System.Security.Cryptography; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace System |
||||
|
{ |
||||
|
internal static class StringExtensions |
||||
|
{ |
||||
|
public static byte[] Sha1(this string str) |
||||
|
{ |
||||
|
using (var sha = SHA1.Create()) |
||||
|
{ |
||||
|
var hashBytes = sha.ComputeHash(Encoding.ASCII.GetBytes(str)); |
||||
|
return hashBytes; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace System.Text.Json |
||||
|
{ |
||||
|
internal static class JsonElementExtensions |
||||
|
{ |
||||
|
public static IEnumerable<string> GetRootStrings(this JsonDocument json, string key) |
||||
|
{ |
||||
|
return json.RootElement.GetStrings(key); |
||||
|
} |
||||
|
|
||||
|
public static IEnumerable<string> GetStrings(this JsonElement json, string key) |
||||
|
{ |
||||
|
var result = new List<string>(); |
||||
|
|
||||
|
if (json.TryGetProperty(key, out JsonElement property) && property.ValueKind == JsonValueKind.Array) |
||||
|
{ |
||||
|
foreach (var jsonProp in property.EnumerateArray()) |
||||
|
{ |
||||
|
result.Add(jsonProp.GetString()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
public static string GetRootString(this JsonDocument json, string key, string defaultValue = "") |
||||
|
{ |
||||
|
if (json.RootElement.TryGetProperty(key, out JsonElement property)) |
||||
|
{ |
||||
|
return property.GetString(); |
||||
|
} |
||||
|
return defaultValue; |
||||
|
} |
||||
|
|
||||
|
public static string GetString(this JsonElement json, string key, string defaultValue = "") |
||||
|
{ |
||||
|
if (json.TryGetProperty(key, out JsonElement property)) |
||||
|
{ |
||||
|
return property.GetString(); |
||||
|
} |
||||
|
return defaultValue; |
||||
|
} |
||||
|
|
||||
|
public static int GetRootInt32(this JsonDocument json, string key, int defaultValue = 0) |
||||
|
{ |
||||
|
if (json.RootElement.TryGetProperty(key, out JsonElement property) && property.TryGetInt32(out int value)) |
||||
|
{ |
||||
|
return value; |
||||
|
} |
||||
|
return defaultValue; |
||||
|
} |
||||
|
|
||||
|
public static int GetInt32(this JsonElement json, string key, int defaultValue = 0) |
||||
|
{ |
||||
|
if (json.TryGetProperty(key, out JsonElement property) && property.TryGetInt32(out int value)) |
||||
|
{ |
||||
|
return value; |
||||
|
} |
||||
|
return defaultValue; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
namespace LINGYUN.Abp.WeChat.Authorization |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 与微信认证相关的静态(可变)常量
|
||||
|
/// </summary>
|
||||
|
public static class AbpWeChatAuthorizationConsts |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 微信授权名称
|
||||
|
/// </summary>
|
||||
|
public const string AuthenticationScheme = "WeChat"; |
||||
|
/// <summary>
|
||||
|
/// 微信授权显示名称
|
||||
|
/// </summary>
|
||||
|
public static string DisplayName = "WeChat"; |
||||
|
/// <summary>
|
||||
|
/// 微信个人信息标识
|
||||
|
/// </summary>
|
||||
|
public static string ProfileKey { get; set; } = "wechat.profile"; |
||||
|
/// <summary>
|
||||
|
/// 微信提供者标识
|
||||
|
/// </summary>
|
||||
|
public static string ProviderKey { get; set; } = AuthenticationScheme; |
||||
|
/// <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,6 +1,6 @@ |
|||||
namespace LINGYUN.Abp.WeChat.Authorization |
namespace LINGYUN.Abp.WeChat.Authorization |
||||
{ |
{ |
||||
public class AbpWeChatOptions |
public class AbpWeChatAuthorizationOptions |
||||
{ |
{ |
||||
public string AppId { get; set; } |
public string AppId { get; set; } |
||||
public string AppSecret { get; set; } |
public string AppSecret { get; set; } |
||||
@ -0,0 +1,48 @@ |
|||||
|
namespace LINGYUN.Abp.WeChat.Authorization |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 微信认证身份类型,可以像 <see cref="Volo.Abp.Security.Claims.AbpClaimTypes"/> 自行配置
|
||||
|
/// <br />
|
||||
|
/// See: <see cref="https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html"/>
|
||||
|
/// </summary>
|
||||
|
public class AbpWeChatClaimTypes |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 用户的唯一标识
|
||||
|
/// </summary>
|
||||
|
public static string OpenId { get; set; } = "wx-openid"; // 可变更
|
||||
|
/// <summary>
|
||||
|
/// 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。
|
||||
|
/// </summary>
|
||||
|
public static string UnionId { get; set; } = "wx-unionid"; //可变更
|
||||
|
/// <summary>
|
||||
|
/// 用户昵称
|
||||
|
/// </summary>
|
||||
|
public static string NickName { get; set; } = "nickname"; |
||||
|
/// <summary>
|
||||
|
/// 用户的性别,值为1时是男性,值为2时是女性,值为0时是未知
|
||||
|
/// </summary>
|
||||
|
public static string Sex { get; set; } = "sex"; |
||||
|
/// <summary>
|
||||
|
/// 国家,如中国为CN
|
||||
|
/// </summary>
|
||||
|
public static string Country { get; set; } = "country"; |
||||
|
/// <summary>
|
||||
|
/// 用户个人资料填写的省份
|
||||
|
/// </summary>
|
||||
|
public static string Province { get; set; } = "province"; |
||||
|
/// <summary>
|
||||
|
/// 普通用户个人资料填写的城市
|
||||
|
/// </summary>
|
||||
|
public static string City { get; set; } = "city"; |
||||
|
/// <summary>
|
||||
|
/// 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空。
|
||||
|
/// 若用户更换头像,原有头像URL将失效。
|
||||
|
/// </summary>
|
||||
|
public static string AvatarUrl { get; set; } = "avatar"; |
||||
|
/// <summary>
|
||||
|
/// 用户特权信息,json 数组,如微信沃卡用户为(chinaunicom)
|
||||
|
/// </summary>
|
||||
|
public static string Privilege { get; set; } = "privilege"; |
||||
|
} |
||||
|
} |
||||
@ -1,22 +0,0 @@ |
|||||
namespace LINGYUN.Abp.WeChat.Authorization |
|
||||
{ |
|
||||
public class WeChatAuthorizationConsts |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// 微信提供者标识
|
|
||||
/// </summary>
|
|
||||
public static string ProviderKey { get; set; } = "WeChat"; |
|
||||
/// <summary>
|
|
||||
/// 微信Code参数名称
|
|
||||
/// </summary>
|
|
||||
public static string WeCahtCodeKey { get; set; } = "wx-code"; |
|
||||
/// <summary>
|
|
||||
/// 微信OpenId参数名称
|
|
||||
/// </summary>
|
|
||||
public static string WeCahtOpenIdKey { get; set; } = "wx-open-id"; |
|
||||
/// <summary>
|
|
||||
/// 微信SessionKey参数名称
|
|
||||
/// </summary>
|
|
||||
public static string WeCahtSessionKey { get; set; } = "wx-session-key"; |
|
||||
} |
|
||||
} |
|
||||
@ -1,8 +0,0 @@ |
|||||
namespace Volo.Abp.Security.Claims |
|
||||
{ |
|
||||
public class WeChatClaimTypes |
|
||||
{ |
|
||||
public static string OpenId { get; set; } = "wx-openid"; |
|
||||
public static string UnionId { get; set; } = "wx-unionid"; |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue