Api接口签名验证
通过特性来统一验证的入口,实现ActionFilterAttribute接口来进行接口的签名验证
1 /// <summary> 2 /// 标准接口基类Controller 3 /// </summary> 4 [SignVerification] 5 public abstract class BaseApiController : Controller 6 { 7 } 8 9 /// <summary> 10 /// 接口签名验证 11 /// </summary> 12 public class SignVerificationAttribute : ActionFilterAttribute,IAuthenticationFilter 13 { 14 }
实现的思路为:
1.不同对接方的接口(插件)定义不同的验证key,不同的插件间不能混用验证key
2.不同的插件生成不同的partnerId,partnerKey。请求的Url中需要携带partnerId,通过partnerId作为key在redis中找到对应的插件验证信息(包括:partnerId,partnerKey等)
3.Url参数中必须包含partnerId,ts(时间戳),sign(加密签名)。ts时间戳的有效时间为5分钟,sign为(时间戳:formBody:partnerId:partnerKey)的MD5加密
4.如果通过partnerId可以找到对应的验证信息,再把(时间戳:formBody:partnerId:partnerKey)MD5加密后和sign比较确保请求没有被篡改
5.确保partnerId为当前插件而非其他插件的,因为redis是共用的,只是通过key去取值而已
签名方式
将时间戳和请求Form参数以及PartnerKey以冒号连接,如(时间戳:body:partnerId:PartnerKey)
将连接好的字符串进行MD5生成sign
Url参数
参数 | 说明 | 类型 | 必须 | 备注 |
---|---|---|---|---|
pid | partnerId | string | 是 | |
ts | 时间戳(格式:yyyyMMddHHmmss) | string | 是 | 时间戳的有效时间为5分钟 |
sign | MD5(时间戳:body:partnerId:pkey) | string | 是 | 参考签名方式 |
具体代码实现
1 /// <summary> 2 /// 接口签名验证 3 /// </summary> 4 public class SignVerificationAttribute : ActionFilterAttribute, IAuthenticationFilter 5 { 6 private readonly IDefaultUserService _defaultUserService; 7 private readonly IInterfaceSignProvider _interfaceSignProvider; 8 public SignVerificationAttribute() 9 { 10 _defaultUserService = ObjectContainer.GetService<IDefaultUserService>(); 11 _interfaceSignProvider = ObjectContainer.GetService<IInterfaceSignProvider>(); 12 } 13 14 public void OnAuthentication(AuthenticationContext filterContext) 15 { 16 var request = filterContext.HttpContext.Request; 17 var partnerId = request.QueryString["pid"]; 18 var timeStamp = request.QueryString["ts"]; 19 var sign = request.QueryString["sign"];//获取Url参数 20 var body = GetBodyText(request.InputStream); 21 22 if (!ValidSign(filterContext,timeStamp, sign, body,partnerId,out IInterfaceSignInfo signInfo))//加密验证 23 { 24 filterContext.Result = new ApiResult {Success = false, ErrorMessage = "无效签名"}; 25 return; 26 } 27 28 var service = ObjectContainer.GetService<IAuthenticationService>(); 29 var userId = _defaultUserService.GetDefaultUserId(signInfo.LicNo); 30 var identity = service.SignIn(userId, signInfo.LicNo, false, TimeSpan.FromMinutes(5), SessionType.WebApi); 31 var newPrincipal = new GenericPrincipal(identity, new string[] { }); 32 filterContext.Principal = newPrincipal; 33 } 34 private static string GetBodyText(Stream stream) 35 { 36 using (var ms = new MemoryStream()) 37 { 38 stream.CopyTo(ms); 39 return Encoding.UTF8.GetString(ms.ToArray()); 40 } 41 } 42 43 private bool ValidSign(AuthenticationContext filterContext,string timeStamp, string sign, string body,string partnerId,out IInterfaceSignInfo signInfo) 44 { 45 signInfo = null; 46 if (!string.IsNullOrEmpty(timeStamp) && !string.IsNullOrEmpty(sign)&& !string.IsNullOrEmpty(partnerId)) 47 { 48 var cache = _interfaceSignProvider.GetInterfaceSignInfo(partnerId);//通过partnerId当key读取redis 49 if (cache.Enabled) 50 { 51 var areaName = filterContext.RouteData.DataTokens["area"]?.ToString().ToLower();//获取请求的area,即请求的是哪个插件 52 if (string.IsNullOrEmpty(areaName) || !cache.PluginCode.ToLower().StartsWith(areaName)) 53 { 54 return false;//PluginCode需以areaName开头,否则意味着不是同一个插件(如:PluginCode=juwov1,areaName=JuWo) 55 } 56 if (DateTime.TryParseExact(timeStamp, "yyyyMMddHHmmss", CultureInfo.CurrentCulture.DateTimeFormat, DateTimeStyles.AllowWhiteSpaces, out var time) && 57 (DateTime.Now - time).TotalMinutes <= 5)//时间戳有效期为5分钟 58 { 59 signInfo = cache; 60 var hashKey = EncryptHelper.Hash($"{timeStamp}:{body}:{partnerId}:{cache.PartnerKey}", "MD5").ToLowerInvariant();//MD5加密对比 61 return string.Equals(hashKey, sign); 62 } 63 } 64 65 } 66 return false; 67 } 68 public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext){} 69 }
这样就实现了接口的签名验证了。但是还有一个问题是,如果同时存在多个不同的对接接口(插件)时,partnerId,PartnerKey应该是不一样的。即插件1和插件2的验证key是不能混用的。
可以通过路由来区分不同的插件,来选择进入不同的area,通过area来区分不同的插件验证key。
1 public class JuWoAreaRegistration: AreaRegistration 2 { 3 public override void RegisterArea(AreaRegistrationContext context) 4 { 5 context.MapRoute( 6 "JuWo_default", 7 "api/JuWo/{controller}/{action}/{id}", 8 new {action = "Index", id = UrlParameter.Optional}, 9 new[] {"iERP.Its.Web.Areas.JuWo.Controllers"} 10 ); 11 } 12 13 public override string AreaName => "JuWo"; 14 }
在之前的ValidSign方法中,通过var areaName = filterContext.RouteData.DataTokens[“area”]?.ToString().ToLower();来获取到当前请求的是哪个插件,在把url上获取到的partnerId与我们之前约定好的比较看是否能对应。