springboot:给接⼝增加签名验证(springboot2.3.1)⼀,为什么要给接⼝做签名验证?
1,app客户端在与服务端通信时,通常都是以接⼝的形式实现,
这种形式的安全⽅⾯有可能出现以下问题:
被⾮法访问(例如:发短信的接⼝通常会被利⽤来垃圾短信)
被重复访问  (例如:在提交订单时多点了⼏次提交按钮)
⽽客户端存在的弱点是:对接⼝站的地址不能轻易修改,
所以我们需要针对从app到接⼝的接⼝做签名验证,
接⼝不能随便app之外的应⽤访问
2,要注意的地⽅:
我们给app分配⼀个app_id和⼀个app_secret
app对app_secret的保存要做到不会被轻易的反编译出来,
否则安全就没有了保障
android平台建议保存到⼆进制的so⽂件中
说明:刘宏缔的架构森林是⼀个专注架构的博客,地址:
对应的源码可以访问这⾥获取:
说明:作者:刘宏缔邮箱: 371125307@qq
⼆,演⽰项⽬的相关信息
1,项⽬的地址
github/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 = Logger("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 = Header("appId");
if (StringUtils.isBlank(appId)) {
return false;
}
String timestampStr = Header("timestamp");
if (StringUtils.isBlank(timestampStr)) {
return false;
}
String sign = Header("sign");
if (StringUtils.isBlank(sign)) {
return false;
}
String nonce = Header("nonce");
if (StringUtils.isBlank(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) {
<("发⽣异常",e);
}
//前端的时间戳与服务器当前时间戳相差如果⼤于180,判定当前请求的timestamp⽆效
if (Math.abs(timestamp - System.currentTimeMillis() / 1000) > 180) {
return false;
}
//nonce是否存在于redis中,检查当前请求是否是重复请求
boolean nonceExists = redisStringUtil.hasStringkey(SIGN_KEY+timestampStr+nonce);
if (nonceExists) {
return false;
}
//后端MD5签名校验与前端签名sign值⽐对
if (!(sign.equalsIgnoreCase(signEcrypt))) {
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",springboot结构
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>
如图:
说明:
2,查看效果:
成功时返回:
{"status":0,"msg":"操作成功","data":null}
报错时返回:
{"msg":"sign签名校验失败","status":10007}
五,查看spring boot的版本:  .  ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/  ___)| |_)| | | | | || (_| |  ) ) ) )
'  |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot ::        (v2.3.1.RELEASE)

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。