攻击者发送一个目的主机已经接收过的包,特别是在认证的过程中,用于认证用户身份所接收的包,来达到欺骗系统的目的,主要用于身份认证过程,破坏认证的安全性。也可利用系统中POST请求数据包未针对单个请求设置有效的验证参数,导致会话请求可以重放,无限制的向数据库中插入海量数据,或无限制的上传文件到系统中,造成资源浪费。
//用于获取当前系统时间,以毫秒为单位
@RequestMapping(value="queryCurrentTime",method=RequestMethod.GET)
public Map<String,Object> queryCurrentTime(){
Map<String,Object> map = new HashMap<>();
map.put("code","SUCCESS");
map.put("message","成功!");
map.put("data",System.currentTimeMillis());
}
import request from '@/utiils/request.js'
import CryptoJS from 'crypto-js'
//随机数
let guid = function(){
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c){
var r=Math.random()*16 | 0,
v = c == 'x' ? r:(r & 0x3 | 0x8);
return v.toString(16);
})
}
// 请求拦截器
service.interceptors.request.use(
async (config) => {
// 在发送请求之前做一些事情
//此处只对post做处理, 如若对get请求做处理,将此post校验if去掉即可
if(config.method =="post"){
// 防止死循环
if (url.indexOf('queryCurrentTime') !== -1) {
return config
}
const req = {
url: 'http://localhost:8888/queryCurrentTime',
method: 'get',
}
//async await 同步
await request(req).then(response => {
// 服务器获取时间戳
let timestamp = data.data
const uuid = require('uuid')
// 在localStorage中获取盐值
// let saltValue = window.localStorage.getItem('uId')
let saltValue = 'CURRENTIME'
let params = ''
if (config.method === 'post' && config.data) {
params = decodeURIComponent(config.data)
} else {
if (config.params) {
let paramsKeys = []
paramsKeys = Object.keys(config.params)
// 根据key进行排序
paramsKeys = paramsKeys.sort()
let paramsValues = []
paramsKeys.forEach(value => {
if(typeof config.params[value] !== 'object'){
paramsValues.push(config.params[value])
}
})
params = paramsValues.join('-')
}
}
// console.log(params)
// 生成随机数uuid
let uId = uuid.v4()
// MD5签名
let sign = CryptoJS.md5(timestamp + uId + params + saltValue)
config.headers.timestamp = timestamp
config.headers.sign = sign
config.headers.replayToken = uId
})
}
return config
},
error => {
// 做一些请求错误
console.log(error)
return Promise.reject(error)
}
)
@Configuration
public class FilterConfig{
@Autowired
private CommitTokenService commitTokenService ;
//此处解决RedisTemplate对象无法注入的问题 !! 将过滤器定义为bean对象,交由spring管理
//过滤器属于servlet范围,无法直接注入对象
//springboot关于拦截器或者过滤器无法注入bean的问题 ? 参考 https://segmentfault.com/a/1190000012107467
@Bean
public FormFilter formFilter(){
return new FormFilter();
}
@Bean
public FilterRegistrationBean filterRegistrationBean (){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean ();
filterRegistrationBean.setOrder(3);
filterRegistrationBean.setFilter(formFilter());
filterRegistrationBean.setName("FormFilter");
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
public class ExceptionCommonUtils{
pulbic static String getId(){
return uuid.replace("-","");
}
//用来响应信息
public static void responseMessage(HttpServletResponse response,Integer code,String message)throw IOException{
Map<String,Object> map = new HashMap<>();
map.put("code",code);
map.put("message",message);
response.setContentType(text/json;charset=utf-8);
JSONObject fromObject = JSONObject.formObject(map);
PrintWriter pw = response.getWriter();
pw.write(fromObject.toString());
pw.flush();
pw.close();
}
}
public class IntegrityChecking{
//盐值
private static final String MARKER = "CURRENTTIME";
//获取sign值
public static String getSign(String timestamp,String raToken,String data){
StringBuilder sb = new StringBuilder ();
sb.append(timestamp);
sb.append(raToken);
sb.append(data);
sb.append(MARKER);
return DigestUtils.md5DigestAsHex(sb.toString().getBytes());
}
public class FormFilter implements Filter{
@Autowired
private RedisTemplate redisTemplate;
private static final long EXPIRTIME = 3600L; //1小时
private static final String PREFIX = "WQ";
public FormFilter (){}
@Override
public void doFilter(ServletRequest arg0,ServletResponse arg1,FilterChain arg2)throws IOException,ServletException{
HttpServletRequest req = (HttpServletRequest)arg0;
HttpServletResponse response = (HttpServletResponse)arg1;
//如果是获取时间戳的请求,直接放行,或者是get请求进行防重放,完整性校验处理
String url = req.getRequestURI();
if(url.indexOf("queryCurrentTime") != -1 || "GET".equals(req.getMethod())){
arg2.doFilter(req,response);
}else{
//随机数
String timestamp = req.getHeader("timestamp");
//token
String raToken = req.getHeader("replayToken");
//前台MD5生成的sign密文字符串
String sign = req.getHeader("sign");
try{
//防止timestamp raToken被篡改
if(!"".equals(timestamp) && !"".equals(raToken) && !"".equals(sign) && null != timestamp && null != raToken && null != sign){
String strString = "";
StringBuilder str = new StringBuilder();
Enumeration<String> param = req.getParameterNames();
if("POST".equals(req.getMethod())){
while(param.hasMoreElements()){
String key = String.valueOf(param.nextElement());
String value = req.getParameter(key);
str.append(key).append("=").append(value).append("&");
}
}else if("GET".equals(req.getMethod())){
while(param.hasMoreElements()){
String key = String.valueOf(param.nextElement());
String value = req.getParameter(key);
str.append(value).append("-");
}
}
//去掉最后一个字符
if(!"".equals(str.toString()) && str.length() > 0){
strString = str.toString().substring(0,str.length() -1);
}
// 对比sign值
String parSign = IntegrityChecking.getSign(timestamp,raToken,strString);
if(!sign.equals(parSign)){
throw new RunTimeException("请勿中途修改数据,触发完整性校验!");
}
//计算时间差
long parseInt = Long.parseLong(timestamp);
long currentTimeMills = System.currentTimeMills();
long reqTime = (currentTimeMills - parseInt );
//60秒内的sign放入redis中
if(reqTime <= 60000){
//检查key是否存在
if(!redisTemplate.hasKey(PREFIX + sign)){
//设置变量值的过期时间
redisTemplate.opsForValue().set(PREFIX + sign,0,EXPIRTIME,TimeUnit.SECONDS);
}else{
throw new RunTimeException("请勿重复提交表单!");
}
}else{
//大于60秒,判为请求超时
throw new RunTimeException("请求超时,请勿重复提交表单!");
}
arg2.doFilter(req,response);
} else {
throw new RunTimeException("TIMESTAMP.TOKEN.SIGN为空,请勿重复提交表单或触发完整性校验!");
}
}catch(RunTimeException e){
ExceptionCommonUtils.responseMessage(response,e.getCode(),e.getMessage());
}catch(Exception e){
ExceptionCommonUtils.responseMessage(response,"50000","formFilter异常!");
}
}
}