需求描述:

  项目里的几个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}&timestamp=${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秒

  • 小程序内调用

接口鉴权之sign签名校验与JWT验证的更多相关文章

  1. EasyNVR摄像机网页H5全平台无插件直播流媒体播放服务二次开发之接口鉴权示例讲解

    背景需求 EasyNVR的使用者应该都清楚的了解到,EasyNVR一个强大的功能就是可以进行全平台的无插件直播.主要原因在于rtsp协议的视频流(默认是需要插件才可以播放的)经由EasyNVR处理可以 ...

  2. laravel JWTAuth实现api接口鉴权(基础篇)

    官网:https://jwt-auth.readthedocs.io 参考:https://learnku.com/articles/10885/full-use-of-jwt#99529f 1.to ...

  3. 接口鉴权,提供给第三方调用的接口,进行sign签名

    //场景:公司要跟第三方公司合作,提供接口给对方对接,这样需要对接口进行授权,不然任何人都可以调我们公司的接口,会导致安全隐患: 思路: 在每个接口请求参数都带上ApiKey 和sign签名: 我们在 ...

  4. 项目API接口鉴权流程总结

    权益需求对接中,公司跟第三方公司合作,有时我们可能作为甲方,提供接口给对方,有时我们也作为乙方,调对方接口,这就需要API使用签名方法(Sign)对接口进行鉴权.每一次请求都需要在请求中包含签名信息, ...

  5. HiMall 3接口鉴权参考

    签名算法 为了防止API调用过程中被黑客恶意篡改,调用任何一个API都需要携带签名,HOP服务端会根据请求参数,对签名进行验证,签名不合法的请求将会被拒绝.TOP目前支持的签名算法只有一种:MD5(s ...

  6. python笔记44-HTTP对外接口sign签名

    前言 一般公司对外的接口都会用到sign签名,对不同的客户提供不同的apikey ,这样可以提高接口请求的安全性,避免被人抓包后乱请求. sign签名是一种很常见的方式 sign签名 签名参数sign ...

  7. YAPI接口自动鉴权功能部署详解

    安装准备 以下操作,默认要求自己部署过yapi,最好是部署过yapi二次开发环境. 无论是选择在线安装或者是本地安装,都需要安装client工具. 1.yapi-cli:npm install yap ...

  8. WebApi安全性 参数签名校验(结合Axios使用)

    接口参数签名校验,是WebApi接口服务最重要的安全防护手段之一. 结合项目中实际使用情况,介绍下前后端参数签名校验实现方案. 签名校验规则 http请求,有两种传参形式: 1.通过url传参,最常见 ...

  9. JWT对SpringCloud进行系统认证和服务鉴权

    JWT对SpringCloud进行系统认证和服务鉴权 一.为什么要使用jwt?在微服务架构下的服务基本都是无状态的,传统的使用session的方式不再适用,如果使用的话需要做同步session机制,所 ...

随机推荐

  1. 关于客户和供应商预制凭证添加WBS字段

    客户和供应商的预制凭证的对应标准屏幕SAPLF0400301和SAPLF0400302并没有提供WBS字段,有的需求需要增强WBS字段到屏幕上,本文会介绍增强WBS字段的步骤,也请读者多多指教.为了不 ...

  2. sql文件转换为excel文件

    最近经常需要把sql整理成excel,本人比较懒,所以写一个小工具,用到了jxl包.以前没有接触过,正好了解一下. 一.基础知识       jxl操作excel包括对象 Workbook,Sheet ...

  3. vue踩坑记,持续更新中......

    1.运行项目报错 you may use special comments to disable some waring. use //eslint-disable-next-line.....吧啦吧 ...

  4. Linux下使用acme.sh申请和管理Let’s Encrypt证书

    关于Let's Encrypt 免费SSL证书 Let's Encrypt 作为一个公共且免费 SSL 的项目逐渐被广大用户传播和使用,是由 Mozilla.Cisco.Akamai.IdenTrus ...

  5. 如何在面试中介绍自己的项目经验(面向java改进版)

    本人于3年前写的博文,如何在面试中介绍自己的项目经验,经过大家的捧场,陆续得到了将近7万个点击量,也得到了众多网站公众号的转载,不过自己感觉,这篇文章更多的是偏重于方法,没有具体给到Java方面相关的 ...

  6. vim_command

    vi 打开vi空白面板 vi filename 以编辑模式打开文件.如果参数为已有文件,在vi中打开:如果参数为新文件名,在vi退出时提示用户保存编辑内容 vi -R filename 以只读模式打开 ...

  7. linux kernel操作GPIO函数

    一.头文件 #include <asm/gpio.h> 二.注册 GPIO int gpio_request(unsigned gpio, const char *label) 功能:申请 ...

  8. 使用Java语言编写一个五子棋UI界面并实现网络对战功能(非局域网)

    使用Java语言编写一个五子棋UI界面并实现网络对战功能(非局域网) 一,前期准备 1,Java IDE(Eclipse)与JDK的安装与配置jdk-15.0.1-免配置路径版提取码:earu免安装版 ...

  9. Typora笔记上传到播客时图片不显示问题解决(已解决)

    前言: ​ 相信我们都遇到过,使用Typora做笔记是一件非常令人舒服的事,然而,它却有一个非常难受的地方,那就是我们在做完笔记想要将其上传到自己的博客时,复制粘贴的图片无法显示.因为Typora复制 ...

  10. IntelliJ IDEA启动界面的秘密:当编程遇到艺术

    细心的同学会发现Intellij IDEA每次发版本的时候都会有不同的启动界面背景,都很比较抽象的艺术图像. JetBrains的其它产品也有自己独特的设计. 但是这背后是怎么实现的.有什么寓意却很少 ...