diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/WeChatWorkInternalUserFinder.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/WeChatWorkUserClaimProvider.cs similarity index 66% rename from aspnet-core/framework/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/WeChatWorkInternalUserFinder.cs rename to aspnet-core/framework/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/WeChatWorkUserClaimProvider.cs index eb44979a6..fda85cb68 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/WeChatWorkInternalUserFinder.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.Identity.WeChat.Work/LINGYUN/Abp/Identity/WeChat/Work/WeChatWorkUserClaimProvider.cs @@ -8,16 +8,17 @@ using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity; +using Volo.Abp.Uow; namespace LINGYUN.Abp.Identity.WeChat.Work; [Dependency(ServiceLifetime.Transient, ReplaceServices = true)] -[ExposeServices(typeof(IWeChatWorkInternalUserFinder))] -public class WeChatWorkInternalUserFinder : IWeChatWorkInternalUserFinder +[ExposeServices(typeof(IWeChatWorkUserClaimProvider))] +public class WeChatWorkUserClaimProvider : IWeChatWorkUserClaimProvider { protected IdentityUserManager UserManager { get; } - public WeChatWorkInternalUserFinder( + public WeChatWorkUserClaimProvider( IdentityUserManager userManager) { UserManager = userManager; @@ -55,4 +56,22 @@ public class WeChatWorkInternalUserFinder : IWeChatWorkInternalUserFinder return userIdentifiers; } + + [UnitOfWork] + public async virtual Task BindUserAsync( + Guid userId, + string weChatUserId, + CancellationToken cancellationToken = default) + { + var user = await UserManager.GetByIdAsync(userId); + var existsWeChatUserId = GetUserOpenIdOrNull(user, AbpWeChatWorkGlobalConsts.ProviderName); + if (!existsWeChatUserId.IsNullOrWhiteSpace()) + { + user.RemoveLogin(AbpWeChatWorkGlobalConsts.ProviderName, existsWeChatUserId); + } + user.AddLogin(new Microsoft.AspNetCore.Identity.UserLoginInfo( + AbpWeChatWorkGlobalConsts.ProviderName, + weChatUserId, + AbpWeChatWorkGlobalConsts.DisplayName)); + } } diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeAppService.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeAppService.cs index 06f8a7b06..d7369ae4b 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeAppService.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeAppService.cs @@ -4,12 +4,24 @@ using Volo.Abp.Application.Services; namespace LINGYUN.Abp.WeChat.Work.Authorize; public interface IWeChatWorkAuthorizeAppService : IApplicationService { + /// + /// 生成授权链接 + /// + /// 授权回调Url名称 + /// 响应类型 + /// 授权范围 + /// Task GenerateOAuth2AuthorizeAsync( - string redirectUri, + string urlName, string responseType = "code", string scope = "snsapi_base"); - + /// + /// 生成登录链接 + /// + /// 授权回调Url名称 + /// 登录类型 + /// Task GenerateOAuth2LoginAsync( - string redirectUri, - string loginType = "ServiceApp"); + string urlName, + string loginType = "CorpApp"); } diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/Dtos/AgentConfigDto.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/Dtos/AgentConfigDto.cs new file mode 100644 index 000000000..4ce84bffb --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/Dtos/AgentConfigDto.cs @@ -0,0 +1,6 @@ +namespace LINGYUN.Abp.WeChat.Work.JsSdk.Dtos; +public class AgentConfigDto +{ + public string AgentId { get; set; } + public string CorpId { get; set; } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/Dtos/JsApiSignatureDto.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/Dtos/JsApiSignatureDto.cs new file mode 100644 index 000000000..67d321223 --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/Dtos/JsApiSignatureDto.cs @@ -0,0 +1,17 @@ +namespace LINGYUN.Abp.WeChat.Work.JsSdk.Dtos; +public class JsApiSignatureDto +{ + public string Nonce { get; set; } + public string Timestamp { get; set; } + public string Signature { get; set; } + public JsApiSignatureDto() + { + + } + public JsApiSignatureDto(string nonce, string timestamp, string signature) + { + Nonce = nonce; + Timestamp = timestamp; + Signature = signature; + } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/IWeChatWorkJsSdkAppService.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/IWeChatWorkJsSdkAppService.cs new file mode 100644 index 000000000..daccd6f7f --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application.Contracts/LINGYUN/Abp/WeChat/Work/JsSdk/IWeChatWorkJsSdkAppService.cs @@ -0,0 +1,13 @@ +using LINGYUN.Abp.WeChat.Work.JsSdk.Dtos; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace LINGYUN.Abp.WeChat.Work.JsSdk; +public interface IWeChatWorkJsSdkAppService : IApplicationService +{ + Task GetAgentConfigAsync(); + + Task GetSignatureAsync(string url); + + Task GetAgentSignatureAsync(string url); +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN.Abp.WeChat.Work.Application.csproj b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN.Abp.WeChat.Work.Application.csproj index 283c07e8f..8ce77df14 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN.Abp.WeChat.Work.Application.csproj +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN.Abp.WeChat.Work.Application.csproj @@ -15,6 +15,7 @@ + diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkApplicationModule.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkApplicationModule.cs index 0eb380c9c..be9618245 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkApplicationModule.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkApplicationModule.cs @@ -1,11 +1,13 @@ using Volo.Abp.Application; using Volo.Abp.Modularity; +using Volo.Abp.UI.Navigation; namespace LINGYUN.Abp.WeChat.Work; [DependsOn( typeof(AbpWeChatWorkApplicationContractsModule), typeof(AbpWeChatWorkModule), + typeof(AbpUiNavigationModule), typeof(AbpDddApplicationModule))] public class AbpWeChatWorkApplicationModule : AbpModule { diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeAppService.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeAppService.cs index cf3112900..13ab2f28a 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeAppService.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeAppService.cs @@ -1,42 +1,49 @@ -using LINGYUN.Abp.WeChat.Work.Settings; -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Volo.Abp; using Volo.Abp.Application.Services; using Volo.Abp.Security.Encryption; +using Volo.Abp.UI.Navigation.Urls; +using Volo.Abp.Users; namespace LINGYUN.Abp.WeChat.Work.Authorize; [IntegrationService] public class WeChatWorkAuthorizeAppService : ApplicationService, IWeChatWorkAuthorizeAppService { + private readonly IAppUrlProvider _appUrlProvider; private readonly IStringEncryptionService _encryptionService; private readonly IWeChatWorkAuthorizeGenerator _authorizeGenerator; public WeChatWorkAuthorizeAppService( + IAppUrlProvider appUrlProvider, IStringEncryptionService encryptionService, IWeChatWorkAuthorizeGenerator authorizeGenerator) { + _appUrlProvider = appUrlProvider; _encryptionService = encryptionService; _authorizeGenerator = authorizeGenerator; } - public async virtual Task GenerateOAuth2AuthorizeAsync(string redirectUri, string responseType = "code", string scope = "snsapi_base") + public async virtual Task GenerateOAuth2AuthorizeAsync( + string urlName, + string responseType = "code", + string scope = "snsapi_base") { - - var state = _encryptionService.Encrypt($"redirectUri={redirectUri}&responseType={responseType}&scope={scope}&random={Guid.NewGuid():D}").ToMd5(); + var userId = CurrentUser.GetId().ToString("D"); + var state = _encryptionService.Encrypt(userId); + var redirectUri = await _appUrlProvider.GetUrlAsync(AbpWeChatWorkGlobalConsts.ProviderName, urlName); return await _authorizeGenerator.GenerateOAuth2AuthorizeAsync(redirectUri, state, responseType, scope); } - public async virtual Task GenerateOAuth2LoginAsync(string redirectUri, string loginType = "ServiceApp") + public async virtual Task GenerateOAuth2LoginAsync( + string urlName, + string loginType = "CorpApp") { - var state = _encryptionService.Encrypt($"redirectUri={redirectUri}&loginType={loginType}&random={Guid.NewGuid():D}").ToMd5(); - - var corpId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.CorpId); - - Check.NotNullOrEmpty(corpId, nameof(corpId)); + var userId = CurrentUser.GetId().ToString("D"); + var state = _encryptionService.Encrypt(userId); + var redirectUri = await _appUrlProvider.GetUrlAsync(AbpWeChatWorkGlobalConsts.ProviderName, urlName); - return await _authorizeGenerator.GenerateOAuth2LoginAsync(corpId, redirectUri, state, loginType); + return await _authorizeGenerator.GenerateOAuth2LoginAsync(redirectUri, state, loginType); } } diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/JsSdk/WeChatWorkJsSdkAppService.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/JsSdk/WeChatWorkJsSdkAppService.cs new file mode 100644 index 000000000..17d0dba4c --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.Application/LINGYUN/Abp/WeChat/Work/JsSdk/WeChatWorkJsSdkAppService.cs @@ -0,0 +1,50 @@ +using LINGYUN.Abp.WeChat.Work.Features; +using LINGYUN.Abp.WeChat.Work.JsSdk.Dtos; +using LINGYUN.Abp.WeChat.Work.Settings; +using Microsoft.AspNetCore.Authorization; +using System.Threading.Tasks; +using System.Web; +using Volo.Abp.Application.Services; +using Volo.Abp.Features; + +namespace LINGYUN.Abp.WeChat.Work.JsSdk; + +// [Authorize] +[RequiresFeature(WeChatWorkFeatureNames.Enable)] +public class WeChatWorkJsSdkAppService : ApplicationService, IWeChatWorkJsSdkAppService +{ + private readonly IJsApiTicketProvider _ticketProvider; + + public WeChatWorkJsSdkAppService(IJsApiTicketProvider ticketProvider) + { + _ticketProvider = ticketProvider; + } + + public async virtual Task GetAgentConfigAsync() + { + var corpId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.CorpId); + var agentId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.AgentId); + + return new AgentConfigDto + { + CorpId = corpId, + AgentId = agentId, + }; + } + + public async virtual Task GetAgentSignatureAsync(string url) + { + var jsApiTicket = await _ticketProvider.GetAgentTicketInfoAsync(); + var signatureData = _ticketProvider.GenerateSignature(jsApiTicket, HttpUtility.UrlDecode(url)); + + return new JsApiSignatureDto(signatureData.Nonce, signatureData.Timestamp, signatureData.Signature); + } + + public async virtual Task GetSignatureAsync(string url) + { + var jsApiTicket = await _ticketProvider.GetTicketInfoAsync(); + var signatureData = _ticketProvider.GenerateSignature(jsApiTicket, HttpUtility.UrlDecode(url)); + + return new JsApiSignatureDto(signatureData.Nonce, signatureData.Timestamp, signatureData.Signature); + } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeController.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeController.cs index c32f652fe..9130cf8d5 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeController.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using Volo.Abp; using Volo.Abp.AspNetCore.Mvc; @@ -34,19 +35,19 @@ public class WeChatWorkAuthorizeController : AbpControllerBase, IWeChatWorkAutho /// /// 详情见企业微信文档: /// - /// 企业内部应用标识 - /// 登录成功重定向url + /// 授权回调Url名称 /// oauth响应类型 /// oauth授权范围 /// [HttpGet] - [Route("oauth2")] + [Authorize] + [Route("oauth2/generate")] public virtual Task GenerateOAuth2AuthorizeAsync( - [FromQuery(Name = "redirect_uri")] string redirectUri, + [FromQuery] string urlName, [FromQuery(Name = "response_type")] string responseType = "code", [FromQuery] string scope = "snsapi_base") { - return _service.GenerateOAuth2AuthorizeAsync(redirectUri, responseType, scope); + return _service.GenerateOAuth2AuthorizeAsync(responseType, scope); } /// @@ -55,19 +56,19 @@ public class WeChatWorkAuthorizeController : AbpControllerBase, IWeChatWorkAutho /// /// 详情见企业微信文档: /// - /// 企业内部应用标识 - /// 登录成功重定向url + /// 授权回调Url名称 /// oauth响应类型 /// oauth授权范围 /// [HttpGet] - [Route("oauth2/authorize")] + [Authorize] + [Route("oauth2")] public async virtual Task OAuth2AuthorizeAsync( - [FromQuery(Name = "redirect_uri")] string redirectUri, + [FromQuery] string urlName, [FromQuery(Name = "response_type")] string responseType = "code", [FromQuery] string scope = "snsapi_base") { - var url = await _service.GenerateOAuth2AuthorizeAsync(redirectUri, responseType, scope); + var url = await _service.GenerateOAuth2AuthorizeAsync(urlName, responseType, scope); return Redirect(url); } @@ -78,17 +79,17 @@ public class WeChatWorkAuthorizeController : AbpControllerBase, IWeChatWorkAutho /// /// 详情见企业微信文档: /// - /// 登录成功重定向url + /// 授权回调Url名称 /// 登录类型, ServiceApp:服务商登录;CorpApp:企业自建/代开发应用登录 - /// 企业自建应用/服务商代开发应用 AgentID,当login_type=CorpApp时填写 /// [HttpGet] - [Route("oauth2/login")] + [Authorize] + [Route("oauth2/login/generate")] public virtual Task GenerateOAuth2LoginAsync( - [FromQuery(Name = "redirect_uri")] string redirectUri, - [FromQuery(Name = "login_type")] string loginType = "ServiceApp") + string urlName, + string loginType = "CorpApp") { - return _service.GenerateOAuth2LoginAsync(redirectUri, loginType); + return _service.GenerateOAuth2LoginAsync(urlName, loginType); } /// @@ -97,17 +98,17 @@ public class WeChatWorkAuthorizeController : AbpControllerBase, IWeChatWorkAutho /// /// 详情见企业微信文档: /// - /// 登录成功重定向url + /// 授权回调Url名称 /// 登录类型, ServiceApp:服务商登录;CorpApp:企业自建/代开发应用登录 - /// 企业自建应用/服务商代开发应用 AgentID,当login_type=CorpApp时填写 /// [HttpGet] - [Route("oauth2/login/sso")] + [Authorize] + [Route("oauth2/login")] public async virtual Task OAuth2LoginAsync( - [FromQuery(Name = "redirect_uri")] string redirectUri, - [FromQuery(Name = "login_type")] string loginType = "ServiceApp") + string urlName, + string loginType = "CorpApp") { - var url = await _service.GenerateOAuth2LoginAsync(redirectUri, loginType); + var url = await _service.GenerateOAuth2LoginAsync(urlName, loginType); return Redirect(url); } diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/JsSdk/WeChatWorkJsSdkController.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/JsSdk/WeChatWorkJsSdkController.cs new file mode 100644 index 000000000..e28a7ee99 --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work.HttpApi/LINGYUN/Abp/WeChat/Work/JsSdk/WeChatWorkJsSdkController.cs @@ -0,0 +1,46 @@ +using LINGYUN.Abp.WeChat.Work.JsSdk.Dtos; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Auditing; + +namespace LINGYUN.Abp.WeChat.Work.JsSdk; + +// [Authorize] +[Controller] +[DisableAuditing] +[Route("api/wechat/work/jssdk")] +[Area(AbpWeChatWorkRemoteServiceConsts.ModuleName)] +[RemoteService(Name = AbpWeChatWorkRemoteServiceConsts.RemoteServiceName)] +public class WeChatWorkJsSdkController : AbpControllerBase, IWeChatWorkJsSdkAppService +{ + private readonly IWeChatWorkJsSdkAppService _service; + + public WeChatWorkJsSdkController(IWeChatWorkJsSdkAppService service) + { + _service = service; + } + + [HttpGet] + [Route("agent-config")] + public virtual Task GetAgentConfigAsync() + { + return _service.GetAgentConfigAsync(); + } + + [HttpGet] + [Route("agent-signature")] + public virtual Task GetAgentSignatureAsync(string url) + { + return _service.GetAgentSignatureAsync(url); + } + + [HttpGet] + [Route("signature")] + public virtual Task GetSignatureAsync(string url) + { + return _service.GetSignatureAsync(url); + } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkGlobalConsts.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkGlobalConsts.cs index d2722e389..21f8856b5 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkGlobalConsts.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/AbpWeChatWorkGlobalConsts.cs @@ -5,7 +5,7 @@ public class AbpWeChatWorkGlobalConsts /// /// 企业微信对应的Provider名称 /// - public static string ProviderName { get; set; } = "WeCom"; + public static string ProviderName { get; set; } = "WorkWeixin"; /// /// 企业微信授权类型 /// diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeGenerator.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeGenerator.cs index 1b009c805..121915242 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeGenerator.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkAuthorizeGenerator.cs @@ -25,13 +25,13 @@ public interface IWeChatWorkAuthorizeGenerator /// /// /// - /// + /// /// /// Task GenerateOAuth2LoginAsync( string redirectUri, string state, string loginType = "ServiceApp", - string agentid = "", + string agentId = "", string lang = "zh"); } diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkInternalUserFinder.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkUserClaimProvider.cs similarity index 63% rename from aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkInternalUserFinder.cs rename to aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkUserClaimProvider.cs index 42a0511b7..9a6b19960 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkInternalUserFinder.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/IWeChatWorkUserClaimProvider.cs @@ -4,7 +4,10 @@ using System.Threading; using System.Threading.Tasks; namespace LINGYUN.Abp.WeChat.Work.Authorize; -public interface IWeChatWorkInternalUserFinder +/// +/// 企业微信用户身份提供者 +/// +public interface IWeChatWorkUserClaimProvider { /// /// 通过用户标识查询企业微信用户标识 @@ -24,4 +27,15 @@ public interface IWeChatWorkInternalUserFinder Task> FindUserIdentifierListAsync( IEnumerable userIdList, CancellationToken cancellationToken = default); + /// + /// 绑定用户企业微信 + /// + /// 用户Id + /// 企业微信用户Id + /// + /// + Task BindUserAsync( + Guid userId, + string weChatUserId, + CancellationToken cancellationToken = default); } diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/NullWeChatWorkInternalUserFinder.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/NullWeChatWorkUserClaimProvider.cs similarity index 61% rename from aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/NullWeChatWorkInternalUserFinder.cs rename to aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/NullWeChatWorkUserClaimProvider.cs index d9ac3748b..cc4921f84 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/NullWeChatWorkInternalUserFinder.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/NullWeChatWorkUserClaimProvider.cs @@ -8,9 +8,9 @@ using Volo.Abp.DependencyInjection; namespace LINGYUN.Abp.WeChat.Work.Authorize; [Dependency(ServiceLifetime.Singleton, TryRegister = true)] -public class NullWeChatWorkInternalUserFinder : IWeChatWorkInternalUserFinder +public class NullWeChatWorkUserClaimProvider : IWeChatWorkUserClaimProvider { - public readonly static IWeChatWorkInternalUserFinder Instance = new NullWeChatWorkInternalUserFinder(); + public readonly static IWeChatWorkUserClaimProvider Instance = new NullWeChatWorkUserClaimProvider(); public Task FindUserIdentifierAsync( Guid userId, CancellationToken cancellationToken = default) @@ -25,4 +25,11 @@ public class NullWeChatWorkInternalUserFinder : IWeChatWorkInternalUserFinder { return Task.FromResult(new List()); } + public Task BindUserAsync( + Guid userId, + string weChatUserId, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException("请使用 AbpIdentityWeChatWorkModule 模块实现企业微信用户绑定!"); + } } diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeGenerator.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeGenerator.cs index bf684ed89..319b3d2f4 100644 --- a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeGenerator.cs +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/Authorize/WeChatWorkAuthorizeGenerator.cs @@ -57,14 +57,19 @@ public class WeChatWorkAuthorizeGenerator : IWeChatWorkAuthorizeGenerator, ISing } public async virtual Task GenerateOAuth2LoginAsync( - string appid, string redirectUri, string state, string loginType = "ServiceApp", + string agentId = "", string lang = "zh") { - var agentId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.AgentId); + if (agentId.IsNullOrWhiteSpace()) + { + agentId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.AgentId); + } + var corpId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.CorpId); + Check.NotNullOrEmpty(corpId, nameof(corpId)); Check.NotNullOrEmpty(agentId, nameof(agentId)); var client = HttpClientFactory.CreateClient(AbpWeChatWorkGlobalConsts.LoginClient); @@ -75,7 +80,7 @@ public class WeChatWorkAuthorizeGenerator : IWeChatWorkAuthorizeGenerator, ISing .Append(client.BaseAddress.AbsoluteUri.EnsureEndsWith('/')) .Append("wwlogin/sso/login") .AppendFormat("?login_type={0}", loginType) - .AppendFormat("&appid={0}", appid) + .AppendFormat("&appid={0}", corpId) .AppendFormat("&agentid={0}", agentId) .AppendFormat("&redirect_uri={0}", HttpUtility.UrlEncode(redirectUri)) .AppendFormat("&state={0}", state) diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/IJsApiTicketProvider.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/IJsApiTicketProvider.cs new file mode 100644 index 000000000..59dde2512 --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/IJsApiTicketProvider.cs @@ -0,0 +1,32 @@ +using LINGYUN.Abp.WeChat.Work.JsSdk.Models; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.WeChat.Work.JsSdk; +/// +/// JS-SDK临时票据提供者 +/// See: https://developer.work.weixin.qq.com/document/path/90506 +/// +public interface IJsApiTicketProvider +{ + /// + /// 获取企业 jsapi_ticket + /// + /// + /// + Task GetTicketInfoAsync(CancellationToken cancellationToken = default); + /// + /// 获取应用 jsapi_ticket + /// + /// + /// + Task GetAgentTicketInfoAsync(CancellationToken cancellationToken = default); + /// + /// 获取JS-SDK签名 + /// + /// JS-SDK临时票据 + /// 生成签名的url + /// + /// + JsApiSignatureData GenerateSignature(JsApiTicketInfo ticketInfo, string url, CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/JsApiTicketHelper.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/JsApiTicketHelper.cs new file mode 100644 index 000000000..f600da4fc --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/JsApiTicketHelper.cs @@ -0,0 +1,65 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace LINGYUN.Abp.WeChat.Work.JsSdk; +public static class JsApiTicketHelper +{ + private static string[] _randomChars = new string[] + { + "a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z", + "A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z" + }; + + public static string GenerateNonce() + { + var r = new Random(); + var sb = new StringBuilder(); + var length = _randomChars.Length; + for (var i = 0; i < 15; i++) + { + sb.Append(_randomChars[r.Next(length - 1)]); + } + return sb.ToString(); + } + + public static long GenerateTimestamp() + { + return (DateTime.Now.ToUniversalTime().Ticks - 621355968000000000) / 10000000; + } + + private static string ToSha1(string str) + { + using (var sha = SHA1.Create()) + { + var data = sha.ComputeHash(Encoding.UTF8.GetBytes(str)); + + var sb = new StringBuilder(); + foreach (var d in data) + { + sb.Append(d.ToString("x2")); + } + return sb.ToString(); + } + } + /// + /// 生成JS-SDK签名 + /// See: https://developer.work.weixin.qq.com/document/path/90506 + /// + /// + /// + /// + public static string GenerateSignature( + string jsapiTicket, + string nonce, + string timestamp, + string url) + { + var sb = new StringBuilder(); + sb.Append("jsapi_ticket=").Append(jsapiTicket).Append("&") + .Append("noncestr=").Append(nonce).Append("&") + .Append("timestamp=").Append(timestamp).Append("&") + .Append("url=").Append(url.IndexOf("#") >= 0 ? url.Substring(0, url.IndexOf("#")) : url); + return ToSha1(sb.ToString()); + } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/JsApiTicketProvider.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/JsApiTicketProvider.cs new file mode 100644 index 000000000..934e125e7 --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/JsApiTicketProvider.cs @@ -0,0 +1,105 @@ +using LINGYUN.Abp.WeChat.Work.JsSdk.Models; +using LINGYUN.Abp.WeChat.Work.Token; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.WeChat.Work.JsSdk; +public class JsApiTicketProvider : IJsApiTicketProvider, ISingletonDependency +{ + public ILogger Logger { get; set; } + + protected IHttpClientFactory HttpClientFactory { get; } + protected IDistributedCache Cache { get; } + protected IWeChatWorkTokenProvider WeChatWorkTokenProvider { get; } + + public JsApiTicketProvider( + IHttpClientFactory httpClientFactory, + IWeChatWorkTokenProvider weChatWorkTokenProvider, + IDistributedCache cache) + { + WeChatWorkTokenProvider = weChatWorkTokenProvider; + HttpClientFactory = httpClientFactory; + Cache = cache; + + Logger = NullLogger.Instance; + } + + public async virtual Task GetAgentTicketInfoAsync(CancellationToken cancellationToken = default) + { + var cacheKey = nameof(GetAgentTicketInfoAsync); + var token = await WeChatWorkTokenProvider.GetTokenAsync(cancellationToken); + var cackeItem = await GetCacheItemAsync( + cacheKey, + $"/cgi-bin/ticket/get?access_token={token.AccessToken}&type=agent_config", + cancellationToken); + + return new JsApiTicketInfo(cackeItem.Ticket, cackeItem.ExpiresIn); + } + + public async virtual Task GetTicketInfoAsync(CancellationToken cancellationToken = default) + { + var cacheKey = nameof(GetTicketInfoAsync); + var token = await WeChatWorkTokenProvider.GetTokenAsync(cancellationToken); + var cackeItem = await GetCacheItemAsync( + cacheKey, + $"/cgi-bin/get_jsapi_ticket?access_token={token.AccessToken}", + cancellationToken); + + return new JsApiTicketInfo(cackeItem.Ticket, cackeItem.ExpiresIn); + } + + public virtual JsApiSignatureData GenerateSignature(JsApiTicketInfo ticketInfo, string url, CancellationToken cancellationToken = default) + { + var nonce = JsApiTicketHelper.GenerateNonce(); + var timestamp = JsApiTicketHelper.GenerateTimestamp().ToString(); + var signature = JsApiTicketHelper.GenerateSignature(ticketInfo.Ticket, nonce, timestamp, url); + + return new JsApiSignatureData(nonce, timestamp, signature); + } + + protected async virtual Task GetCacheItemAsync( + string cacheKey, + string jsapiTicketUrl, + CancellationToken cancellationToken = default) + { + var cacheItem = await Cache.GetAsync(cacheKey, token: cancellationToken); + + if (cacheItem != null) + { + Logger.LogDebug($"Found JsApiTicket in the cache: {cacheKey}"); + return cacheItem; + } + + Logger.LogDebug($"Not found JsApiTicket in the cache, getting from the httpClient: {cacheKey}"); + + var client = HttpClientFactory.CreateClient(AbpWeChatWorkGlobalConsts.ApiClient); + + using var response = await client.GetAsync( + jsapiTicketUrl, + cancellationToken); + var ticketInfoResponse = await response.DeserializeObjectAsync(); + var ticketInfo = ticketInfoResponse.ToJsApiTicket(); + cacheItem = new JsApiTicketInfoCacheItem(ticketInfo.Ticket, ticketInfo.ExpiresIn); + + Logger.LogDebug($"Setting the cache item: {cacheKey}"); + + var cacheOptions = new DistributedCacheEntryOptions + { + // 设置绝对过期时间为Token有效期剩余的二分钟 + AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(ticketInfo.ExpiresIn - 100), + }; + + await Cache.SetAsync(cacheKey, cacheItem, cacheOptions, token: cancellationToken); + + Logger.LogDebug($"Finished setting the cache item: {cacheKey}"); + + return cacheItem; + } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiSignatureData.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiSignatureData.cs new file mode 100644 index 000000000..8d7c054c7 --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiSignatureData.cs @@ -0,0 +1,13 @@ +namespace LINGYUN.Abp.WeChat.Work.JsSdk.Models; +public class JsApiSignatureData +{ + public string Nonce { get; } + public string Timestamp { get; } + public string Signature { get; } + public JsApiSignatureData(string nonce, string timestamp, string signature) + { + Nonce = nonce; + Timestamp = timestamp; + Signature = signature; + } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfo.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfo.cs new file mode 100644 index 000000000..3b432736a --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfo.cs @@ -0,0 +1,22 @@ +namespace LINGYUN.Abp.WeChat.Work.JsSdk.Models; +public class JsApiTicketInfo +{ + /// + /// 生成签名所需的 jsapi_ticket,最长为512字节 + /// + public string Ticket { get; set; } + /// + /// 凭证的有效时间(秒) + /// + public int ExpiresIn { get; set; } + public JsApiTicketInfo() + { + + } + + public JsApiTicketInfo(string ticket, int expiresIn) + { + Ticket = ticket; + ExpiresIn = expiresIn; + } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfoCacheItem.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfoCacheItem.cs new file mode 100644 index 000000000..eb48d8133 --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfoCacheItem.cs @@ -0,0 +1,19 @@ +namespace LINGYUN.Abp.WeChat.Work.JsSdk.Models; + +public class JsApiTicketInfoCacheItem +{ + public string Ticket { get; set; } + + public int ExpiresIn { get; set; } + + public JsApiTicketInfoCacheItem() + { + + } + + public JsApiTicketInfoCacheItem(string ticket, int expiresIn) + { + Ticket = ticket; + ExpiresIn = expiresIn; + } +} diff --git a/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfoResponse.cs b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfoResponse.cs new file mode 100644 index 000000000..e5c77dac3 --- /dev/null +++ b/aspnet-core/framework/wechat/LINGYUN.Abp.WeChat.Work/LINGYUN/Abp/WeChat/Work/JsSdk/Models/JsApiTicketInfoResponse.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; +using System.Text.Json.Serialization; + +namespace LINGYUN.Abp.WeChat.Work.JsSdk.Models; + +public class JsApiTicketInfoResponse : WeChatWorkResponse +{ + /// + /// 生成签名所需的 jsapi_ticket,最长为512字节 + /// + [JsonProperty("ticket")] + [JsonPropertyName("ticket")] + public string Ticket { get; set; } + /// + /// 凭证的有效时间(秒) + /// + [JsonProperty("expires_in")] + [JsonPropertyName("expires_in")] + [System.Text.Json.Serialization.JsonConverter(typeof(NumberToStringConverter))] + public int ExpiresIn { get; set; } + + public JsApiTicketInfo ToJsApiTicket() + { + ThrowIfNotSuccess(); + return new JsApiTicketInfo(Ticket, ExpiresIn); + } +}