接口参数签名校验, 是 webApi 接口服务最重要的安全防护手段之一. 结合项目中实际使用情况, 介绍下前后端参数签名校验实现方案.
签名校验规则
http 请求, 有两种传参形式:
1. 通过 url 传参, 最常见的就是 get 请求(实际上 post,put,delete 都可以使用这种传参方式), 如:
http://api.XXX.com/getproduct?id=value1
2. 通过 request body 传参, 最常见的就是 post 请求, 如下图所示
我们针对于以上两种传参方式, 采用不同的签名校验规则(注: 签名算法规则仅供参考).WebApi 是不支持通过 url 和 body 同时传参数的, 所以在服务端可以通过 HttpContext.Current.Request.QueryString 获取到 form 参数进行判断, 执行不同逻辑, 如下代码所示:
- var form = HttpContext.Current.Request.QueryString; // 请求的 url 参数
- var data = string.Empty;
- if (form.Count> 0)
- {
- // 第一步: 取出所有 form 参数
- IDictionary<string, string> parameters = new Dictionary<string, string>();
- for (var f = 0; f <form.Count; f++)
- {
- var key = form.Keys[f];
- parameters.Add(key, form[key]);
- }
- // 第二步: 把字典按 Key 的字母顺序排序
- IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(parameters);
- var dem = sortedParams.GetEnumerator();
- // 第三步: 把所有参数名和参数值串在一起
- var query = new StringBuilder();
- while (dem.MoveNext())
- {
- var key = dem.Current.Key;
- var value = dem.Current.Value;
- if (!string.IsNullOrEmpty(key)) query.Append(key).Append(value);
- }
- data = query.ToString();
- }
- else
- {
- // 请求输入的内容, 即 body 内容
- var stream = HttpContext.Current.Request.InputStream;
- stream.Position = 0;
- var responseJson = string.Empty;
- var streamReader = new StreamReader(stream);
- data = streamReader.ReadToEnd();
- stream.Position = 0;
- }
通过上述逻辑之后, data 变量中存储的就是接口参数内容. 以下是实际签名校验逻辑
- /// <summary>
- /// 签名校验
- /// </summary>
- /// <param name="timeStamp">时间戳(按秒)</param>
- /// <param name="data">参数内容</param>
- /// <param name="signature">前端签名值</param>
- /// <returns></returns>
- public bool Validate(string timeStamp, string data, string signature)
- {
- var hash = MD5.Create();
- // 拼接签名数据
- var signStr = timeStamp + data;
- // 将字符串中字符按升序排序
- var sortStr = string.Concat(signStr.OrderBy(c => c));
- var bytes = Encoding.UTF8.GetBytes(sortStr);
- // 使用 32 位大写 MD5 签名
- var md5Val = hash.ComputeHash(bytes);
- var result = new StringBuilder();
- foreach (var c in md5Val) result.Append(c.ToString("X2"));
- var s = result.ToString().ToUpper();
- // 与前端传过来的签名参数进行比对
- return s == signature;
- }
Action 拦截器实现对某些 API 进行签名校验
创建 WebApi 的 Action 拦截器 HandlerSecretAttribute
- /// <summary>
- /// 签名安全拦截过滤器
- /// </summary>
- public class HandlerSecretAttribute : ActionFilterAttribute
- {
- private readonly ExcuteMode _customMode;
- /// <summary > 默认构造</summary>
- /// <param name="Mode">认证模式</param>
- public HandlerSecretAttribute(ExcuteMode Mode)
- {
- _customMode = Mode;
- }
- /// <summary>
- /// 安全校验
- /// </summary>
- /// <param name="filterContext"></param>
- public override void OnActionExecuting(HttpActionContext filterContext)
- {
- // 是否忽略权限验证
- if (_customMode == ExcuteMode.Ignore) return;
- // 从 http 请求的头里面获取 AppId
- var request = filterContext.Request;
- var method = request.Method.Method;
- var appId = ""; // 客户端应用唯一标识
- long timeStamp; // 时间戳, 校验 10 分钟内有效
- var signature = ""; // 参数签名, 去除空参数, 按字母倒序排序进行 Md5 签名 为了提高传参过程中, 防止参数被恶意修改, 在请求接口的时候加上 sign 可以有效防止参数被篡改
- try
- {
- appId = request.Headers.GetValues("appId").SingleOrDefault();
- timeStamp = Convert.ToInt64(request.Headers.GetValues("timeStamp").SingleOrDefault());
- signature = request.Headers.GetValues("signature").SingleOrDefault();
- }
- catch (Exception ex)
- {
- throw new UserFriendlyException("签名参数异常:" + ex.Message);
- }
- #region 安全校验
- //TODO: 实际逻辑处理
- base.OnActionExecuting(filterContext);
- #endregion
- }
- }
具体的使用, 如下图所示:
如果是需要全局注册, 请在 WebApi.config 中配置, 如下图所示:
有关 HandlerSecretAttribute 的源代码, 我已整理放至 GitHub 上 https://github.com/yinboxie/BlogExampleDemo
前端 Axios 请求统一拦截处理
客户端 http 请求 (以 Axios 为例) 进行统一的拦截处理. 前端用过诸多 http 插件, 如 Ajax,fetch,vue-resoure,axios 等, 个人感觉 axios 的请求拦截是最好用的.
- import axios from 'axios'
- import { sign } from './sign'
- let _ = require('lodash')
- var service = axios.create({
- baseURL:'http://xxx.com'
- timeout:10000 // 请求超时时间
- })
- // request 拦截器
- service.interceptors.request.use(
- config => {
- let token = getToken()
- if (token) {
- config.headers['token'] = token // 让每个请求携带自定义 token 请根据实际情况自行修改
- }
- // 如果接口需要签名, 则通过请求时, headers 中传递 sign 参数 true
- if (config.headers['sign']) {
- config = sign(config) // 核心签名逻辑, 独立封装了处理函数
- }
- return config
- },
- error => {
- // Do something with request error
- console.log(error) // for debug
- Promise.reject(error)
- }
- )
sign 函数核心源码
- import md5 from 'js-md5'
- let _ = require('lodash')
- /**
- * 接口参数签名
- * @param {*} config 请求配置
- */
- export const sign = config => {
- // 获取到秒级的时间戳, 与后端对应
- let tmp = new Date()
- .getTime()
- .toString()
- .substr(0, 10)
- let header = {
- appId:'pmes',
- timeStamp: tmp,
- signature: ''
- }
- let signStr = _.toString(header.timeStamp)
- if (config.params) {
- // url 参数签名
- let pArray = []
- for (let p in config.params) {
- pArray.push(p)
- }
- let sArray = pArray.sort()
- for (let item of sArray) {
- signStr += item + _.toString(config.params[item])
- }
- } else if (config.data) {
- // request body 参数的内容
- signStr += JSON.stringify(config.data)
- }
- // 签名核心逻辑
- let newsignStr = _.sortBy(signStr, s => s.charCodeAt(0)).join('')
- let s = md5(newsignStr).toUpperCase()
- header.signature = s
- config = Object.assign(config, { headers: header })
- return config
- }
实际的调用函数
- // post 请求, body 传参
- axios.post('/Login/CheckLoginTest1',
- { Account: 'xiaowang',Password: '123' },
- { headers: { sign: true }}
- ).then(d => {
- console.log(d)
- })
- // post 请求, url 传参
- axios.post('/Login/CheckLoginTest3',
- null,
- {
- params: {t1: '2',t2: '3'},
- headers: { sign: true }
- }
- ).then(d => {
- console.log(d)
- })
- // get 请求
- axios.get('/Login/CheckLoginTest2',
- {
- params: {t1: '2',t2: '3'},
- headers: { sign: true }
- }
- ).then(d => {
- console.log(d)
- })
总结
为了保证 WebApi 数据在通信时的安全性, 需要采取多重安全防护: 参数签名校验, token 验证, 跨域权限, 时间戳过期校验, 允许请求的 appId 校验等. 当然具体的规则我们都得依据项目实际情况去实现, 这里就不再展开讨论其他方式的实现.
参考
WebApi 安全性 使用 TOKEN + 签名验证
来源: https://www.cnblogs.com/xyb0226/p/11048342.html