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

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

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

说明:刘宏缔的架构森林是一个专注架构的博客,

网站:https://blog.imgtouch.com
本文: https://blog.imgtouch.com/index.php/2023/05/23/springboot-gei-jie-kou-zeng-jia-qian-ming-yan-zheng-springboot231/

         对应的源码可以访问这里获取: 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)

 

posted @ 2020-07-01 18:27  刘宏缔的架构森林  阅读(10453)  评论(0编辑  收藏  举报