项目到后期都会遇到安全测试问题,本篇整理一下博主的项目怎么解决会话重放和数据篡改问题的。
一般我们的项目中涉及增删改查操作时,前台发起请求后,攻击者利用抓包工具恶意修改客户端的请求参数,就会使服务器执行攻击者想要执行的操作。
对于增删改操作,攻击者抓包后可以无限次发起同样的请求,这样就会使服务器产生很多无用数据甚至崩溃,这就是所谓的会话重放。而对于查询操作一般不会有这个问题,因为查询并没有改变数据(既然都能查到了随你怎么查呗),但是如果测试人员要求改,那我们苦命的开发只能改喽(心中MMP?)。
博主一开始只针对增删改操作做了修改,修改方法:
首先,写一个自定义注解,对controller中需要作验证的方法上加上注解,代码:
public class TokenInfo {
/**
* 用户token信息
*/
private static Map<String,String> userToken=new HashMap<String,String>();
public static String getUserToken(String userId){
if(!userToken.containsKey(userId)){
userToken.put(userId, "");
}
return userToken.get(userId);
}
public static void setUserToken(String userId,String token){
userToken.put(userId, token);
}
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TokenAnnotation {
public abstract boolean open();
}
用户登录后,系统第一次访问服务器时(这里由于博主的项目登录系统调用的接口,无法在登录方法中操作),后台生成一个随机值(TOKEN),存到用户对象(USER)中,并返回前台,前台存到sessionStorage中(sessionStorage类似cookie,但存放的数据比cookie多,浏览器关闭时清空),有增删改操作并再次发起请求时(默认使用ajax请求,这里就需要注意前期代码的规范问题了),使用ajaxSend方法将sessionStorage中token值取出,加到参数中传递到后台。后台写一个拦截器,获取请求的方法以及请求参数,如果请求的方法加了注解即验证token,从用户对象中取出token值与前台传过来的token比较,如果相同则验证通过,生成新的token值保存并返回前台保存(这里需要对controller中方法的返回值作修改,加上token值,这里又涉及到代码的规范问题了?),如此循环,如果验证失败则作相应处理。代码:
/**
* token验证
* method为需要访问的method对象,自己想办法获取
*/
public boolean tokenAnnotation(Method method,HttpServletRequest request,HttpServletResponse response){
if(method.isAnnotationPresent(TokenAnnotation.class)){
TokenAnnotation tokenAnn = method.getAnnotation(TokenAnnotation.class);
boolean open = tokenAnn.open();
if(open){
//得到用户对象信息,这里就不写全了
user = ······
String token = request.getParamter("TOKENVALUE");
String trueToken = TokenInfo.getUserToken(user.getUserId());
if(token!=null&&token.equals(trueToken)){
//更新token
WebUtil.openToken(user);
}
}
}
}
public static void openToken(User user){
//设置token
String tokenValue = getNewToken();
TokenInfo.setUserToken(user.getUserId(),tokenValue);
user.setTokenValue(tokenValue);
}
public static String getNewToken(){
//生成10位数的token
String token = "";
Random ran = new Random();
for(int i=0;i<10;i++){
token+=ran.nextInt(10);
}
return token;
}
controller中方法这里就不贴了,总体来说不是是map就是json,直接putTOKEN值就行了,前台ajax响应后,利用ajaxComplete,将token值放到sessionStorage中。
另外需要说明的一点,所有的ajax请求需要设置为同步,如果ajax中有嵌套另一个ajax,可以再在前后台保存一个token修改次数(requestStamp),ajaxComplete方法作相应修改,相应的后台请求的方法返回时,REQUESTSTAMP需要加1并且返回前台
var requestStamp = xhr.responseJSON.REQUESTSTAMP;
if(requestStamp > sessionStorage.STAMP){
sessionStorage.STAMP = requestStamp ;
sessionStorage.TOKENVALUE = xhr.responseJSON.TOKENVALUE ;
}
至此增删改的token验证功能已经做好了,如果不涉及查询一切都没有问题,然而···
好吧,不扯了,上干货。对于查询操作,最多有个参数篡改问题,对于这个问题使用上面的token验证是无法起作用的。这个问题困扰了博主很久,最后使用了最简单粗暴的方法,加时间戳,直接防止抓包!
具体方法:前台发请求的时候获取本地时间,加到参数中并加密,传到后台,拦截器拦截到参数中带有时间戳的,解密,与服务器本地时间作比较,时间差大于2000毫秒的则验证不通过,这里需要注意客户端与服务端本身就有时间差,我的做法是系统第一次访问后后台返回本地时间,前台接收后与本地时间相减存到sessionStorage中,下次发起请求时发送的时间参数加上这个时间差。
//系统进入的第一个页面,将初始token值,初始stamp,服务器时间保存
sessionStorage.TOKENVALUE = "${token}";
sessionStorage.STAMP = 0;
var server_time = "${server_time}";
var user_time = (new Date()).getTime();
//发请求加上时间戳参数时加上这个时间差再加密
sessionStorage.DTIME = server_time - user_time;
int show = 0;
int show2 = 0;
int show3 = 0;
for (String key : paramMap.keySet()) {
String[] values = paramMap.get(key);
for (int i = 0; i < values.length; i++) {
String param = values[i];
if(key.equals("BF1DC")){
show++;
try {
Des desObj = new Des();
String dec = desObj.strDec(param, "1", "2", "3");
String thatTime = dec.substring(1);
Long lostTime = System.currentTimeMillis()-Long.parseLong(thatTime);
if(lostTime>2000){
show3++;
}
} catch (Exception e) {
// TODO: handle exception
show2++;
}
}
}
}
if(show>0&&(show2>0||show3>0)){
String head = request.getHeader("X-Requested-With");
if(!StringUtil.isEmpty(head)){
//ajax请求则将错误信息返回到data中,ajax success方法中作相应处理
}else{
//普通请求直接返回错误页面
}
}
写到这里终于大功告成了,那么问题来了,为什么一开始不用时间戳呢?博主只能说,程序员都是一步一个坑爬过去的???,还有最重要的一点,项目初期就要严格规范下代码书写规范,便于维护。最最最后一点,老项目碰不得???。