• spring boot:给接口增加签名验证(spring boot 2.3.1)


    一,为什么要给接口做签名验证?

    1,app客户端在与服务端通信时,通常都是以接口的形式实现,
    这种形式的安全方面有可能出现以下问题:
    被非法访问(例如:发短信的接口通常会被利用来垃圾短信)
    被重复访问  (例如:在提交订单时多点了几次提交按钮)
    而客户端存在的弱点是:对接口站的地址不能轻易修改,
    所以我们需要针对从app到接口的接口做签名验证,
    接口不能随便app之外的应用访问
     
    2,要注意的地方:
       我们给app分配一个app_id和一个app_secret
       app对app_secret的保存要做到不会被轻易的反编译出来,
       否则安全就没有了保障
       android平台建议保存到二进制的so文件中 
     

    说明:刘宏缔的架构森林是一个专注架构的博客,地址:https://www.cnblogs.com/architectforest

             对应的源码可以访问这里获取: https://github.com/liuhongdi/

    说明:作者:刘宏缔 邮箱: 371125307@qq.com

     

    二,演示项目的相关信息

      1,项目的地址
    https://github.com/liuhongdi/apisign
      2,项目的原理:
    给客户端分发:appId,appSecret,version三个字串
    appId:分配给客户端的id
    appSecret:密钥字串,客户端要安全保存
    version:服务端的接口版本
     
    客户端在发送请求前,
    用appId + appSecret + timestamp +  nonce + version做md5,生成sign字串,
    这个字串和appId/timestamp/nonce一起发送到服务端
    服务端验证sign是否正确,
    如果有误则拦截请求
     
      3,项目的结构 
     如图:
     

    三, java代码说明:

    1,SignInterceptor.java
    @Component
    public class SignInterceptor implements HandlerInterceptor {
        private static final String SIGN_KEY = "apisign_";
        private static final Logger logger = LogManager.getLogger("bussniesslog");
        @Resource
        private RedisStringUtil redisStringUtil;
    
        /*
        *@author:liuhongdi
        *@date:2020/7/1 下午4:00
        *@description:
         * @param request:请求对象
         * @param response:响应对象
         * @param handler:处理对象:controller中的信息   *
         * *@return:true表示正常,false表示被拦截
        */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //依次检查各变量是否存在?
            String appId = request.getHeader("appId");
            if (StringUtils.isBlank(appId)) {
                ServletUtil.renderString(response, JSON.toJSONString(ResultUtil.error(ResponseCode.SIGN_NO_APPID)));
                return false;
            }
            String timestampStr = request.getHeader("timestamp");
            if (StringUtils.isBlank(timestampStr)) {
                ServletUtil.renderString(response, JSON.toJSONString(ResultUtil.error(ResponseCode.SIGN_NO_TIMESTAMP)));
                return false;
            }
            String sign = request.getHeader("sign");
            if (StringUtils.isBlank(sign)) {
                ServletUtil.renderString(response, JSON.toJSONString(ResultUtil.error(ResponseCode.SIGN_NO_SIGN)));
                return false;
            }
            String nonce = request.getHeader("nonce");
            if (StringUtils.isBlank(nonce)) {
                ServletUtil.renderString(response, JSON.toJSONString(ResultUtil.error(ResponseCode.SIGN_NO_NONCE)));
                return false;
            }
            //得到正确的sign供检验用
            String origin = appId + Constants.APP_SECRET + timestampStr + nonce + Constants.APP_API_VERSION;
            String signEcrypt = MD5Util.md5(origin);
            long timestamp = 0;
            try {
                timestamp = Long.parseLong(timestampStr);
            } catch (Exception e) {
                logger.error("发生异常",e);
            }
            //前端的时间戳与服务器当前时间戳相差如果大于180,判定当前请求的timestamp无效
            if (Math.abs(timestamp - System.currentTimeMillis() / 1000) > 180) {
                ServletUtil.renderString(response, JSON.toJSONString(ResultUtil.error(ResponseCode.SIGN_TIMESTAMP_INVALID)));
                return false;
            }
            //nonce是否存在于redis中,检查当前请求是否是重复请求
            boolean nonceExists = redisStringUtil.hasStringkey(SIGN_KEY+timestampStr+nonce);
            if (nonceExists) {
                ServletUtil.renderString(response, JSON.toJSONString(ResultUtil.error(ResponseCode.SIGN_DUPLICATION)));
                return false;
            }
            //后端MD5签名校验与前端签名sign值比对
            if (!(sign.equalsIgnoreCase(signEcrypt))) {
                ServletUtil.renderString(response, JSON.toJSONString(ResultUtil.error(ResponseCode.SIGN_VERIFY_FAIL)));
                return false;
            }
            //将timestampstr+nonce存进redis
            redisStringUtil.setStringValue(SIGN_KEY+timestampStr+nonce, nonce, 180L);
            //sign校验无问题,放行
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        }
    }

    说明:如果客户端请求的数据缺少会被拦截

              与服务端的appSecret等参数md5生成的sign不一致也会被拦截

              时间超时/重复请求也会被拦截


     2,DefaultMvcConfig.java
    @Configuration
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
    public class DefaultMvcConfig implements WebMvcConfigurer {
    
        @Resource
        private SignInterceptor signInterceptor;
    
        /**
         * 添加Interceptor
    * liuhongdi
    */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(signInterceptor) .addPathPatterns("/**") //所有请求都需要进行报文签名sign .excludePathPatterns("/html/*","/js/*"); //排除html/js目录 } }

    说明:用来添加interceptor

     

    四,效果验证:

    1,js代码实现:
      说明:我们在这里使用js代码供仅演示使用,app_secret作为密钥不能使用js保存:
      
    <body>
    <a href="javascript:login('right')">login(right)</a><br/>
    <a href="javascript:login('error')">login(error)</a><br/>
    <script>
        //vars
        var appId="wap";
        var version="1.0";
    
        //得到sign
        function getsign(appSecret,timestamp,nonce) {
            var origin = appId + appSecret + timestamp +  nonce + version;
            console.log("origin:"+origin);
            var sign = hex_md5(origin);
            return sign;
        }
    
        //访问login这个api
        //说明:这里仅仅是举例子,在ios/android开发中,appSecret要以二进制的形式编译保存
        function login(isright) {
            //right secret
            var appSecret_right="30c722c6acc64306a88dd93a814c9f0a";
            //error secret
            var appSecret_error="aabbccdd";
            var timestamp = parseInt((new Date()).getTime()/1000);
            var nonce = Math.floor(Math.random()*8999)+1000;
            var sign = "";
            if (isright == 'right') {
                 sign = getsign(appSecret_right,timestamp,nonce);
            } else {
                 sign = getsign(appSecret_error,timestamp,nonce);
            }
    var postdata = { username:"a", password:"b" } $.ajax({ type:"POST", url:"/user/login", data:postdata, //返回数据的格式 datatype: "json", //在请求之前调用的函数 beforeSend: function(request) { request.setRequestHeader("appId", appId); request.setRequestHeader("timestamp", timestamp); request.setRequestHeader("sign", sign); request.setRequestHeader("nonce", nonce); }, //成功返回之后调用的函数 success:function(data){ if (data.status == 0) { alert('success:'+data.msg); } else { alert("failed:"+data.msg); } }, //调用执行后调用的函数 complete: function(XMLHttpRequest, textStatus){ //complete }, //调用出错执行的函数 error: function(){ //请求出错处理 } }); } </script> </body>

    如图:

    说明:

    login(right):使用正确的appSecret访问login这个接口
    login(error):使用错误的appSecret访问login这个接口

     
     
    2,查看效果:
    成功时返回:
    {"status":0,"msg":"操作成功","data":null}
    报错时返回:
    {"msg":"sign签名校验失败","status":10007}

    五,查看spring boot的版本: 

      .   ____          _            __ _ _
     /\ / ___'_ __ _ _(_)_ __  __ _    
    ( ( )\___ | '_ | '_| | '_ / _` |    
     \/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
     =========|_|==============|___/=/_/_/_/
     :: Spring Boot ::        (v2.3.1.RELEASE)
  • 相关阅读:
    【leetcode】38. Count and Say
    【leetcode】132. Palindrome Partitioning II
    New Concept English three (56)
    New Concept English three (55)
    New Concept English three (54)
    listening 1
    New Concept English three (53)
    BEC translation exercise 4
    New Concept English three (52)
    MBA 工商管理课程-风险型决策方法
  • 原文地址:https://www.cnblogs.com/architectforest/p/13220459.html
Copyright © 2020-2023  润新知