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

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)

spring boot:给接口增加签名验证(spring boot 2.3.1)的更多相关文章

  1. spring boot: 设计接口站api的版本号,支持次版本号(spring boot 2.3.2)

    一,为什么接口站的api要使用版本号? 1,当服务端接口的功能发生改进后, 客户端如果不更新版本,    则服务端返回的功能可能不能使用,    所以在服务端功能升级后,     客户端也要相应的使用 ...

  2. spring boot rest 接口集成 spring security(2) - JWT配置

    Spring Boot 集成教程 Spring Boot 介绍 Spring Boot 开发环境搭建(Eclipse) Spring Boot Hello World (restful接口)例子 sp ...

  3. spring boot rest 接口集成 spring security(1) - 最简配置

    Spring Boot 集成教程 Spring Boot 介绍 Spring Boot 开发环境搭建(Eclipse) Spring Boot Hello World (restful接口)例子 sp ...

  4. Spring Boot初识(3)- Spring Boot整合Swagger

    一.本文介绍 如果Web项目是完全前后端分离的话(我认为现在完全前后端分离已经是趋势了)一般前端和后端交互都是通过接口的,对接口入参和出参描述的文档就是Mock文档.随着接口数量的增多和参数的个数增加 ...

  5. 47. Spring Boot发送邮件【从零开始学Spring Boot】

    (提供源代码) Spring提供了非常好用的JavaMailSender接口实现邮件发送.在Spring Boot的Starter模块中也为此提供了自动化配置.下面通过实例看看如何在Spring Bo ...

  6. Spring Boot (十): Spring Boot Admin 监控 Spring Boot 应用

    Spring Boot (十): Spring Boot Admin 监控 Spring Boot 应用 1. 引言 在上一篇文章<Spring Boot (九): 微服务应用监控 Spring ...

  7. Spring Boot 2.X(七):Spring Cache 使用

    Spring Cache 简介 在 Spring 3.1 中引入了多 Cache 的支持,在 spring-context 包中定义了org.springframework.cache.Cache 和 ...

  8. spring boot系列(七)spring boot 使用mongodb

    1 pom.xml配置 增加包依赖:spring-boot-starter-data-mongodb <dependency> <groupId>org.springframe ...

  9. Spring Boot 2.0(二):Spring Boot 2.0尝鲜-动态 Banner

    Spring Boot 2.0 提供了很多新特性,其中就有一个小彩蛋:动态 Banner,今天我们就先拿这个来尝尝鲜. 配置依赖 使用 Spring Boot 2.0 首先需要将项目依赖包替换为刚刚发 ...

随机推荐

  1. 15个随机图片API

    15个随机图片API 妈妈再也不用担心我网站没图用了呜 请不要重复刷新此页面 ! 找了很久的说,你难道不想收藏一下吗 其中有些 API 速度并不太好,可能会拖慢贵站的速度 我也不能保证这些 API 能 ...

  2. [LeetCode]617. 合并二叉树(递归)

    ###题目 给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠. 你需要将他们合并为一个新的二叉树.合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新 ...

  3. HTTP协议(二)---请求和响应

    HTTP通过请求和响应的交换达成通信. HTTP请求 请求报文由请求行(请求方法.请求URI.协议版本).请求首部字段以及内容实体(可能没有)构成. 下面是一个GET请求,没有内容实体: 下面是 一个 ...

  4. docker部署rockermq集群(docker-compose版本)

    此处采用docker-compose部署 rockermq主主集群模式 创建相关文件夹 此处创建的文件一一对应docker-compose.yml文件中的映射文件夹,酌情创建,主要需要创建配置文件夹. ...

  5. 云计算openstack共享组件——时间同步服务ntp(2)

    一.标准时间讲解 地球分为东西十二个区域,共计 24 个时区 格林威治作为全球标准时间即 (GMT 时间 ),东时区以格林威治时区进行加,而西时区则为减. 地球的轨道并非正圆,在加上自转速度逐年递减, ...

  6. jenkins结合cygwin软件实现从centos发布代码rsync到windows server2019的过程

    jenkins结合cygwin软件实现从centos发布代码rsync到windows server2019的过程 1.下载cygwin这个软件打开https://cygwin.com/install ...

  7. Node.js 从零开发 web server博客项目[数据存储]

    web server博客项目 Node.js 从零开发 web server博客项目[项目介绍] Node.js 从零开发 web server博客项目[接口] Node.js 从零开发 web se ...

  8. vue中父子组件传值问题 通过props 和 $emit()方法

    (代码在最后) 1.父组件给子组件传值直接通过props,听着很简单,但是对于初学者来说还是比较难以理解的,今天小白通过自己的实践操作结合代码分析一下 案例  把模态框单独的抽离出来,当作一个组件 第 ...

  9. OOD 面向对象面试干货分享| 面向对象设计的SOLID原则

    S.O.L.I.D是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写. 简写 全拼 中文翻译 SRP The Single Res ...

  10. ansible-doc到底有多好用,助你玩转各种模块

    #使用ansible-doc:查看各种模块的帮助 #命令格式: ansible-doc -l #列出所有的模块列表 ansible-doc -s 模块名 #查看指定模块的参数 ansible-doc ...