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


    接口参数签名校验,是WebApi接口服务最重要的安全防护手段之一. 结合项目中实际使用情况,介绍下前后端参数签名校验实现方案。

    签名校验规则

    http请求,有两种传参形式:

    1.通过url传参,最常见的就是get请求(实际上post,put,delete都可以使用这种传参方式),如:

    http://api.XXX.com/getproduct?id=value1

    2.通过request body传参,最常见的就是post请求,如下图所示
    webapi参数签名-01
    我们针对于以上两种传参方式,采用不同的签名校验规则(注:签名算法规则仅供参考)。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参数签名-02

    如果是需要全局注册,请在WebApi.config中配置,如下图所示:

    webapi参数签名-03

    有关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+签名验证

  • 相关阅读:
    购物车
    加载网格X文件代码(Unicode版本)
    金字塔代码
    纹理贴图案例
    绘制立方体
    移动端H5开发问题(html2canvas、video、audio)
    opencvpython图像处理学习笔记1
    jmeter报错o.a.j.JMeter: Uncaught exception问题排查
    记一次测试周报
    PHP 浮点数计算精度问题
  • 原文地址:https://www.cnblogs.com/xyb0226/p/11048342.html
Copyright © 2020-2023  润新知