说明:刘宏缔的架构森林是一个专注架构的博客,地址:https://www.cnblogs.com/architectforest
对应的源码可以访问这里获取: https://github.com/liuhongdi/
说明:作者:刘宏缔 邮箱: 371125307@qq.com
https://github.com/liuhongdi/apisign

@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不一致也会被拦截
时间超时/重复请求也会被拦截
@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
<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这个接口
{"status":0,"msg":"操作成功","data":null}
{"msg":"sign签名校验失败","status":10007}
. ____ _ __ _ _ /\\ / ___‘_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | ‘_ | ‘_| | ‘_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ‘ |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.1.RELEASE)
spring boot:给接口增加签名验证(spring boot 2.3.1)
原文:https://www.cnblogs.com/architectforest/p/13220459.html