接口鉴权之sign签名校验与JWT验证
优质文章,第一时间送达
作者 | 那一片蓝海
来源 | urlify.cn/FJjmqa
需求描述:
项目里的几个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: Math.round(new Date() / 1000),
sign: ''
}
data.sign = utilMd5.hexMD5(`${this.globalData.app_key}${this.globalData.app_secret}${data.timestamp}`)
if (params.method.toUpperCase() == '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
}
}
wx.request({
//解构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秒

小程序内调用

锋哥最新SpringCloud分布式电商秒杀课程发布
👇👇👇