69 changed files with 1055 additions and 941 deletions
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,24 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net9.0</TargetFramework> |
||||
|
<AssemblyName>LINGYUN.Abp.WeChat.Work.AspNetCore</AssemblyName> |
||||
|
<PackageId>LINGYUN.Abp.WeChat.Work.AspNetCore</PackageId> |
||||
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
||||
|
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
||||
|
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Volo.Abp.AspNetCore" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.WeChat.Work\LINGYUN.Abp.WeChat.Work.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,11 @@ |
|||||
|
using Volo.Abp.AspNetCore; |
||||
|
using Volo.Abp.Modularity; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work.AspNetCore; |
||||
|
|
||||
|
[DependsOn( |
||||
|
typeof(AbpWeChatWorkModule), |
||||
|
typeof(AbpAspNetCoreModule))] |
||||
|
public class AbpWeChatWorkAspNetCoreModule : AbpModule |
||||
|
{ |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Work; |
||||
|
|
||||
|
namespace Microsoft.AspNetCore.Authentication.WeChat.Work; |
||||
|
|
||||
|
public static class WeChatWorkOAuthConsts |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 微信个人信息标识
|
||||
|
/// </summary>
|
||||
|
public static string ProfileKey => AbpWeChatWorkGlobalConsts.ProfileKey; |
||||
|
/// <summary>
|
||||
|
/// 微信提供者标识
|
||||
|
/// </summary>
|
||||
|
public static string ProviderKey => AbpWeChatWorkGlobalConsts.ProviderName; |
||||
|
/// <summary>
|
||||
|
/// 微信提供者显示名称
|
||||
|
/// </summary>
|
||||
|
public static string DisplayName => AbpWeChatWorkGlobalConsts.DisplayName; |
||||
|
/// <summary>
|
||||
|
/// 回调地址
|
||||
|
/// </summary>
|
||||
|
public static string CallbackPath { get; set; } = "/signin-wxwork"; |
||||
|
/// <summary>
|
||||
|
/// 微信客户端内的网页登录
|
||||
|
/// </summary>
|
||||
|
public const string AuthorizationEndpoint = "https://login.work.weixin.qq.com/wwlogin/sso/login"; |
||||
|
/// <summary>
|
||||
|
/// 用户允许授权后通过返回的code换取access_token地址
|
||||
|
/// </summary>
|
||||
|
public const string TokenEndpoint = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 使用access_token获取用户个人信息地址
|
||||
|
/// </summary>
|
||||
|
public const string UserInformationEndpoint = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo"; |
||||
|
|
||||
|
public const string UserInfoScope = "snsapi_privateinfo"; |
||||
|
|
||||
|
public const string LoginScope = "snsapi_base"; |
||||
|
} |
||||
@ -0,0 +1,329 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Work.Settings; |
||||
|
using LINGYUN.Abp.WeChat.Work.Token; |
||||
|
using Microsoft.AspNetCore.Authentication.OAuth; |
||||
|
using Microsoft.AspNetCore.WebUtilities; |
||||
|
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.Linq; |
||||
|
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; |
||||
|
using Volo.Abp.Settings; |
||||
|
|
||||
|
namespace Microsoft.AspNetCore.Authentication.WeChat.Work; |
||||
|
|
||||
|
public class WeChatWorkOAuthHandler : OAuthHandler<WeChatWorkOAuthOptions> |
||||
|
{ |
||||
|
protected ISettingProvider SettingProvider { get; } |
||||
|
public WeChatWorkOAuthHandler( |
||||
|
ISettingProvider settingProvider, |
||||
|
IWeChatWorkTokenProvider wechatWorkTokenProvider, |
||||
|
IOptionsMonitor<WeChatWorkOAuthOptions> options, |
||||
|
ILoggerFactory logger, |
||||
|
UrlEncoder encoder) |
||||
|
: base(options, logger, encoder) |
||||
|
{ |
||||
|
SettingProvider = settingProvider; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 重写应用配置
|
||||
|
/// </summary>
|
||||
|
/// <returns></returns>
|
||||
|
protected async override Task InitializeHandlerAsync() |
||||
|
{ |
||||
|
var settings = await SettingProvider.GetAllAsync( |
||||
|
new[] { |
||||
|
WeChatWorkSettingNames.Connection.CorpId, |
||||
|
WeChatWorkSettingNames.Connection.AgentId, |
||||
|
WeChatWorkSettingNames.Connection.Secret, |
||||
|
}); |
||||
|
|
||||
|
var corpId = settings.FirstOrDefault(x => x.Name == WeChatWorkSettingNames.Connection.CorpId)?.Value; |
||||
|
var agentId = settings.FirstOrDefault(x => x.Name == WeChatWorkSettingNames.Connection.AgentId)?.Value; |
||||
|
var secret = settings.FirstOrDefault(x => x.Name == WeChatWorkSettingNames.Connection.Secret)?.Value; |
||||
|
|
||||
|
Check.NotNullOrEmpty(corpId, nameof(corpId)); |
||||
|
Check.NotNullOrEmpty(agentId, nameof(agentId)); |
||||
|
Check.NotNullOrEmpty(secret, nameof(secret)); |
||||
|
|
||||
|
// 用配置项重写
|
||||
|
Options.CorpId = corpId; |
||||
|
Options.ClientId = agentId; |
||||
|
Options.ClientSecret = secret; |
||||
|
|
||||
|
Options.TimeProvider ??= TimeProvider.System; |
||||
|
|
||||
|
await base.InitializeHandlerAsync(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 第一步:构建用户授权地址
|
||||
|
/// </summary>
|
||||
|
protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri) |
||||
|
{ |
||||
|
var isWeChatBrewserRequest = IsWeChatBrowser(); |
||||
|
|
||||
|
var scope = isWeChatBrewserRequest |
||||
|
? WeChatWorkOAuthConsts.UserInfoScope |
||||
|
: WeChatWorkOAuthConsts.LoginScope; |
||||
|
|
||||
|
var parameters = new Dictionary<string, string> |
||||
|
{ |
||||
|
{ "appid", Options.CorpId }, |
||||
|
{ "redirect_uri", redirectUri }, |
||||
|
{ "response_type", "code" }, |
||||
|
{ "scope", scope }, |
||||
|
{ "agentid", Options.ClientId }, |
||||
|
{ "lang", "zh" }, |
||||
|
}; |
||||
|
|
||||
|
var state = Options.StateDataFormat.Protect(properties); |
||||
|
|
||||
|
parameters["state"] = state; ; |
||||
|
|
||||
|
return $"{QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, parameters)}"; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 第二步:code换取access_token
|
||||
|
/// </summary>
|
||||
|
protected async override Task<OAuthTokenResponse> ExchangeCodeAsync(OAuthCodeExchangeContext context) |
||||
|
{ |
||||
|
var parameters = new Dictionary<string, string>() |
||||
|
{ |
||||
|
{ "corpid", Options.CorpId }, |
||||
|
{ "corpsecret", Options.ClientSecret }, |
||||
|
}; |
||||
|
|
||||
|
var address = QueryHelpers.AddQueryString(Options.TokenEndpoint, parameters); |
||||
|
|
||||
|
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 (payload.GetRootInt32("errcode") != 0) |
||||
|
{ |
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 第三步:构建用户票据
|
||||
|
/// </summary>
|
||||
|
/// <param name="identity"></param>
|
||||
|
/// <param name="properties"></param>
|
||||
|
/// <param name="tokens"></param>
|
||||
|
/// <returns></returns>
|
||||
|
/// <exception cref="HttpRequestException"></exception>
|
||||
|
protected async virtual Task<AuthenticationTicket> CreateTicketAsync(string code, ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) |
||||
|
{ |
||||
|
var address = QueryHelpers.AddQueryString(Options.UserInformationEndpoint, new Dictionary<string, string> |
||||
|
{ |
||||
|
["access_token"] = tokens.AccessToken, |
||||
|
["code"] = code |
||||
|
}); |
||||
|
|
||||
|
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 (payload.GetRootInt32("errcode") != 0) |
||||
|
{ |
||||
|
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); |
||||
|
|
||||
|
return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); |
||||
|
} |
||||
|
|
||||
|
protected async override Task<HandleRequestResult> HandleRemoteAuthenticateAsync() |
||||
|
{ |
||||
|
var query = Request.Query; |
||||
|
|
||||
|
var state = query["state"]; |
||||
|
|
||||
|
var properties = Options.StateDataFormat.Unprotect(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 = Options.TimeProvider.GetUtcNow() + TimeSpan.FromSeconds(value); |
||||
|
authTokens.Add(new AuthenticationToken |
||||
|
{ |
||||
|
Name = "expires_at", |
||||
|
Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
properties.StoreTokens(authTokens); |
||||
|
} |
||||
|
|
||||
|
var ticket = await CreateTicketAsync(code, identity, properties, tokens); |
||||
|
if (ticket != null) |
||||
|
{ |
||||
|
return HandleRequestResult.Success(ticket); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return HandleRequestResult.Fail("Failed to retrieve user information from remote server.", properties); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual bool IsWeChatBrowser() |
||||
|
{ |
||||
|
var userAgent = Request.Headers[HeaderNames.UserAgent].ToString(); |
||||
|
|
||||
|
return userAgent.Contains("micromessenger", StringComparison.InvariantCultureIgnoreCase); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Work.Security.Claims; |
||||
|
using Microsoft.AspNetCore.Authentication.OAuth; |
||||
|
using Microsoft.AspNetCore.Http; |
||||
|
using System.Security.Claims; |
||||
|
using Volo.Abp.Security.Claims; |
||||
|
|
||||
|
namespace Microsoft.AspNetCore.Authentication.WeChat.Work; |
||||
|
|
||||
|
public class WeChatWorkOAuthOptions : OAuthOptions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 企业Id
|
||||
|
/// </summary>
|
||||
|
public string CorpId { get; set; } |
||||
|
public WeChatWorkOAuthOptions() |
||||
|
{ |
||||
|
// 用于防止初始化错误,会在OAuthHandler.InitializeHandlerAsync中进行重写
|
||||
|
CorpId = "CorpId"; |
||||
|
ClientId = "ClientId"; |
||||
|
ClientSecret = "ClientSecret"; |
||||
|
|
||||
|
CallbackPath = new PathString(WeChatWorkOAuthConsts.CallbackPath); |
||||
|
|
||||
|
AuthorizationEndpoint = WeChatWorkOAuthConsts.AuthorizationEndpoint; |
||||
|
TokenEndpoint = WeChatWorkOAuthConsts.TokenEndpoint; |
||||
|
UserInformationEndpoint = WeChatWorkOAuthConsts.UserInformationEndpoint; |
||||
|
|
||||
|
Scope.Add(WeChatWorkOAuthConsts.LoginScope); |
||||
|
Scope.Add(WeChatWorkOAuthConsts.UserInfoScope); |
||||
|
|
||||
|
ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "userid"); |
||||
|
ClaimActions.MapJsonKey(ClaimTypes.Name, "userid"); |
||||
|
ClaimActions.MapJsonKey("sub", "userid"); |
||||
|
|
||||
|
// 把自定义的身份标识写进令牌
|
||||
|
ClaimActions.MapJsonKey(AbpWeChatWorkClaimTypes.UserId, "userid"); |
||||
|
ClaimActions.MapJsonKey(AbpWeChatWorkClaimTypes.QrCode, "qr_code"); |
||||
|
ClaimActions.MapJsonKey(AbpWeChatWorkClaimTypes.BizMail, "biz_mail"); |
||||
|
ClaimActions.MapJsonKey(AbpWeChatWorkClaimTypes.Address, "address"); |
||||
|
ClaimActions.MapJsonKey(AbpClaimTypes.PhoneNumber, "mobile"); |
||||
|
ClaimActions.MapJsonKey(AbpClaimTypes.Email, "email"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
using LINGYUN.Abp.WeChat.Work; |
||||
|
using Microsoft.AspNetCore.Authentication.WeChat.Work; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using System; |
||||
|
|
||||
|
namespace Microsoft.AspNetCore.Authentication; |
||||
|
|
||||
|
public static class WeChatWorkAuthenticationExtensions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// </summary>
|
||||
|
public static AuthenticationBuilder AddWeChatWork( |
||||
|
this AuthenticationBuilder builder) |
||||
|
{ |
||||
|
return builder |
||||
|
.AddWeChatWork( |
||||
|
AbpWeChatWorkGlobalConsts.AuthenticationScheme, |
||||
|
AbpWeChatWorkGlobalConsts.DisplayName, |
||||
|
options => { }); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// </summary>
|
||||
|
public static AuthenticationBuilder AddWeChatWork( |
||||
|
this AuthenticationBuilder builder, |
||||
|
Action<WeChatWorkOAuthOptions> configureOptions) |
||||
|
{ |
||||
|
return builder |
||||
|
.AddWeChatWork( |
||||
|
AbpWeChatWorkGlobalConsts.AuthenticationScheme, |
||||
|
AbpWeChatWorkGlobalConsts.DisplayName, |
||||
|
configureOptions); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// </summary>
|
||||
|
public static AuthenticationBuilder AddWeChatWork( |
||||
|
this AuthenticationBuilder builder, |
||||
|
string authenticationScheme, |
||||
|
Action<WeChatWorkOAuthOptions> configureOptions) |
||||
|
{ |
||||
|
return builder |
||||
|
.AddWeChatWork( |
||||
|
authenticationScheme, |
||||
|
AbpWeChatWorkGlobalConsts.DisplayName, |
||||
|
configureOptions); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// </summary>
|
||||
|
public static AuthenticationBuilder AddWeChatWork( |
||||
|
this AuthenticationBuilder builder, |
||||
|
string authenticationScheme, |
||||
|
string displayName, |
||||
|
Action<WeChatWorkOAuthOptions> configureOptions) |
||||
|
{ |
||||
|
return builder |
||||
|
.AddOAuth<WeChatWorkOAuthOptions, WeChatWorkOAuthHandler>( |
||||
|
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,26 @@ |
|||||
|
using System; |
||||
|
using System.Text.Json; |
||||
|
using System.Text.Json.Serialization; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WeChat.Work; |
||||
|
|
||||
|
internal class NumberToStringConverter : JsonConverter<string> |
||||
|
{ |
||||
|
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) |
||||
|
{ |
||||
|
if (reader.TokenType == JsonTokenType.Number) |
||||
|
{ |
||||
|
return reader.GetInt32().ToString(); |
||||
|
} |
||||
|
if (reader.TokenType == JsonTokenType.String) |
||||
|
{ |
||||
|
return reader.GetString(); |
||||
|
} |
||||
|
throw new JsonException("Unexpected token type"); |
||||
|
} |
||||
|
|
||||
|
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) |
||||
|
{ |
||||
|
writer.WriteStringValue(value); |
||||
|
} |
||||
|
} |
||||
@ -1,35 +0,0 @@ |
|||||
using System.Collections.Generic; |
|
||||
|
|
||||
namespace LINGYUN.Abp.WeChat.Work.Security; |
|
||||
/// <summary>
|
|
||||
/// 企业微信加解密配置
|
|
||||
/// </summary>
|
|
||||
public class WeChatWorkCryptoConfiguration : Dictionary<string, string> |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// 用于生成签名的Token
|
|
||||
/// </summary>
|
|
||||
public string Token { |
|
||||
get => this.GetOrDefault(nameof(Token)); |
|
||||
set => this[nameof(Token)] = value; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// 用于消息加密的密钥
|
|
||||
/// </summary>
|
|
||||
public string EncodingAESKey { |
|
||||
get => this.GetOrDefault(nameof(EncodingAESKey)); |
|
||||
set => this[nameof(EncodingAESKey)] = value; |
|
||||
} |
|
||||
|
|
||||
public WeChatWorkCryptoConfiguration() |
|
||||
{ |
|
||||
|
|
||||
} |
|
||||
|
|
||||
public WeChatWorkCryptoConfiguration(string token, string encodingAESKey) |
|
||||
{ |
|
||||
this[nameof(Token)] = token; |
|
||||
this[nameof(EncodingAESKey)] = encodingAESKey; |
|
||||
} |
|
||||
} |
|
||||
@ -1,12 +0,0 @@ |
|||||
using JetBrains.Annotations; |
|
||||
using System.Collections.Generic; |
|
||||
|
|
||||
namespace LINGYUN.Abp.WeChat.Work.Security; |
|
||||
public class WeChatWorkCryptoConfigurationDictionary : Dictionary<string, WeChatWorkCryptoConfiguration> |
|
||||
{ |
|
||||
[CanBeNull] |
|
||||
public WeChatWorkCryptoConfiguration GetCryptoConfigurationOrNull(string feture) |
|
||||
{ |
|
||||
return this.GetOrDefault(feture); |
|
||||
} |
|
||||
} |
|
||||
@ -1,43 +0,0 @@ |
|||||
using JetBrains.Annotations; |
|
||||
using LINGYUN.Abp.WeChat.Work.Security; |
|
||||
|
|
||||
namespace LINGYUN.Abp.WeChat.Work; |
|
||||
/// <summary>
|
|
||||
/// 企业微信应用配置
|
|
||||
/// </summary>
|
|
||||
public class WeChatWorkApplicationConfiguration |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// 应用的标识
|
|
||||
/// </summary>
|
|
||||
public string AgentId { get; set; } |
|
||||
/// <summary>
|
|
||||
/// 应用的凭证密钥
|
|
||||
/// </summary>
|
|
||||
public string Secret { get; set; } |
|
||||
/// <summary>
|
|
||||
/// 应用加密配置
|
|
||||
/// </summary>
|
|
||||
public WeChatWorkCryptoConfigurationDictionary CryptoKeys { get; set; } |
|
||||
|
|
||||
public WeChatWorkApplicationConfiguration() |
|
||||
{ |
|
||||
CryptoKeys = new WeChatWorkCryptoConfigurationDictionary(); |
|
||||
} |
|
||||
|
|
||||
public WeChatWorkApplicationConfiguration(string agentId, string secret) |
|
||||
{ |
|
||||
AgentId = agentId; |
|
||||
Secret = secret; |
|
||||
CryptoKeys = new WeChatWorkCryptoConfigurationDictionary(); |
|
||||
} |
|
||||
|
|
||||
[NotNull] |
|
||||
public WeChatWorkCryptoConfiguration GetCryptoConfiguration(string feture) |
|
||||
{ |
|
||||
return CryptoKeys.GetCryptoConfigurationOrNull(feture) |
|
||||
?? throw new AbpWeChatWorkException("WeChatWork:101404", $"WeChat Work crypto was not found configuration with feture '{feture}' .") |
|
||||
.WithData("AgentId", AgentId) |
|
||||
.WithData("Feture", feture); |
|
||||
} |
|
||||
} |
|
||||
@ -1,14 +0,0 @@ |
|||||
using JetBrains.Annotations; |
|
||||
using System.Collections.Generic; |
|
||||
|
|
||||
namespace LINGYUN.Abp.WeChat.Work; |
|
||||
public class WeChatWorkApplicationConfigurationDictionary : Dictionary<string, WeChatWorkApplicationConfiguration> |
|
||||
{ |
|
||||
[NotNull] |
|
||||
public WeChatWorkApplicationConfiguration GetConfiguration(string agentId) |
|
||||
{ |
|
||||
return this.GetOrDefault(agentId) |
|
||||
?? throw new AbpWeChatWorkException("WeChatWork:100404", $"WeChat Work application was not found configuration with agent '{agentId}' .") |
|
||||
.WithData("AgentId", agentId); |
|
||||
} |
|
||||
} |
|
||||
@ -1,11 +0,0 @@ |
|||||
namespace LINGYUN.Abp.WeChat.Work; |
|
||||
|
|
||||
public class WeChatWorkOptions |
|
||||
{ |
|
||||
public WeChatWorkApplicationConfigurationDictionary Applications { get; set; } |
|
||||
|
|
||||
public WeChatWorkOptions() |
|
||||
{ |
|
||||
Applications = new WeChatWorkApplicationConfigurationDictionary(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,91 +0,0 @@ |
|||||
using Microsoft.AspNetCore.Authentication; |
|
||||
using Microsoft.AspNetCore.Authentication.Cookies; |
|
||||
using Microsoft.Extensions.Options; |
|
||||
using Microsoft.Net.Http.Headers; |
|
||||
using System.Text.Encodings.Web; |
|
||||
using Volo.Abp.Http; |
|
||||
|
|
||||
namespace LY.MicroService.Applications.Single.Authentication; |
|
||||
|
|
||||
public class AbpCookieAuthenticationHandler : CookieAuthenticationHandler |
|
||||
{ |
|
||||
public AbpCookieAuthenticationHandler( |
|
||||
IOptionsMonitor<CookieAuthenticationOptions> options, |
|
||||
ILoggerFactory logger, |
|
||||
UrlEncoder encoder) : base(options, logger, encoder) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public AbpCookieAuthenticationHandler( |
|
||||
IOptionsMonitor<CookieAuthenticationOptions> options, |
|
||||
ILoggerFactory logger, |
|
||||
UrlEncoder encoder, |
|
||||
ISystemClock clock) : base(options, logger, encoder, clock) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
protected const string XRequestFromHeader = "X-Request-From"; |
|
||||
protected const string DontRedirectRequestFromHeader = "vben"; |
|
||||
protected override Task InitializeEventsAsync() |
|
||||
{ |
|
||||
var events = new CookieAuthenticationEvents |
|
||||
{ |
|
||||
OnRedirectToLogin = ctx => |
|
||||
{ |
|
||||
if (string.Equals(ctx.Request.Headers[XRequestFromHeader], DontRedirectRequestFromHeader, StringComparison.Ordinal)) |
|
||||
{ |
|
||||
// ctx.Response.Headers.Location = ctx.RedirectUri;
|
|
||||
ctx.Response.StatusCode = 401; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
ctx.Response.Redirect(ctx.RedirectUri); |
|
||||
} |
|
||||
return Task.CompletedTask; |
|
||||
}, |
|
||||
OnRedirectToAccessDenied = ctx => |
|
||||
{ |
|
||||
if (string.Equals(ctx.Request.Headers[XRequestFromHeader], DontRedirectRequestFromHeader, StringComparison.Ordinal)) |
|
||||
{ |
|
||||
// ctx.Response.Headers.Location = ctx.RedirectUri;
|
|
||||
ctx.Response.StatusCode = 401; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
ctx.Response.Redirect(ctx.RedirectUri); |
|
||||
} |
|
||||
return Task.CompletedTask; |
|
||||
}, |
|
||||
OnRedirectToLogout = ctx => |
|
||||
{ |
|
||||
if (string.Equals(ctx.Request.Headers[XRequestFromHeader], DontRedirectRequestFromHeader, StringComparison.Ordinal)) |
|
||||
{ |
|
||||
// ctx.Response.Headers.Location = ctx.RedirectUri;
|
|
||||
ctx.Response.StatusCode = 401; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
ctx.Response.Redirect(ctx.RedirectUri); |
|
||||
} |
|
||||
return Task.CompletedTask; |
|
||||
}, |
|
||||
OnRedirectToReturnUrl = ctx => |
|
||||
{ |
|
||||
if (string.Equals(ctx.Request.Headers[XRequestFromHeader], DontRedirectRequestFromHeader, StringComparison.Ordinal)) |
|
||||
{ |
|
||||
// ctx.Response.Headers.Location = ctx.RedirectUri;
|
|
||||
ctx.Response.StatusCode = 401; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
ctx.Response.Redirect(ctx.RedirectUri); |
|
||||
} |
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
Events = events; |
|
||||
|
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
} |
|
||||
@ -1,16 +0,0 @@ |
|||||
namespace LY.MicroService.Applications.Single.DataSeeder; |
|
||||
|
|
||||
public class DataSeederWorker : BackgroundService |
|
||||
{ |
|
||||
protected IDataSeeder DataSeeder { get; } |
|
||||
|
|
||||
public DataSeederWorker(IDataSeeder dataSeeder) |
|
||||
{ |
|
||||
DataSeeder = dataSeeder; |
|
||||
} |
|
||||
|
|
||||
protected async override Task ExecuteAsync(CancellationToken stoppingToken) |
|
||||
{ |
|
||||
await DataSeeder.SeedAsync(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,16 +0,0 @@ |
|||||
namespace LY.MicroService.Applications.Single.IdentityResources; |
|
||||
|
|
||||
public class CustomIdentityResources |
|
||||
{ |
|
||||
public class AvatarUrl : IdentityServer4.Models.IdentityResource |
|
||||
{ |
|
||||
public AvatarUrl() |
|
||||
{ |
|
||||
Name = IdentityConsts.ClaimType.Avatar.Name; |
|
||||
DisplayName = IdentityConsts.ClaimType.Avatar.DisplayName; |
|
||||
Description = IdentityConsts.ClaimType.Avatar.Description; |
|
||||
Emphasize = true; |
|
||||
UserClaims = new string[] { IdentityConsts.ClaimType.Avatar.Name }; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,89 +0,0 @@ |
|||||
using Microsoft.AspNetCore.Authentication; |
|
||||
using Microsoft.AspNetCore.Authentication.Cookies; |
|
||||
using Microsoft.AspNetCore.Http; |
|
||||
using Microsoft.Extensions.Logging; |
|
||||
using Microsoft.Extensions.Options; |
|
||||
using System.Text.Encodings.Web; |
|
||||
using System.Threading.Tasks; |
|
||||
using Volo.Abp.Http; |
|
||||
|
|
||||
namespace LY.MicroService.AuthServer.Authentication; |
|
||||
|
|
||||
public class AbpCookieAuthenticationHandler : CookieAuthenticationHandler |
|
||||
{ |
|
||||
public AbpCookieAuthenticationHandler( |
|
||||
IOptionsMonitor<CookieAuthenticationOptions> options, |
|
||||
ILoggerFactory logger, |
|
||||
UrlEncoder encoder) : base(options, logger, encoder) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public AbpCookieAuthenticationHandler( |
|
||||
IOptionsMonitor<CookieAuthenticationOptions> options, |
|
||||
ILoggerFactory logger, |
|
||||
UrlEncoder encoder, |
|
||||
ISystemClock clock) : base(options, logger, encoder, clock) |
|
||||
{ |
|
||||
} |
|
||||
protected override Task InitializeEventsAsync() |
|
||||
{ |
|
||||
var events = new CookieAuthenticationEvents |
|
||||
{ |
|
||||
OnRedirectToLogin = ctx => |
|
||||
{ |
|
||||
if (ctx.Request.CanAccept(MimeTypes.Application.Json)) |
|
||||
{ |
|
||||
ctx.Response.Headers.Location = ctx.RedirectUri; |
|
||||
ctx.Response.StatusCode = 401; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
ctx.Response.Redirect(ctx.RedirectUri); |
|
||||
} |
|
||||
return Task.CompletedTask; |
|
||||
}, |
|
||||
OnRedirectToAccessDenied = ctx => |
|
||||
{ |
|
||||
if (ctx.Request.CanAccept(MimeTypes.Application.Json)) |
|
||||
{ |
|
||||
ctx.Response.Headers.Location = ctx.RedirectUri; |
|
||||
ctx.Response.StatusCode = 403; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
ctx.Response.Redirect(ctx.RedirectUri); |
|
||||
} |
|
||||
return Task.CompletedTask; |
|
||||
}, |
|
||||
OnRedirectToLogout = ctx => |
|
||||
{ |
|
||||
if (ctx.Request.CanAccept(MimeTypes.Application.Json)) |
|
||||
{ |
|
||||
ctx.Response.Headers.Location = ctx.RedirectUri; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
ctx.Response.Redirect(ctx.RedirectUri); |
|
||||
} |
|
||||
return Task.CompletedTask; |
|
||||
}, |
|
||||
OnRedirectToReturnUrl = ctx => |
|
||||
{ |
|
||||
if (ctx.Request.CanAccept(MimeTypes.Application.Json)) |
|
||||
{ |
|
||||
ctx.Response.Headers.Location = ctx.RedirectUri; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
ctx.Response.Redirect(ctx.RedirectUri); |
|
||||
} |
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
Events = events; |
|
||||
|
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Loading…
Reference in new issue