对于前后端分离的项目,后端人员通常都要对发起请求的用户的合法性和权限进行审核(比如用户每次请求都要携带token,token校验通过的才放行),只要审核通过了,基本上都允许用户的后续操作。可是这样就安全了吗?任何一个在后端开发浸淫多年的人,都会不知不觉间往数据安全方面倾注更多的精力。笔者作为某小公司中的唯一后端开发,在数据安全这块也是操碎了心的,先是重构了登录注册接口,防止用户账号密码被暴力破解;接着又引入了token令牌机制,对所有访问网站资源的请求的进行安全过滤;紧接着又对支付系统进行了重构,对金额和密码进行了加密传输,···。咱也是不得不关心啊,身在其位就得谋其事,想象一下,未来哪天项目终于上了正轨,开始盈利,你正准备迎接成功的时候,突然用户账号被频繁盗取或者网站接口被人恶意调用导致服务器崩溃了等等,那时才蛋疼。
回到前面的话题,只是对用户的合法性校验通过就足够了吗?如果是合法用户进行恶意操作呢?说的通俗一点,一个通过正常流程注册的合法的账号,在1秒钟内10次调用了同一个接口,这种请求合理吗?如果1秒钟内调用了100次呢?如果这个接口是类似抢单或支付的接口呢?退一步说,这个社会是很和谐美好的,好人总会比坏人多,但是如果你遇到的是一个不靠谱的前端开发人员呢?他在调用后端接口的时候,不会对必传字段做空值判断,更不会在用户点击按钮之后,后端还没有返回处理结果的那不算很长但用户足以再多点几次按钮的时间内将按钮禁用掉,这样也会造成接口被重复调用。
因此,防止接口被重复调用也是后端开发人员需要认真对待的问题。
小弟不才,仅能在已有的知识范畴内勉强给出一种临时解决方案,也算对得起自己现今所处的岗位了,下面是代码:
package interceptors;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.alibaba.fastjson.JSON;
//使用spring的拦截器来实现拦截重复请求的功能
public class UserPermissionInterceptor implements HandlerInterceptor {
private static final Logger logger = Logger.getLogger(UserPermissionInterceptor.class);
/**
* 完成页面的render后调用
*/
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object object, Exception exception)
throws Exception {
}
/**
* 在调用controller具体方法后拦截
*/
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object object,
ModelAndView modelAndView) throws Exception {
//请求处理后,删除标识
String requestKey =(String) request.getAttribute("ACCESS_KEY");
if(requestKey!=null){
RedisUtils.del(requestKey);
request.removeAttribute("ACCESS_KEY");
}
}
/**
* 在调用controller具体方法前拦截
*/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object object) throws Exception {
String requestUri = request.getRequestURI();
String url = requestUri.substring(request.getContextPath().length());//获取此次请求访问的是哪个接口
String ip=MobileUtil.getIpAddr(request);//获取当前的ip地址值
String token = request.getParameter("TOKEN");//获取用户令牌,合法请求必然携带
//没有携带令牌,说明是非法请求
if(StringUtils.isBlank(token)){
return false;
}
//获取此次请求的用户设备信息,判断是pc端、app、还是公众号端发来的请求,这个需要前端人员在调用接口时设置指定参数
int device = MobileUtil.judgeDevice(request);
if(device==0){//没有设备标识,说明是非法请求
return false;
}
//判断是否是重复请求
String requestKey=device+"_"+ip+"_"+token+"_"+url;
boolean b = RedisUtils.transactSet(requestKey, "1", 5);//设置5秒,只是为了防止拦截器的后置处理方法没执行到(比如突然断电),导致后续的同类请求都不能执行
if(!b){
Map<String,Object> map=new HashMap<>();
map.put("success",false);
map.put("result","请求太频繁,请稍后再试");
PrintWriter out = response.getWriter();
out.print(JSON.toJSONString(map));
out.close();
return false;
}
//如果请求允许,就记住key,请求处理完后,还要删除标识
request.setAttribute("ACCESS_KEY", requestKey);
//此处省略一些校验令牌合法性的操作
return true;
}
}
我的项目用的是SSM架构,在spring-mvc.xml配置文件中需要加上下面这段配置:
<!-- 拦截器 -->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**" />
<bean class="interceptors.UserPermissionInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
MobileUtil类如下:
package util.mobile;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang.StringUtils;
public class MobileUtil {
/**
* 根据自定义的请求头参数判断当前登录的设备
*/
public static int judgeDevice(HttpServletRequest request) {
//登录的设备
String device = request.getHeader("ACCESS-DEVICE");
if(StringUtils.isBlank(device)){
String userAgent = request.getHeader("user-agent");
if(userAgent!=null&&userAgent.toLowerCase().contains("mozilla")){
return 1;//pc端的登录
}else{
return 0;//非法请求
}
}else{
switch (device.toLowerCase()) {
case "wechat_coming_snafu"://微信公众号
return 2;
case "ios_coming_snafu"://ios
return 3;
case "android_coming_snafu"://android
return 4;
default:
return 0;
}
}
}
/**
* 获取请求的客户端的ip地址
*
* @param request
* @return
*/
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
ipAddress = inet.getHostAddress();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15 && ipAddress.indexOf(",") > 0 ) { // "***.***.***.***".length()
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
return ipAddress;
}
}
RedisUtils类详见我的另一篇博客Jedis常用工具类,包含一些具有事务的设置值的方法