说明:刘宏缔的架构森林是一个专注架构的博客,地址: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