「Java 开发实例」SpringBoot 防止 CSRF 攻击

83 阅读3分钟

🙏废话不多说系列,直接开整🙏

cat008.png

两分钟内不能重复请求一个接口,类似于防止重复请求。(防止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>
  1. Guava 用于内存中存储 token 信息;
  2. 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

image.png

② 调用业务接口

请求头没有携带 csrf-token

image.png

请求头中携带 csrf-token,再次进行请求 ,成功返回

image.png

总结

缺点:每次请求都需要先获取下 token 接口,才能够正常请求。


🙏至此,非常感谢阅读🙏

cat008.png