接⼝鉴权之sign签名校验与JWT验证
需求描述:
  项⽬⾥的⼏个Webapi接⼝需要进⾏鉴权,同接⼝可被⼩程序或⽹页调⽤,⼩程序⾥没有⽤户登录的概念,⽹页⾥有⽤户登录的概念,对于调⽤⽅来源是⼩程序的情况下进⾏放权,其他情况下需要有⾝份验证。也就是说给所有⼩程序请求进⾏放⾏,给⽹页请求进⾏jwt⾝份验证。由于我的⼩程序没有⽤户登录的功能,所以要针对⼩程序和⽹页设计出两套完全不同的鉴权⽅式。
鉴权流程设计:
  查阅相关资料,最终决定的鉴权⽅式:
⼩程序采⽤sign签名检验
⽹页采⽤⽬前⽐较流⾏的JWT的token校验
通过AOP的思想使⽤.Net的Attribute进⾏拦截请求
代码实现
  主要是服务端写⼀个Attribute,判断是⼩程序还是⽹页,然后采⽤不同的两种不同的鉴权⽅式。
Attribute代码:
public class WxAllowFilterAttribute : BaseActionFilter
{
private static readonly int _errorCode = 401;
public override void OnActionExecuting(HttpActionContext filterContext)
{
var iswx = filterContext.iswx();//判断是否是⼩程序发来的请求
if (iswx)
{
          //⼩程序的签名校验
if (!filterContext.checkwx()) {
filterContext.Response = Error("⼩程序签名验证失败", _errorCode);
};
}
else
{
          //JWT的token校验
string token = filterContext.GetToken();
if (string.IsNullOrEmpty(token))
{
filterContext.Response = Error("缺少token", _errorCode);
return;
}
if (!JWTHelper.CheckToken(token, JWTHelper.JWTSecret))
{
filterContext.Response = Error("token校验失败!", _errorCode);
return;
}
var payload = JWTHelper.GetPayload<JWTPayload>(token);
if (payload.Expire < DateTime.Now)
{
filterContext.Response = Error("token过期!", _errorCode);
return;
}
base.OnActionExecuting(filterContext);
}
}
}
扩展类
public static class HttpRequest
{
public static readonly string wx_secret = ConfigurationManager.AppSettings["wx_secret"];
///<summary>
///获取Token
///</summary>
/
//<param name="req">请求</param>
///<returns></returns>
public static string GetToken(this HttpActionContext req)
{
string tokenHeader = req.Request.Headers.Authorization == null ? "" : req.Request.Headers.Authorization.Parameter;
if (string.IsNullOrEmpty(tokenHeader))
return null;
string pattern = "^Bearer (.*?)$";
if (!Regex.IsMatch(tokenHeader, pattern))
throw new Exception("token格式不对!格式为:Bearer {token}");
string token = Regex.Match(tokenHeader, pattern).Groups[1].ToString();
if (string.IsNullOrEmpty(token))
throw new Exception("token不能为空!");
return token;
}
///<summary>
///判断是否
///</summary>
///<param name="req"></param>
///<returns></returns>
public static bool iswx(this HttpActionContext req)
{
var queryList = req.Request.RequestUri.Query.Split('&').ToList<string>();
Dictionary<String, String> pList = new Dictionary<String, String>();
if (queryList.Count < 2)
{
return false;
}
else
{
queryList.ForEach(x =>
{
var a = x.Split('=');
if (a.Count() >= 2)
{
pList.Add(a[0], a[1]);
}
});
var iswx = pList.Any(x => x.Key == "app_key" && x.Value == "wx");//判断是否有标识的字段
return iswx;
}
}
///<summary>
///检验sign是否合法
/
//</summary>
///<param name="req"></param>
///<returns></returns>
public static bool checkwx(this HttpActionContext req)
{
var queryList = req.Request.RequestUri.Query.Split('&').ToList<string>();
Dictionary<String, String> pList = new Dictionary<String, String>();
queryList.ForEach(x =>
{
var a = x.Split('=');
if (a.Count() >= 2)
{
pList.Add(a[0], a[1]);
}
});
var app_key = pList["app_key"];
var app_secret = wx_secret;
var timetamp = pList["timestamp"];
var sign = pList["sign"];
if (!string.IsNullOrEmpty(timetamp)) {
var tamp=Convert.ToInt64(timetamp);
var nowtamp = ToTimestamp(DateTime.Now);
var a = nowtamp-tamp;
if (a >= 15) {
return false;
}
}
StringBuilder sb = new StringBuilder();
sb.Append(app_key);
sb.Append(app_secret);
sb.Append(timetamp);
var newsign = GetMD5(sb.ToString());
return newsign == sign;
}
public static string GetMD5(string sDataIn)
{
MD5CryptoServiceProvider provider = new MD5CryptoServiceProvider();
byte[] bytes = Encoding.UTF8.GetBytes(sDataIn);
byte[] buffer2 = provider.ComputeHash(bytes);
provider.Clear();
string str = "";
for (int i = 0; i < buffer2.Length; i++)
{
str = str + buffer2[i].ToString("X").PadLeft(2, '0');
}
return str.ToLower();
}
public static long ToTimestamp(this DateTime target)
{
return (target.ToUniversalTime().Ticks - 621355968000000000) / 10000000;
}
}
Filter基类
public class BaseActionFilter : ActionFilterAttribute
{
/
/public virtual void OnActionExecuting(HttpActionContext filterContext)签名字符串是什么
//{
//}
//public virtual void OnActionExecuted(HttpActionContext filterContext)
//{
//}
///<summary>
///返回JSON
///</summary>
///<param name="json">json字符串</param>
///<returns></returns>
public HttpResponseMessage JsonContent(string json)
{
var content = new StringContent(json, Encoding.UTF8, "application/json");
return new HttpResponseMessage { Content = content, StatusCode = HttpStatusCode.OK };
}
public HttpResponseMessage IsSuccess()
{
AjaxResult res = new AjaxResult
{
IsSuccess = true,
Msg = "请求成功!"
};
return JsonContent(JsonHelper.SerializeObject(res));
}
///<summary>
///返回成功
///</summary>
///<param name="msg">消息</param>
///<returns></returns>
public HttpResponseMessage IsSuccess(string msg)
{
AjaxResult res = new AjaxResult
{
IsSuccess = true,
Msg = msg
};
return JsonContent(JsonHelper.SerializeObject(res));
}
///<summary>
///返回成功
///</summary>
///<param name="data">返回的数据</param>
///<returns></returns>
public HttpResponseMessage IsSuccess<T>(T data)
{
AjaxResult<T> res = new AjaxResult<T>
{
IsSuccess = true,
Msg = "请求成功!",
Data = data
};
return JsonContent(JsonHelper.SerializeObject(res));
}
///<summary>
/
//返回错误
///</summary>
///<returns></returns>
public HttpResponseMessage Error()
{
AjaxResult res = new AjaxResult
{
IsSuccess = false,
Msg = "请求失败!"
};
return JsonContent(JsonHelper.SerializeObject(res));
}
///<summary>
///返回错误
///</summary>
///<param name="msg">错误提⽰</param>
///<returns></returns>
public HttpResponseMessage Error(string msg)
{
AjaxResult res = new AjaxResult
{
IsSuccess = false,
Msg = msg,
};
return JsonContent(JsonHelper.SerializeObject(res));
}
///<summary>
///返回错误
///</summary>
///<param name="msg">错误提⽰</param>
///<param name="errorCode">错误代码</param>
///<returns></returns>
public HttpResponseMessage Error(string msg, int errorCode)
{
AjaxResult res = new AjaxResult
{
IsSuccess = false,
Msg = msg,
StatusCode = errorCode
};
return JsonContent(JsonHelper.SerializeObject(res));
}
}
JWT扩展类
public class JWTHelper
{
private static readonly string _headerBase64Url = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}".Base64UrlEncode();
public static readonly string JWTSecret = ConfigurationManager.AppSettings["JWTSecret"];
///<summary>
///⽣成Token
///</summary>
///<param name="payloadJsonStr">数据JSON字符串</param>
///<param name="secret">密钥</param>
///<returns></returns>
public static string GetToken(string payloadJsonStr, string secret)
{
string payloadBase64Url = payloadJsonStr.Base64UrlEncode();
StringBuilder sb = new StringBuilder();
StringBuilder sb1 = new StringBuilder();
sb.AppendFormat("{0}", _headerBase64Url);
sb.Append(".");
sb.AppendFormat("{0}", payloadBase64Url);
sb1 = sb;
string sign = sb.ToString().ToHMACSHA256String(secret);
string token = sb1.AppendFormat(".{0}", sign).ToString();
return token;
}
///<summary>
///获取Token中的数据
///</summary>
///<typeparam name="T">泛型</typeparam>
///<param name="token">token</param>
///<returns></returns>
public static T GetPayload<T>(string token)
{
if (string.IsNullOrEmpty(token))
{
return default(T);
}
return token.Split('.')[1].Base64UrlDecode().ToObject<T>();
}
///<summary>
///校验Token
///</summary>
///<param name="token">token</param>
///<param name="secret">密钥</param>
///<returns></returns>
public static bool CheckToken(string token, string secret)
{
var items = token.Split('.');
var oldSign = items[2];
StringBuilder sb = new StringBuilder();
sb.AppendFormat("{0}", items[0]);
sb.AppendFormat(".{0}", items[1]);
string newSign = sb.ToString().ToHMACSHA256String(secret);
return oldSign == newSign;
}
}
检验⽤户名密码是否正确的业务接⼝代码这⾥不贴了..
⽹页客户端的代码还没写完,主要思路就是判断缓存⾥是否有token,没有就去把⽤户名密码去调⽤服务端的登录接⼝拿到token后存到缓存⾥,之后的所有请求都在头部带上这个token。
⼩程序客户端代码 :
在app.js中定义⼀个公共的promise请求⽅法,并带上请求的参数(app_key,时间戳,md5加密后的sign等),这⾥要注意区分get和post请求的区别,get是放在url后的,post是放在body⾥的,要对传参的格式要稍加处理
request(params) {
reqTime++;
//加载弹框
wx.showLoading({
title: '加载中...',
mask: true
});
//返回
return new Promise((resolve, reject) => {
var data = {
app_key: this.globalData.app_key,
timestamp: und(new Date() / 1000),
sign: ''
}
data.sign = utilMd5.hexMD5(`${this.globalData.app_key}${this.globalData.app_secret}${data.timestamp}`)
if (UpperCase() == 'POST') {
if (!params.url.includes('?')) {
params.url += '?'
}
var url = `&app_key=${this.globalData.app_key}×tamp=${data.timestamp}&sign=${data.sign}`
params = {
...params,
url: params.url + url
}
data = params.data
} else {
data = {
...params.data,
.
..data
}
}
params = {
...params,
data: {
...data
}
}
//解构params获取请求参数
.
..params,
success: (result) => {
resolve(result);
},
fail: (err) => {
reject(err);
},
complete: () => {
reqTime--;
//停⽌加载
if (!reqTime)
wx.hideLoading();
}
});
});
}
这边说明下,我的app_key和app_secret都是写在app.js⾥的公共变量中的,app_key在url⾥是暴露的,但是app_secret是绝不能被暴露的。光知道app_key是⽆法⽣成正确的sign的,必须app_key,app_secret和timestap三者的加密才能⽣成正确的sign。我把app_secret写在app.js中可能不是安全的做法,但是通过请求服务器去获取app.secret⼜要⾯临⽹络请求的安全问题,最多对字符串进⾏加密解密,但也不能说绝对安全了。app_secret怎么处理最安全我⽬前也没想到很好的办法。。
好了,以上就是⼩程序的鉴权⽅法,⼩程序客户端在请求时只需要调⽤这个公共⽅法就⾏。
鉴权测试结果
给控制器或者⽅法前⾯加上鉴权的特性[WxAllowFilter]
PostMan直接调⽤不带任何sign等参数
伪造⼩程序参数签名验证失败或者时间戳超过10秒

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。