feat: 增加服务号回调签名校验及扫码回调幂等处理
- `FuwuhaoManager` 新增 `ValidateCallback` 方法,用于校验微信回调签名 - `FuwuhaoOptions` 增加 `CallbackToken` 配置项 - `QrCodeResponse` 属性添加 `JsonPropertyName` 标注,支持 JSON 序列化映射 - `FuwuhaoService` 在回调接口中增加签名校验,并通过分布式锁实现幂等处理 - 调整场景值解析逻辑,过滤非扫码/关注事件 - 优化缓存过期时间设置
This commit is contained in:
@@ -4,8 +4,6 @@ namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Fuwuhao;
|
|||||||
|
|
||||||
public class SceneCacheDto
|
public class SceneCacheDto
|
||||||
{
|
{
|
||||||
public string Scene { get; set; }
|
|
||||||
|
|
||||||
public SceneResultEnum SceneResult { get; set; } = SceneResultEnum.Wait;
|
public SceneResultEnum SceneResult { get; set; } = SceneResultEnum.Wait;
|
||||||
|
|
||||||
public SceneTypeEnum SceneType { get; set; }
|
public SceneTypeEnum SceneType { get; set; }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
|
using Medallion.Threading;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
@@ -27,6 +28,7 @@ public class FuwuhaoService : ApplicationService
|
|||||||
private IDistributedCache<SceneCacheDto> _sceneCache;
|
private IDistributedCache<SceneCacheDto> _sceneCache;
|
||||||
private IAccountService _accountService;
|
private IAccountService _accountService;
|
||||||
private IFileService _fileService;
|
private IFileService _fileService;
|
||||||
|
public IDistributedLockProvider DistributedLock => LazyServiceProvider.LazyGetService<IDistributedLockProvider>();
|
||||||
|
|
||||||
public FuwuhaoService(ILogger<FuwuhaoService> logger, IHttpContextAccessor accessor, FuwuhaoManager fuwuhaoManager,
|
public FuwuhaoService(ILogger<FuwuhaoService> logger, IHttpContextAccessor accessor, FuwuhaoManager fuwuhaoManager,
|
||||||
IDistributedCache<SceneCacheDto> sceneCache, IAccountService accountService, IFileService fileService)
|
IDistributedCache<SceneCacheDto> sceneCache, IAccountService accountService, IFileService fileService)
|
||||||
@@ -56,12 +58,12 @@ public class FuwuhaoService : ApplicationService
|
|||||||
/// <param name="signature"></param>
|
/// <param name="signature"></param>
|
||||||
/// <param name="timestamp"></param>
|
/// <param name="timestamp"></param>
|
||||||
/// <param name="nonce"></param>
|
/// <param name="nonce"></param>
|
||||||
/// <param name="openId"></param>
|
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost("fuwuhao/callback")]
|
[HttpPost("fuwuhao/callback")]
|
||||||
public async Task<string> PostCallbackAsync([FromQuery] string signature, [FromQuery] string timestamp,
|
public async Task<string> PostCallbackAsync([FromQuery] string signature, [FromQuery] string timestamp,
|
||||||
[FromQuery] string nonce)
|
[FromQuery] string nonce)
|
||||||
{
|
{
|
||||||
|
_fuwuhaoManager.ValidateCallback(signature,timestamp,nonce);
|
||||||
var request = _accessor.HttpContext.Request;
|
var request = _accessor.HttpContext.Request;
|
||||||
// 1. 读取原始 XML 内容
|
// 1. 读取原始 XML 内容
|
||||||
using var reader = new StreamReader(request.Body, Encoding.UTF8);
|
using var reader = new StreamReader(request.Body, Encoding.UTF8);
|
||||||
@@ -72,21 +74,35 @@ public class FuwuhaoService : ApplicationService
|
|||||||
var body = (FuwuhaoCallModel)serializer.Deserialize(stringReader);
|
var body = (FuwuhaoCallModel)serializer.Deserialize(stringReader);
|
||||||
|
|
||||||
//获取场景值,后续通过场景值设置缓存状态,前端轮询这个场景值用户是否已操作即可
|
//获取场景值,后续通过场景值设置缓存状态,前端轮询这个场景值用户是否已操作即可
|
||||||
var scene = body.EventKey;
|
var scene = body.EventKey.Replace("qrscene_","");
|
||||||
|
if (!(body.Event is "SCAN" or "subscribe"))
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException("当前回调只处理扫码 与 关注");
|
||||||
|
}
|
||||||
if (scene is null)
|
if (scene is null)
|
||||||
{
|
{
|
||||||
throw new UserFriendlyException("服务号返回无场景值");
|
throw new UserFriendlyException("服务号返回无场景值");
|
||||||
}
|
}
|
||||||
|
|
||||||
var cache = await _sceneCache.GetAsync($"fuwuhao:{scene}");
|
//制作幂等
|
||||||
|
await using (var handle =
|
||||||
|
await DistributedLock.TryAcquireLockAsync($"Yi:fuwuhao:callbacklock:{scene}", TimeSpan.FromSeconds(60)))
|
||||||
|
{
|
||||||
|
if (handle == null)
|
||||||
|
{
|
||||||
|
return "success"; // 跳过直接返回成功
|
||||||
|
}
|
||||||
|
|
||||||
//根据操作类型,进行业务处理,返回处理结果,再写入缓存,10s过去,相当于用户10s扫完app后,轮询要在10秒内完成
|
var cache = await _sceneCache.GetAsync($"fuwuhao:{scene}");
|
||||||
var scenResult = await _fuwuhaoManager.CallBackHandlerAsync(cache.SceneType, body.FromUserName, cache.UserId);
|
|
||||||
cache.SceneResult = scenResult;
|
|
||||||
|
|
||||||
await _sceneCache.SetAsync($"fuwuhao:{scene}", cache,
|
//根据操作类型,进行业务处理,返回处理结果,再写入缓存,10s过去,相当于用户10s扫完app后,轮询要在10秒内完成
|
||||||
new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10) });
|
var scenResult =
|
||||||
|
await _fuwuhaoManager.CallBackHandlerAsync(cache.SceneType, body.FromUserName, cache.UserId);
|
||||||
|
cache.SceneResult = scenResult;
|
||||||
|
|
||||||
|
await _sceneCache.SetAsync($"fuwuhao:{scene}", cache,
|
||||||
|
new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(50) });
|
||||||
|
}
|
||||||
|
|
||||||
return "success";
|
return "success";
|
||||||
}
|
}
|
||||||
@@ -154,16 +170,16 @@ public class FuwuhaoService : ApplicationService
|
|||||||
//根据code获取到openid、微信用户昵称、头像
|
//根据code获取到openid、微信用户昵称、头像
|
||||||
var userInfo = await _fuwuhaoManager.GetUserInfoByCodeAsync(code);
|
var userInfo = await _fuwuhaoManager.GetUserInfoByCodeAsync(code);
|
||||||
var files = new FormFileCollection();
|
var files = new FormFileCollection();
|
||||||
|
|
||||||
// 下载头像并添加到系统文件中
|
// 下载头像并添加到系统文件中
|
||||||
if (!string.IsNullOrEmpty(userInfo.HeadImgUrl))
|
if (!string.IsNullOrEmpty(userInfo.HeadImgUrl))
|
||||||
{
|
{
|
||||||
using var httpClient = new HttpClient();
|
using var httpClient = new HttpClient();
|
||||||
var imageBytes = await httpClient.GetByteArrayAsync(userInfo.HeadImgUrl);
|
var imageBytes = await httpClient.GetByteArrayAsync(userInfo.HeadImgUrl);
|
||||||
var imageStream = new MemoryStream(imageBytes);
|
var imageStream = new MemoryStream(imageBytes);
|
||||||
|
|
||||||
// 从URL中提取文件扩展名,默认为png
|
// 从URL中提取文件扩展名,默认为png
|
||||||
var fileName = $"avatar_{userInfo.OpenId}.png";
|
var fileName = $"avatar_{userInfo.OpenId}.png";
|
||||||
var formFile = new FormFile(imageStream, 0, imageBytes.Length, "avatar", fileName)
|
var formFile = new FormFile(imageStream, 0, imageBytes.Length, "avatar", fileName)
|
||||||
{
|
{
|
||||||
Headers = new HeaderDictionary(),
|
Headers = new HeaderDictionary(),
|
||||||
@@ -171,6 +187,7 @@ public class FuwuhaoService : ApplicationService
|
|||||||
};
|
};
|
||||||
files.Add(formFile);
|
files.Add(formFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await _fileService.Post(files);
|
var result = await _fileService.Post(files);
|
||||||
|
|
||||||
var userId = await _accountService.PostSystemRegisterAsync(new RegisterDto
|
var userId = await _accountService.PostSystemRegisterAsync(new RegisterDto
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text;
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
@@ -18,11 +19,12 @@ public class FuwuhaoManager : DomainService
|
|||||||
private ISqlSugarRepository<AiUserExtraInfoEntity> _userRepository;
|
private ISqlSugarRepository<AiUserExtraInfoEntity> _userRepository;
|
||||||
|
|
||||||
public FuwuhaoManager(IOptions<FuwuhaoOptions> options, IHttpClientFactory httpClientFactory,
|
public FuwuhaoManager(IOptions<FuwuhaoOptions> options, IHttpClientFactory httpClientFactory,
|
||||||
ISqlSugarRepository<AiUserExtraInfoEntity> userRepository)
|
ISqlSugarRepository<AiUserExtraInfoEntity> userRepository, IDistributedCache<AccessTokenResponse> accessTokenCache)
|
||||||
{
|
{
|
||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
|
_accessTokenCache = accessTokenCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -66,11 +68,11 @@ public class FuwuhaoManager : DomainService
|
|||||||
var requestBody = new
|
var requestBody = new
|
||||||
{
|
{
|
||||||
action_name = "QR_STR_SCENE",
|
action_name = "QR_STR_SCENE",
|
||||||
|
expire_seconds = 600,
|
||||||
action_info = new
|
action_info = new
|
||||||
{
|
{
|
||||||
scene = new
|
scene = new
|
||||||
{
|
{
|
||||||
expire_seconds = 600,
|
|
||||||
scene_str = scene
|
scene_str = scene
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,6 +132,40 @@ public class FuwuhaoManager : DomainService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验微信服务器回调参数是否正确
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="signature">微信加密签名</param>
|
||||||
|
/// <param name="timestamp">时间戳</param>
|
||||||
|
/// <param name="nonce">随机数</param>
|
||||||
|
/// <returns>true表示验证通过,false表示验证失败</returns>
|
||||||
|
public void ValidateCallback(string signature, string timestamp, string nonce)
|
||||||
|
{
|
||||||
|
var token = _options.CallbackToken; // 您设置的token
|
||||||
|
|
||||||
|
// 将token、timestamp、nonce三个参数进行字典序排序
|
||||||
|
var parameters = new[] { token, timestamp, nonce };
|
||||||
|
Array.Sort(parameters);
|
||||||
|
|
||||||
|
// 将三个参数字符串拼接成一个字符串
|
||||||
|
var concatenated = string.Join("", parameters);
|
||||||
|
|
||||||
|
// 进行SHA1计算签名
|
||||||
|
using (var sha1 = SHA1.Create())
|
||||||
|
{
|
||||||
|
var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(concatenated));
|
||||||
|
var calculatedSignature = BitConverter.ToString(hash).Replace("-", "").ToLower();
|
||||||
|
|
||||||
|
// 与URL链接中的signature参数进行对比
|
||||||
|
var result = calculatedSignature.Equals(signature, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException("服务号回调签名异常");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 处理回调逻辑
|
/// 处理回调逻辑
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -11,4 +11,9 @@ public class FuwuhaoOptions
|
|||||||
/// 微信公众号AppSecret
|
/// 微信公众号AppSecret
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Secret { get; set; }
|
public string Secret { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 回调token
|
||||||
|
/// </summary>
|
||||||
|
public string CallbackToken { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
|
namespace Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -8,15 +10,18 @@ public class QrCodeResponse
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 二维码票据
|
/// 二维码票据
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonPropertyName("ticket")]
|
||||||
public string Ticket { get; set; }
|
public string Ticket { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 过期时间(秒)
|
/// 过期时间(秒)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonPropertyName("expire_seconds")]
|
||||||
public int ExpireSeconds { get; set; }
|
public int ExpireSeconds { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 二维码URL
|
/// 二维码URL
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonPropertyName("url")]
|
||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user