Защита конечных точек Spring Boot Rest с помощью Google reCAPTCHA и AOP
Вы можете найти код на github:
Что такое reCAPTCHA?
reCAPTCHA — это бесплатный сервис, который защищает ваш сайт от спама и злоупотреблений. Он использует передовые методы анализа рисков, чтобы отличить людей от ботов.
Давайте рассмотрим следующий сценарий:
У вас есть интернет-магазин, в котором пользователи могут создать учетную запись, указав основную информацию (имя, адрес электронной почты и телефон). После этого шага они получат SMS для подтверждения своего номера телефона, а затем перейдут на главную страницу.
Проблема
Процесс создания отправляет SMS для проверки пользователя. Если в вашем API отсутствует защита от спама, любой может просто злоупотребить им, а поскольку вы (вероятно) используете платного провайдера SMS, это может заставить вас платить большие счета своему провайдеру или просто заблокировать вашу систему SMS, чтобы никто другой не мог зарегистрироваться.
Решение
Просто используйте reCAPTCHA! Этот сервис попросит пользователя доказать, что он не робот, выполнив несколько простых задач для человека (но сложных для роботов), таких как визуальные (определение определенных аспектов на изображениях) или аудио (распознавание речи).
Получение ключей API
Чтобы использовать эту услугу, вам понадобится учетная запись Google, затем просто перейдите по ссылке: , выберите reCAPTCHA v2, отметьте опцию «Я не робот», введите домен(ы) вашего сайта, после чего вы сможете посмотрите ваши 2 ключа: ключ клиента и ключ сервера (вы должны держать его в секрете).
Серверная часть с Spring Boot
Мы будем использовать следующие зависимости maven:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.3.1</version>
</dependency>
</dependencies>
Давайте создадим следующую конечную точку REST (POST): которая будет получать запрос с именем пользователя и любезно его приветствовать.
@RestController
@RequestMapping("/hello")
public class HelloController {
@PostMapping
public String hello(@RequestBody HelloDTO helloDTO){
return new HelloResponseDTO("Hello, " + helloDTO.getName() + "!");
}
}
Давайте проверим это!
curl -d '{"name":"Chris"}' -H "Content-Type: application/json" -X POST
Мы получаем следующий ответ: {‘message’:’Hello, Chris!’}
Добавление защиты reCAPTCHA
В нашем файле «application.properties» мы храним закрытый ключ сервера:
google.recaptcha.secret=SERVER_KEY
Давайте создадим CaptchaValidator:
@Service
public class CaptchaValidator {
private static final String GOOGLE_RECAPTCHA_ENDPOINT = "
@Value("${google.recaptcha.secret}")
private String recaptchaSecret;
public boolean validateCaptcha(String captchaResponse){
RestTemplate restTemplate = new RestTemplate();
MultiValueMap<String, String> requestMap = new LinkedMultiValueMap<>();
requestMap.add("secret", recaptchaSecret);
requestMap.add("response", captchaResponse);
CaptchaResponse apiResponse = restTemplate.postForObject(GOOGLE_RECAPTCHA_ENDPOINT, requestMap, CaptchaResponse.class);
if(apiResponse == null){
return false;
}
return Boolean.TRUE.equals(apiResponse.getSuccess());
}
}
Это ответ API (геттеры и сеттеры опущены для ясности):
public class CaptchaResponse {
private Boolean success;
private Date timestamp;
private String hostname;
@JsonProperty("error-codes")
private List<String> errorCodes;
}
В приведенном выше примере кода мы могли видеть, что мы вводим наш закрытый ключ сервера в CaptchaValidator, а затем отправляем запрос POST в REST API Google с нашим секретом и ответом клиента на капчу.
Мы получаем ответ от Google, содержащий поле (среди прочего) под названием «успех», которое мы можем использовать для проверки правильности ответа клиента на капчу. Если это неверно, мы можем посмотреть коды ошибок, чтобы понять, почему.
Изменение исходной конечной точки для добавления логики CaptchaValidator
Добавляем captchaResponse в тело запроса:
public class HelloDTO {
private String name;
private String captchaResponse;
}
@PostMapping
public String hello(@RequestBody HelloDTO helloDTO){
Boolean isValidCaptcha = captchaService.validateCaptcha(helloDTO.getCaptchaResponse());
if(!isValidCaptcha){
throw new ForbiddenException("Captcha is not valid");
}
return new HelloResponseDTO("Hello, " +helloDTO.getName() +"!");
}
Мы больше не можем тестировать его с помощью CURL, так как теперь он требует проверки по капче, поэтому нам нужно закодировать клиент, чтобы он снова заработал.
Клиентская сторона с Jquery
<html>
<head>
<title>Spring Boot reCAPTCHA with AOP</title>
<script type="text/javascript"
src="webjars/jquery/3.3.1/jquery.min.js"></script>
<script src="
</head>
<body>
<script>
$(document).ready(function () {
$("#button").click(function () {
var captchaResponse = grecaptcha.getResponse();
var name = $("#name").val();
var helloRequest = {
'name': name,
'captchaResponse': captchaResponse
};
$.ajax({
type: "POST",
contentType: 'application/json',
dataType: "json",
data: JSON.stringify(helloRequest),
url: "",
success: function (data) {
alert(data.message);
}
});
});
});
</script>
<div>
<input type="text" id="name"/>
<button type="submit" id="button">Hello</button>
<div class="g-recaptcha" data-sitekey="CLIENT_KEY"></div>
</div>
</body>
</html>
Я выбрал jQuery, потому что могу предоставить пример с 1 файлом, но есть библиотеки для AngularJs, Angular 2+, React, и даже если вы не можете найти библиотеку для своего внешнего интерфейса, вы все равно можете просто использовать простой javascript.
Приведенный выше код приводит к этой простой странице, которая вызывает нашу серверную конечную точку hello с именем и captchaResponse в качестве тела запроса:
Если мы завершим имя, отметим галочкой «Я не робот» и нажмем на кнопку, мы получим предупреждение, приветствующее нас!
Мы только что защитили нашу конечную точку REST с помощью reCAPTCHA, это здорово!
Одна проблема, хотя…
Если мы хотим добавить проверку reCAPTCHA на любую другую конечную точку REST, нам нужно будет выполнить следующие шаги:
Включите поле в тело запроса для получения ответа на капчу.
Добавьте логику в наш контроллер или сервис для проверки капчи
Кажется довольно простым, но это довольно повторяется. А как насчет GET-запросов? У нас не может быть тела на тех…
АОП в помощь!
АОП (аспектно-ориентированное программирование) позволяет нам запускать некоторый код до (или после) вызова одного из наших методов.
Мы собираемся внести следующие изменения в наш код:
Переместите ответ клиента captchaResponse из тела запроса в заголовок http (чтобы быть более общим, а также иметь возможность включать его для запросов, которые не могут иметь тела)
Реализуйте аннотацию АОП, чтобы избавиться от шаблонного кода и получить более чистый код.
Модифицируем клиент, чтобы он отправлял ответ по капче в заголовке, а не в теле:
<script>
$(document).ready(function () {
$("#button").click(function () {
var captchaResponse = grecaptcha.getResponse();
var name = $("#name").val();
var helloRequest = {
'name': name
};
$.ajax({
type: "POST",
contentType: 'application/json',
dataType: "json",
headers: {
"captcha-response": captchaResponse
},
data: JSON.stringify(helloRequest),
url: "",
success: function (data) {
alert(data.message);
}
});
});
});
</script>
2. Создайте аннотацию AOP, чтобы более изящно обрабатывать каптику.
а. Создайте аннотацию
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresCaptcha {
}
б. Создайте аспект
@Aspect
@Component
public class CaptchaAspect {
@Autowired
private CaptchaValidator captchaValidator;
private static final String CAPTCHA_HEADER_NAME = "captcha-response";
@Around("@annotation(RequiresCaptcha)")
public Object validateCaptcha(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String captchaResponse = request.getHeader(CAPTCHA_HEADER_NAME);
boolean isValidCaptcha = captchaValidator.validateCaptcha(captchaResponse);
if(!isValidCaptcha){
throw new ForbiddenException("Invalid captcha");
}
return joinPoint.proceed();
}
}
Основываясь на приведенном выше коде, мы видим, что логика проверки капчи теперь находится в нашем аспекте.
Некоторые пояснения:
@Around("@annotation(RequiresCaptcha)")
Это говорит Spring запустить наш код до вызова метода, аннотированного «@RequiresCaptcha».
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String captchaResponse = request.getHeader(CAPTCHA_HEADER_NAME);
boolean isValidCaptcha = captchaValidator.validateCaptcha(captchaResponse);
if(!isValidCaptcha){
throw new ForbiddenException("Invalid captcha");
}
Этот код извлекает заголовок капчи из текущего HttpServletRequest и отправляет его валидатору для проверки.
Если все в порядке, будет выполнена следующая строка:
return joinPoint.proceed();
Это вызовет исходный метод, который вернет приветствие клиенту. Если капча недействительна, будет возбуждено исключение.
Вернемся к нашему контроллеру
@PostMapping
@RequiresCaptcha
public HelloResponseDTO hello(@RequestBody HelloDTO helloDTO){
return new HelloResponseDTO("Hello, " + helloDTO.getName() + "!");
}
Как мы видим, мы удалили шаблонный код для проверки проверки капчи и вместо этого добавили аннотацию «@RequiresCaptcha».
Теперь мы можем использовать эту аннотацию всякий раз, когда захотим добавить проверку по капче в одну из наших конечных точек REST, и это здорово!
Вы можете увидеть полный код: