1 CSRF
攻击产生的原因
CSRF
(Cross Site Request Forgery,跨站域请求伪造
)是一种网络的攻击方式,它在 2007 年曾被列为互联网 20 大安全隐患之一,也被称为“One Click Attack”或者Session Riding,通常缩写为 CSRF 或者XSRF,是一种对网站的恶意利用也就是人们所知道的钓鱼网站。尽管听起来像跨站脚本(XSS
),但它与 XSS
非常不同,并且攻击方式几乎相左。XSS
利用站点内的信任用户,而 CSRF
则通过伪装来自受信任用户的请求来利用受信任的网站。与 XSS
攻击相比,CSRF
攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比 XSS
更具危险性。
2 API
接口幂等性设计
2.1 API
接口幂等性设计方案
方案一:MVCC 方案
多版本并发控制,该策略主要使用 update with condition
(更新带条件来防止)来保证多次外部请求调用对系统的影响是一致的。
在系统设计的过程中,合理的使用乐观锁,通过 version
或者 updateTime
(timestamp)等其他条件,来做乐观锁的判断条件,这样保证更新操作即使在并发的情况下,也不会有太大的问题。
例如:
// 取出要跟新的对象,带有版本 versoin
select * from tablename where condition=#condition#
update tableName set name=#name#,version=version+1 where version=#version#
在更新的过程中利用 version
来防止,其他操作对对象的并发更新,导致更新丢失。为了避免失败,通常需要一定的重试机制。
方案二:去重表
在插入数据的时候,插入去重表,利用数据库的唯一索引特性,保证唯一的逻辑。
方案三:悲观锁
select for update
,整个执行过程中锁定该订单对应的记录。
注意:这种在 DB 读大于写的情况下尽量少用。
方案四:Token 机制,防止页面重复提交
业务要求:
页面的数据只能被点击提交一次
发生原因:
由于重复点击或者网络重发,或者 nginx
重发等情况会导致数据被重复提交
解决办法:
- 集群环境:采用
token
加redis
(redis 单线程的,处理需要排队) - 单 JVM 环境:采用
token
加redis
或token
加jvm
内存
处理流程:
数据提交前要向服务的申请 token,token 放到 redis 或 jvm 内存,token 有效时间,提交后后台校验 token,同时删除 token,生成新的 token 返回。
token
特点:要申请,一次有效性,可以限流。
2.2 基于 Token
方式防止 API
接口幂等
客户端每次在调用接口的时候,需要在请求头中,传递令牌参数,每次令牌只能用一次。
一旦使用之后,就会被删除,这样可以有效防止重复提交。
步骤:
- 生成令牌接口
- 接口中获取令牌验证
使用场景:
在调用 API 接口的时候,需要传递令牌,该 Api 接口 获取到令牌之后,执行当前业务逻辑,然后把当前的令牌删除掉。
2.2.1 生成令牌接口
package com.snow.utils;
import java.lang.annotation.Retention;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang.StringUtils;
/**
* Token 工具类
*/
public class TokenUtils {
// 这里为了方便,使用 Map 集合代替缓存
private static Map<String, Object> tokenMaps = new ConcurrentHashMap<String, Object>();
// 代码步骤:
// 1.获取令牌
// 2.判断令牌是否在缓存中有对应的数据
// 3.如何缓存没有该令牌的话,直接报错(请勿重复提交)
// 4.如何缓存有该令牌的话,直接执行该业务逻辑
// 5.执行完业务逻辑之后,直接删除该令牌。
/**
* 获取令牌
*
* @return
*/
public static synchronized String getToken() {
// 如果在分布式场景下需要使用分布式全局ID实现
String token = "token" + System.currentTimeMillis();
// hashMap好处可以附带
tokenMaps.put(token, token);
return token;
}
/**
* 查找令牌
*
* @param tokenKey
* @return
*/
public static boolean findToken(String tokenKey) {
// 判断该令牌是否在tokenMap 是否存在
String token = (String) tokenMaps.get(tokenKey);
if (StringUtils.isEmpty(token)) {
return false;
}
// token 获取成功后 删除对应tokenMaps里面的token
tokenMaps.remove(token);
return true;
}
}
2.2.2 接口中获取令牌验证
package com.snow.controller;
import com.snow.entity.OrderEntity;
import com.snow.mapper.OrderMapper;
import com.snow.utils.TokenUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
public class OrderController {
@Autowired
private OrderMapper orderMapper;
@RequestMapping("/getToken")
public String getToken() {
return TokenUtils.getToken();
}
// 验证Token
@RequestMapping(value = "/addOrder", produces = "application/json; charset=utf-8")
public String addOrder(@RequestBody OrderEntity orderEntity, HttpServletRequest request) {
// 代码步骤:
// 1.获取令牌 存放在请求头中
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)) {
return "参数错误!";
}
// 2.判断令牌是否在缓存中有对应的令牌
// 3.如何缓存没有该令牌的话,直接报错(请勿重复提交)
// 4.如何缓存有该令牌的话,直接执行该业务逻辑
// 5.执行完业务逻辑之后,直接删除该令牌。
if (!TokenUtils.findToken(token)) {
return "请勿重复提交!";
}
int result = orderMapper.addOrder(orderEntity);
return result > 0 ? "添加成功" : "添加失败" + "";
}
}
测试:
获取 token:
2.3 防御 CSRF
攻击手段
如何防止机器模拟请求?
使用图形验证码防止机器模拟接口请求攻击,在调用核心业务接口时,比如支付、下单、等接口,最好使用手机短信验证验证或者是人脸识别,防止其他用户使用Token伪造请求。