为了不再裸奔API,我们进行了统一的签名封装,当然这是小项目使用没问题,如果我们有很多微服务,那么建议在网关层统一进行签名验证。
解决以下问题:
- 防止非法请求(请求来源合法性验证)
- 防止重复请求(请求唯一性,利用随机码+时间戳防止重放攻击)
- 防止请求参数被篡改
这里提供两种方式进行签名,方便公共使用。
当然如果自己有其他的业务需求可以自行拿取源码修改
具体原由没必要再去将,我们直接实现
源码:Web Api Sign
签名算法规则
1、拼接appid,timestamp,noncestr字符串,并按照字符进行ASCII排序,得到字符串:appid={appid}×tamp={10位时间戳}&noncestr={16位随机字符串}
2、将appSecret追加到拼接串后面,得到字符串:appid={appid}×tamp={10位时间戳}&noncestr={16位随机字符串}&key={appSecret}
3、将其字符串MD5 32位 加密,然后全部小写 ToLower
4、然后进行 Sha256(md5+appSecret).ToLower() 加密后,得到签名
5、公式:Sha256(md5(appid={appid}×tamp={10位时间戳}&noncestr={16位随机字符串}&key={appSecret})+{appSecret})
方法签名
适用于针对性的个别API进行签名验证
使用样例:
public class HttpSignApiAttribute : HttpSignAttribute
{
/// <summary>
/// 获取密钥
/// </summary>
/// <param name="serviceProvider"></param>
/// <param name="appid"></param>
/// <returns></returns>
public override Task<string> GetAppSecretAsync(IServiceProvider serviceProvider, string appid)
{
appid = "web1ed21e4udroo37fmj";
return Task.FromResult("CdzL5v9s6cmYOqeYW2ZicfdTaT3LdXhJ");
}
/// <summary>
/// 重放处理
/// </summary>
/// <param name="serviceProvider"></param>
/// <param name="appid"></param>
/// <param name="timestamp"></param>
/// <param name="noncestr"></param>
/// <returns></returns>
public override Task<bool> PreventReplayAttackAsync(IServiceProvider serviceProvider, string appid, string timestamp, string noncestr)
{
return base.PreventReplayAttackAsync(serviceProvider, appid, timestamp, noncestr);
}
}
在需要验证的API上设置头部标签
[HttpSignApi]
[HttpGet]
[AllowAnonymous]
public Result<User> Get()
{
return new Result<User>(0, "成功", new User { Id = 1, Name = "test", CreateTime = DateTime.Now });
}
中间件统一实现
具体实现代码是一致的
/// <summary>
/// http签名中间件
/// <para>签名算法:</para>
/// <para>1、拼接appid,timestamp,noncestr字符串,并按照字符进行ASCII排序,得到字符串:appid={appid}&timestamp={10位时间戳}&noncestr={16位随机字符串}</para>
/// <para>2、将appSecret追加到拼接串后面,得到字符串:appid={appid}&timestamp={10位时间戳}&noncestr={16位随机字符串}&key={appSecret}</para>
/// <para>3、将期字符串MD5 32位 加密,然后全部小写 ToLower</para>
/// <para>4、然后进行 Sha256(md5+appSecret).ToLower() 加密后,得到签名</para>
/// <para>公式:Sha256(md5(appid={appid}&timestamp={10位时间戳}&noncestr={16位随机字符串}&key={appSecret})+{appSecret})</para>
/// </summary>
public abstract class HttpSignMiddleware : Middlewares.IMiddleware
{
/// <summary>
/// 方法
/// </summary>
private readonly RequestDelegate _next;
/// <summary>
/// 签名选项
/// </summary>
private readonly HttpSignOptions _options;
/// <summary>
/// 初始化一个<see cref="HttpSignMiddleware"/>类型的实例
/// <para>签名算法:</para>
/// <para>1、拼接appid,timestamp,noncestr字符串,并按照字符进行ASCII排序,得到字符串:appid={appid}&timestamp={10位时间戳}&noncestr={16位随机字符串}</para>
/// <para>2、将appSecret追加到拼接串后面,得到字符串:appid={appid}&timestamp={10位时间戳}&noncestr={16位随机字符串}&key={appSecret}</para>
/// <para>3、将期字符串MD5 32位 加密,然后全部小写 ToLower</para>
/// <para>4、然后进行 Sha256(md5+appSecret).ToLower() 加密后,得到签名</para>
/// <para>公式:Sha256(md5(appid={appid}&timestamp={10位时间戳}&noncestr={16位随机字符串}&key={appSecret})+{appSecret})</para>
/// </summary>
/// <param name="next">方法</param>
/// <param name="options">真实IP选项</param>
public HttpSignMiddleware(RequestDelegate next, IOptions<HttpSignOptions> options)
{
_next = next;
_options = options.Value;
}
public async Task Invoke(HttpContext context)
{
var path = context.Request.Path.Value;
if (!_options.IsOpen || path.IndexOf("/swagger") > -1)
{
await _next.Invoke(context);
return;
}
var headers = context.Request.Headers;
string appid = headers[$"{_options.Prefix}appid"].SafeString();
string sign = headers[$"{_options.Prefix}sign"].SafeString();
string timestamp = headers[$"{_options.Prefix}timestamp"].SafeString();
string noncestr = headers[$"{_options.Prefix}noncestr"].SafeString();
if (appid.IsEmpty() || sign.IsEmpty() || timestamp.IsEmpty() || noncestr.IsEmpty())
{
await WriteAsync(context, HttpSignSubCode.MissingParams);
return;
}
if (timestamp.Length != 10 || noncestr.Length != 16)
{
await WriteAsync(context, HttpSignSubCode.ParamsFail);
return;
}
var nowTimestamp = DateTime.Now.ToTimeStamp(false);
if (nowTimestamp - timestamp.ToLong() > _options.TimeOut)
{
await WriteAsync(context, HttpSignSubCode.RequestTimeout);
return;
}
var appSecret = await GetAppSecretAsync(appid);
if (!(await CreateSignAsync(appid, appSecret, timestamp, noncestr)).Equals(sign))
{
await WriteAsync(context, HttpSignSubCode.SignFail);
return;
}
if (!(await PreventReplayAttackAsync(appid, timestamp, noncestr)))
{
await WriteAsync(context, HttpSignSubCode.RequestFail);
return;
}
await _next.Invoke(context);
}
/// <summary>
/// 获取appSecret密钥
/// </summary>
/// <param name="appid"></param>
/// <returns></returns>
public abstract Task<string> GetAppSecretAsync(string appid);
/// <summary>
/// 防止重放攻击
/// </summary>
/// <param name="appid"></param>
/// <param name="timestamp"></param>
/// <param name="noncestr"></param>
/// <returns></returns>
public virtual async Task<bool> PreventReplayAttackAsync(string appid, string timestamp, string noncestr) => await Task.FromResult(true);
/// <summary>
/// 创建签名
/// </summary>
/// <param name="appid"></param>
/// <param name="appSecret"></param>
/// <param name="timestamp"></param>
/// <param name="noncestr"></param>
/// <returns></returns>
public virtual async Task<string> CreateSignAsync(string appid, string appSecret, string timestamp, string noncestr)
{
await Task.CompletedTask;
return
SignParameters.Create()
.Add("appid", appid)
.Add("timestamp", timestamp)
.Add("noncestr", noncestr)
.CreateSign("key", appSecret);
}
/// <summary>
/// 输出信息
/// </summary>
/// <param name="context"></param>
/// <param name="subCode"></param>
/// <returns></returns>
private async Task WriteAsync(HttpContext context, HttpSignSubCode subCode)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.OK;
var (code, message) = HttpSignCode.Message(subCode);
await context.Response.WriteAsync(new Result<string>(StateCode.Fail, code, message, "").ToJson(), Encoding.UTF8);
}
}
使用方法:
public class SignMiddlewareDemo : HttpSignMiddleware
{
public SignMiddlewareDemo(RequestDelegate next, IOptions<HttpSignOptions> options)
: base(next, options)
{
}
/// <summary>
///
/// </summary>
/// <param name="appid"></param>
/// <returns></returns>
public override Task<string> GetAppSecretAsync(string appid)
{
appid = "web1ed21e4udroo37fmj";
return Task.FromResult("CdzL5v9s6cmYOqeYW2ZicfdTaT3LdXhJ");
}
/// <summary>
/// 重放处理
/// </summary>
/// <param name="appid"></param>
/// <param name="timestamp"></param>
/// <param name="noncestr"></param>
/// <returns></returns>
public override Task<bool> PreventReplayAttackAsync(string appid, string timestamp, string noncestr)
{
return base.PreventReplayAttackAsync(appid, timestamp, noncestr);
}
}
在Startup的Configure中注册中间件
app.UseHttpSign<SignMiddlewareDemo>();
最后,为了完美配合web api,在swagger文档里肯定是需要加上参数的
我们可以注册的时候增加 AddHttpSignDoc 在swagger上增加header参数。
services.AddMiniSwagger(swaggerGenAction: (opt) =>
{
opt.SwaggerDoc("test", new OpenApiInfo
{
Version = "v1.0.0",
Title = $"test",
Description = "test"
});
opt.AddJwtBearerDoc();
opt.AddHttpSignDoc(services);
opt.AddDescriptions(typeof(Program), "XUCore.NetCore.ApiTests.xml");
// TODO:一定要返回true!
opt.DocInclusionPredicate((docName, description) => true);
});