需求描述:

  项目里的几个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. 数据接口请求异常:parsererror

    问题一:直接拿别人的文件放在本地打开 如下图 原因:这是提示"交叉源请求仅支持协议方案:HTTP.数据.Chrome.Chrome扩展.HTTPS." 也就是你不能用本地文件打开, ...

  2. java零基础之--【Lombok】简化类设计神器

    I1. 在类设计中我们必不可少的要进行属性定义,构造方法,setter/getter方法,toString方法定义,如果在设计项目中属性过多则会影响类的阅读性. Lombok作为第三方插件,很好的解决 ...

  3. 负载均衡各个算法JAVA诠释版

    00 前言 首先给大家介绍下什么是负载均衡(来自百科) 负载均衡建立在现有网络结构之上,它提供了一种廉价有效透明的方法扩展 网络设备和 服务器的带宽.增加 吞吐量.加强网络数据处理能力.提高网络的灵活 ...

  4. Linux 时间同步 04 ntp时间同步

    Linux 时间同步 04 ntp时间同步 目录 Linux 时间同步 04 ntp时间同步 安装ntp 配置与外部时间服务器进行时间同步的客户端主机 配置其他客户端与以上客户端主机时间同步 验证查看 ...

  5. idea破解方式:永久激活

    相信很多小伙伴都发现了,每年到年底的时候,idea注册码都大面积的失效,早上到办公室打开电脑发现注册码过期,还要花很长时间找新的: 这里介绍两种破解方式,都是有生之年不到期:与网上现有的方式基本一致. ...

  6. 解决Establishing SSL connection without server‘s identity verification is not recommended.

    每次从数据库中进行查询或者其他操作控制台都会出现以下警告,虽说不是error,但是很显眼.. WARN: Establishing SSL connection without server's id ...

  7. 70.LeetCode爬楼梯

    爬楼梯 点击标题可跳转到官网进行查看 假设你正在爬楼梯.需要 n 阶你才能到达楼顶. 每次你可以爬 1 或 2 个台阶.你有多少种不同的方法可以爬到楼顶呢? 注意:给定 n 是一个正整数. 示例 1: ...

  8. 区间合并 C++

    #include <iostream> #include <vector> #include <algorithm> using namespace std; ty ...

  9. Tomcat的整体架构

    Tomcat通过连接器和容器这两个核心组件完成整体工作,连接器负责处理socket连接和网络字节流与Request和Response对象的转化:容器负责加载和管理Servlet,以及具体处理Reque ...

  10. MongoDB导出导入功能

    导出脚本: mongo_export.sh !#/bin/bash mongoexport -h x.x.x.x  --port 27017 -d database -c collection  -q ...