🙏废话不多说系列,直接开整🙏
两分钟内不能重复请求一个接口,类似于防止重复请求。(防止CSRF攻击)
一、防御思路
(1)提交验证码
在表单中添加一个随机的数字或者字母验证码。通过强制用户和应用进行交互。来有效地遏制 CSRF 工具。
(2)Referer Check
检查假设是否正常页面过来的请求,则极有可能时 CSRF 攻击。
(3)token 验证
再 HTTP 请求中以参的形式或者 Header 中添加一个随机产生的 token ,并在服务器端建立一个拦截器来验证这个 token,假设请求中没有 token 或者 token 内容不对,则觉得有可能是 CSRF 攻击而拒绝该请求。
二、实现演示
(1)引入 maven 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
</dependencies>
- Guava 用于内存中存储 token 信息;
- redis 用于存储 token 的中间件。
(2)自定义注解接口
对重要的接口进行注解的标注,需要 token 的校验。
import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* @author drew
*/
@Documented
@Inherited
@Retention(RUNTIME)
@Target({ METHOD, TYPE})
public @interface ApiIdempotent {
}
(3)定义接口抽象 token 存取
public interface TokenStore {
/**
* 存储token
*
* @param token 校验码
*/
void store(String token);
/**
* 判断 token 是否存在
*
* @param token 校验码
* @return true 存在,false 不存在
*/
boolean exists(String token);
}
(4)TokenStore 存取实现类
① Guava 内存存储 token 实现类
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.concurrent.TimeUnit;
@Service("memoryTokenStore")
public class MemoryTokenStore implements TokenStore {
private final Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(Integer.MAX_VALUE)
.expireAfterWrite(2, TimeUnit.MINUTES)
.build();
@Override
public void store(String token) {
cache.put(token, token);
}
@Override
public boolean exists(String token) {
boolean result = cache.getIfPresent(token) != null;
cache.invalidate(token);
return result;
}
}
② Redis 存储 token 实现类
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.ObjectUtils;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
//@Service("redisTokenStore")
public class RedisTokenStore implements TokenStore {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void store(String token) {
stringRedisTemplate.opsForValue().set(token, token, 2, TimeUnit.MINUTES);
}
@Override
public boolean exists(String token) {
Boolean flag = stringRedisTemplate.delete(token);
return !ObjectUtils.isEmpty(flag);
}
}
(5)Token 安全验证拦截器
该拦截器用来拦截指定请求的 token。token 可以从两个地方取,一个是请求参数中,一个是从请求 header 中取。
import com.deelon.data.transfer.aop.ApiIdempotent;
import com.deelon.data.transfer.service.token.TokenStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/**
* token安全验证拦截器
*/
public class MethodIdempotentCheck implements HandlerInterceptor {
Logger logger = LoggerFactory.getLogger(MethodIdempotentCheck.class);
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private TokenStore tokenStore;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
Class<?> clazz = method.getClass();
if (clazz.isAnnotationPresent(ApiIdempotent.class)) {
if (!checkToken(request)) {
failure(response);
return false;
}
} else {
if (method.isAnnotationPresent(ApiIdempotent.class)) {
if (!checkToken(request)) {
failure(response);
return false;
}
}
}
}
return true;
}
private void failure(HttpServletResponse response) throws Exception {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{\"code\": -1, \"message\": \"非法操作\"}");
}
private boolean checkToken(HttpServletRequest request) {
logger.info("验证token");
String token = request.getParameter("csrf-token");
if (token == null || token.length() == 0) {
token = request.getHeader("csrf-token");
}
logger.info("获取token:{}", token);
if (token == null || token.length() == 0) {
return false;
}
return tokenStore.exists(token);
}
}
(6)配置类
import com.deelon.data.transfer.intercepter.MethodIdempotentCheck;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;
/**
* CSRF配置类
*/
@Configuration
public class CsrfConfig implements WebMvcConfigurer {
@Value("${csrf.patterns:}")
List<String> patterns = new ArrayList<>();
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor()).addPathPatterns(patterns);
}
@Bean
public HandlerInterceptor tokenInterceptor() {
return new MethodIdempotentCheck();
}
}
(7)生成 token 接口
该接口用来生成 token ,并保存 如 TokenStore 的具体实现中。
@RestController
@RequestMapping("/csrf/token")
public class TestCrsfTokenEndpoint {
@Resource
private TokenStore tokenStore ;
@GetMapping("/create")
public Object create() {
String token = UUID.randomUUID().toString().replaceAll("-", "") ;
tokenStore.store(token) ;
return token ;
}
}
(8)测试接口
综合了【(7)生成的token接口】 的内容。
import com.deelon.data.transfer.aop.ApiIdempotent;
import com.deelon.data.transfer.service.token.TokenStore;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.UUID;
/**
* 测试防止 CSRF工具的(测试步骤:1请求获取token接口;2请求业务接口)
*/
@RestController
@RequestMapping("/csrf/token")
public class TestCrsfTokenEndpoint {
@Resource
private TokenStore tokenStore;
/**
* 生成 token 的接口
*
* @return token UUID字符串
*/
@GetMapping("/create")
public Object create() {
String token = UUID.randomUUID().toString().replaceAll("-", "");
tokenStore.store(token);
return token;
}
/**
* 业务接口
*
* @param user 用户
* @return 是否成功
*/
@PostMapping("")
@ApiIdempotent
public String save(@RequestBody User user) {
System.out.println(user.toString());
return "success";
}
static class User {
private Integer id;
private String name;
public User() {
}
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
}
在需要做 token 验证的方法上 或者类上添加 @ApiIdempotent 注解即可。【以上就是所有的代码实现了】
(9)测试演示
① 先获取 token
② 调用业务接口
请求头没有携带 csrf-token
总结
缺点:每次请求都需要先获取下 token 接口,才能够正常请求。
🙏至此,非常感谢阅读🙏