committed by
GitHub
23 changed files with 535 additions and 52 deletions
@ -0,0 +1,6 @@ |
|||
namespace LINGYUN.Abp.WeChat.Work.JsSdk.Dtos; |
|||
public class AgentConfigDto |
|||
{ |
|||
public string AgentId { get; set; } |
|||
public string CorpId { get; set; } |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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<AgentConfigDto> GetAgentConfigAsync(); |
|||
|
|||
Task<JsApiSignatureDto> GetSignatureAsync(string url); |
|||
|
|||
Task<JsApiSignatureDto> GetAgentSignatureAsync(string url); |
|||
} |
|||
@ -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<string> GenerateOAuth2AuthorizeAsync(string redirectUri, string responseType = "code", string scope = "snsapi_base") |
|||
public async virtual Task<string> 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<string> GenerateOAuth2LoginAsync(string redirectUri, string loginType = "ServiceApp") |
|||
public async virtual Task<string> 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); |
|||
} |
|||
} |
|||
|
|||
@ -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<AgentConfigDto> 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<JsApiSignatureDto> 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<JsApiSignatureDto> 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); |
|||
} |
|||
} |
|||
@ -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<AgentConfigDto> GetAgentConfigAsync() |
|||
{ |
|||
return _service.GetAgentConfigAsync(); |
|||
} |
|||
|
|||
[HttpGet] |
|||
[Route("agent-signature")] |
|||
public virtual Task<JsApiSignatureDto> GetAgentSignatureAsync(string url) |
|||
{ |
|||
return _service.GetAgentSignatureAsync(url); |
|||
} |
|||
|
|||
[HttpGet] |
|||
[Route("signature")] |
|||
public virtual Task<JsApiSignatureDto> GetSignatureAsync(string url) |
|||
{ |
|||
return _service.GetSignatureAsync(url); |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
using LINGYUN.Abp.WeChat.Work.JsSdk.Models; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Work.JsSdk; |
|||
/// <summary>
|
|||
/// JS-SDK临时票据提供者
|
|||
/// See: https://developer.work.weixin.qq.com/document/path/90506
|
|||
/// </summary>
|
|||
public interface IJsApiTicketProvider |
|||
{ |
|||
/// <summary>
|
|||
/// 获取企业 jsapi_ticket
|
|||
/// </summary>
|
|||
/// <param name="cancellationToken"></param>
|
|||
/// <returns></returns>
|
|||
Task<JsApiTicketInfo> GetTicketInfoAsync(CancellationToken cancellationToken = default); |
|||
/// <summary>
|
|||
/// 获取应用 jsapi_ticket
|
|||
/// </summary>
|
|||
/// <param name="cancellationToken"></param>
|
|||
/// <returns></returns>
|
|||
Task<JsApiTicketInfo> GetAgentTicketInfoAsync(CancellationToken cancellationToken = default); |
|||
/// <summary>
|
|||
/// 获取JS-SDK签名
|
|||
/// </summary>
|
|||
/// <param name="ticketInfo">JS-SDK临时票据</param>
|
|||
/// <param name="url">生成签名的url</param>
|
|||
/// <param name="cancellationToken"></param>
|
|||
/// <returns></returns>
|
|||
JsApiSignatureData GenerateSignature(JsApiTicketInfo ticketInfo, string url, CancellationToken cancellationToken = default); |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
/// <summary>
|
|||
/// 生成JS-SDK签名
|
|||
/// See: https://developer.work.weixin.qq.com/document/path/90506
|
|||
/// </summary>
|
|||
/// <param name="jsapiTicket"></param>
|
|||
/// <param name="url"></param>
|
|||
/// <returns></returns>
|
|||
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()); |
|||
} |
|||
} |
|||
@ -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<JsApiTicketProvider> Logger { get; set; } |
|||
|
|||
protected IHttpClientFactory HttpClientFactory { get; } |
|||
protected IDistributedCache<JsApiTicketInfoCacheItem> Cache { get; } |
|||
protected IWeChatWorkTokenProvider WeChatWorkTokenProvider { get; } |
|||
|
|||
public JsApiTicketProvider( |
|||
IHttpClientFactory httpClientFactory, |
|||
IWeChatWorkTokenProvider weChatWorkTokenProvider, |
|||
IDistributedCache<JsApiTicketInfoCacheItem> cache) |
|||
{ |
|||
WeChatWorkTokenProvider = weChatWorkTokenProvider; |
|||
HttpClientFactory = httpClientFactory; |
|||
Cache = cache; |
|||
|
|||
Logger = NullLogger<JsApiTicketProvider>.Instance; |
|||
} |
|||
|
|||
public async virtual Task<JsApiTicketInfo> 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<JsApiTicketInfo> 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<JsApiTicketInfoCacheItem> 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<JsApiTicketInfoResponse>(); |
|||
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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
namespace LINGYUN.Abp.WeChat.Work.JsSdk.Models; |
|||
public class JsApiTicketInfo |
|||
{ |
|||
/// <summary>
|
|||
/// 生成签名所需的 jsapi_ticket,最长为512字节
|
|||
/// </summary>
|
|||
public string Ticket { get; set; } |
|||
/// <summary>
|
|||
/// 凭证的有效时间(秒)
|
|||
/// </summary>
|
|||
public int ExpiresIn { get; set; } |
|||
public JsApiTicketInfo() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public JsApiTicketInfo(string ticket, int expiresIn) |
|||
{ |
|||
Ticket = ticket; |
|||
ExpiresIn = expiresIn; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
using Newtonsoft.Json; |
|||
using System.Text.Json.Serialization; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Work.JsSdk.Models; |
|||
|
|||
public class JsApiTicketInfoResponse : WeChatWorkResponse |
|||
{ |
|||
/// <summary>
|
|||
/// 生成签名所需的 jsapi_ticket,最长为512字节
|
|||
/// </summary>
|
|||
[JsonProperty("ticket")] |
|||
[JsonPropertyName("ticket")] |
|||
public string Ticket { get; set; } |
|||
/// <summary>
|
|||
/// 凭证的有效时间(秒)
|
|||
/// </summary>
|
|||
[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); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue