Каталог проектов
Maven
Включите spring-boot-starter-security для Spring Security
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springrestsecurity</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-rest-security</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Зависимости проекта :
> mvn dependency:tree
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------< com.example:springrestsecurity >-------------------
[INFO] Building spring-rest-security 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:3.3.0:tree (default-cli) @ springrestsecurity ---
[INFO] com.example:springrestsecurity:jar:0.0.1-SNAPSHOT
[INFO] +- org.springframework.boot:spring-boot-starter-thymeleaf:jar:2.7.1:compile
[INFO] | +- org.springframework.boot:spring-boot-starter:jar:2.7.1:compile
[INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:2.7.1:compile
[INFO] | | | +- ch.qos.logback:logback-classic:jar:1.2.11:compile
[INFO] | | | | - ch.qos.logback:logback-core:jar:1.2.11:compile
[INFO] | | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.17.2:compile
[INFO] | | | | - org.apache.logging.log4j:log4j-api:jar:2.17.2:compile
[INFO] | | | - org.slf4j:jul-to-slf4j:jar:1.7.36:compile
[INFO] | | +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile
[INFO] | | - org.yaml:snakeyaml:jar:1.30:compile
[INFO] | +- org.thymeleaf:thymeleaf-spring5:jar:3.0.15.RELEASE:compile
[INFO] | | +- org.thymeleaf:thymeleaf:jar:3.0.15.RELEASE:compile
[INFO] | | | +- org.attoparser:attoparser:jar:2.0.5.RELEASE:compile
[INFO] | | | - org.unbescape:unbescape:jar:1.1.6.RELEASE:compile
[INFO] | | - org.slf4j:slf4j-api:jar:1.7.36:compile
[INFO] | - org.thymeleaf.extras:thymeleaf-extras-java8time:jar:3.0.4.RELEASE:compile
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.7.1:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-json:jar:2.7.1:compile
[INFO] | | +- com.fasterxml.jackson.core:jackson-databind:jar:2.13.3:compile
[INFO] | | | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.13.3:compile
[INFO] | | | - com.fasterxml.jackson.core:jackson-core:jar:2.13.3:compile
[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.13.3:compile
[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.13.3:compile
[INFO] | | - com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.13.3:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.7.1:compile
[INFO] | | +- org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.64:compile
[INFO] | | +- org.apache.tomcat.embed:tomcat-embed-el:jar:9.0.64:compile
[INFO] | | - org.apache.tomcat.embed:tomcat-embed-websocket:jar:9.0.64:compile
[INFO] | +- org.springframework:spring-web:jar:5.3.21:compile
[INFO] | | - org.springframework:spring-beans:jar:5.3.21:compile
[INFO] | - org.springframework:spring-webmvc:jar:5.3.21:compile
[INFO] | +- org.springframework:spring-context:jar:5.3.21:compile
[INFO] | - org.springframework:spring-expression:jar:5.3.21:compile
[INFO] +- org.springframework.boot:spring-boot-configuration-processor:jar:2.7.1:compile
[INFO] +- org.projectlombok:lombok:jar:1.18.24:compile
[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:2.7.1:test
[INFO] | +- org.springframework.boot:spring-boot-test:jar:2.7.1:test
[INFO] | +- org.springframework.boot:spring-boot-test-autoconfigure:jar:2.7.1:test
[INFO] | +- com.jayway.jsonpath:json-path:jar:2.7.0:test
[INFO] | | - net.minidev:json-smart:jar:2.4.8:test
[INFO] | | - net.minidev:accessors-smart:jar:2.4.8:test
[INFO] | | - org.ow2.asm:asm:jar:9.1:test
[INFO] | +- jakarta.xml.bind:jakarta.xml.bind-api:jar:2.3.3:compile
[INFO] | | - jakarta.activation:jakarta.activation-api:jar:1.2.2:compile
[INFO] | +- org.assertj:assertj-core:jar:3.22.0:test
[INFO] | +- org.hamcrest:hamcrest:jar:2.2:test
[INFO] | +- org.junit.jupiter:junit-jupiter:jar:5.8.2:test
[INFO] | | +- org.junit.jupiter:junit-jupiter-api:jar:5.8.2:test
[INFO] | | | +- org.opentest4j:opentest4j:jar:1.2.0:test
[INFO] | | | +- org.junit.platform:junit-platform-commons:jar:1.8.2:test
[INFO] | | | - org.apiguardian:apiguardian-api:jar:1.1.2:test
[INFO] | | +- org.junit.jupiter:junit-jupiter-params:jar:5.8.2:test
[INFO] | | - org.junit.jupiter:junit-jupiter-engine:jar:5.8.2:test
[INFO] | | - org.junit.platform:junit-platform-engine:jar:1.8.2:test
[INFO] | +- org.mockito:mockito-core:jar:4.5.1:test
[INFO] | | +- net.bytebuddy:byte-buddy:jar:1.12.11:compile
[INFO] | | +- net.bytebuddy:byte-buddy-agent:jar:1.12.11:test
[INFO] | | - org.objenesis:objenesis:jar:3.2:test
[INFO] | +- org.mockito:mockito-junit-jupiter:jar:4.5.1:test
[INFO] | +- org.skyscreamer:jsonassert:jar:1.5.0:test
[INFO] | | - com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test
[INFO] | +- org.springframework:spring-core:jar:5.3.21:compile
[INFO] | | - org.springframework:spring-jcl:jar:5.3.21:compile
[INFO] | +- org.springframework:spring-test:jar:5.3.21:test
[INFO] | - org.xmlunit:xmlunit-core:jar:2.9.0:test
[INFO] +- org.springframework.boot:spring-boot-starter-security:jar:2.7.1:compile
[INFO] | +- org.springframework:spring-aop:jar:5.3.21:compile
[INFO] | +- org.springframework.security:spring-security-config:jar:5.7.2:compile
[INFO] | - org.springframework.security:spring-security-web:jar:5.7.2:compile
[INFO] +- org.springframework.security:spring-security-test:jar:5.7.2:test
[INFO] | - org.springframework.security:spring-security-core:jar:5.7.2:compile
[INFO] | - org.springframework.security:spring-security-crypto:jar:5.7.2:compile
[INFO] +- org.springframework.boot:spring-boot-starter-data-jpa:jar:2.7.1:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-aop:jar:2.7.1:compile
[INFO] | | - org.aspectj:aspectjweaver:jar:1.9.7:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-jdbc:jar:2.7.1:compile
[INFO] | | +- com.zaxxer:HikariCP:jar:4.0.3:compile
[INFO] | | - org.springframework:spring-jdbc:jar:5.3.21:compile
[INFO] | +- jakarta.transaction:jakarta.transaction-api:jar:1.3.3:compile
[INFO] | +- jakarta.persistence:jakarta.persistence-api:jar:2.2.3:compile
[INFO] | +- org.hibernate:hibernate-core:jar:5.6.9.Final:compile
[INFO] | | +- org.jboss.logging:jboss-logging:jar:3.4.3.Final:compile
[INFO] | | +- antlr:antlr:jar:2.7.7:compile
[INFO] | | +- org.jboss:jandex:jar:2.4.2.Final:compile
[INFO] | | +- com.fasterxml:classmate:jar:1.5.1:compile
[INFO] | | +- org.hibernate.common:hibernate-commons-annotations:jar:5.1.2.Final:compile
[INFO] | | - org.glassfish.jaxb:jaxb-runtime:jar:2.3.6:compile
[INFO] | | +- org.glassfish.jaxb:txw2:jar:2.3.6:compile
[INFO] | | +- com.sun.istack:istack-commons-runtime:jar:3.0.12:compile
[INFO] | | - com.sun.activation:jakarta.activation:jar:1.2.2:runtime
[INFO] | +- org.springframework.data:spring-data-jpa:jar:2.7.1:compile
[INFO] | | +- org.springframework.data:spring-data-commons:jar:2.7.1:compile
[INFO] | | +- org.springframework:spring-orm:jar:5.3.21:compile
[INFO] | | - org.springframework:spring-tx:jar:5.3.21:compile
[INFO] | - org.springframework:spring-aspects:jar:5.3.21:compile
[INFO] +- com.h2database:h2:jar:2.1.214:compile
[INFO] +- org.springframework.boot:spring-boot-devtools:jar:2.7.1:compile
[INFO] | +- org.springframework.boot:spring-boot:jar:2.7.1:compile
[INFO] | - org.springframework.boot:spring-boot-autoconfigure:jar:2.7.1:compile
[INFO] - javax.validation:validation-api:jar:2.0.1.Final:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.451 s
[INFO] Finished at: 2022-07-18T10:22:10+07:00
[INFO] ------------------------------------------------------------------------
Spring Controller
Просмотрите еще раз контроллер Book Controller, позже мы интегрируем его с Spring Security для защиты конечных точек REST.
package com.example.springrestsecurity;
import com.example.springrestsecurity.error.BookNotFoundException;
import com.example.springrestsecurity.error.BookUnSupportedFieldPatchException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import java.util.List;
import java.util.Map;
@RestController
@Validated
public class BookController {
@Autowired
private BookRepository repository;
@GetMapping("/books")
List<Book> findAll() {
return repository.findAll();
}
@PostMapping("/books")
@ResponseStatus(HttpStatus.CREATED)
Book newBook(@Valid @RequestBody Book newBook) {
return repository.save(newBook);
}
@GetMapping("/books/{id}")
Book findOne(@PathVariable @Min(1) Long id) {
return repository.findById(id)
.orElseThrow(() -> new BookNotFoundException(id));
}
@PutMapping("/books/{id}")
Book saveOrUpdate(@RequestBody Book newBook, @PathVariable Long id) {
return repository.findById(id)
.map(x -> {
x.setName(newBook.getName());
x.setAuthor(newBook.getAuthor());
x.setPrice(newBook.getPrice());
return repository.save(x);
})
.orElseGet(() -> {
newBook.setId(id);
return repository.save(newBook);
});
}
@PatchMapping("/books/{id}")
Book patch(@RequestBody Map<String, String> update, @PathVariable Long id) {
return repository.findById(id)
.map(x -> {
String author = update.get("author");
if (!StringUtils.isEmpty(author)) {
x.setAuthor(author);
return repository.save(x);
} else {
throw new BookUnSupportedFieldPatchException(update.keySet());
}
})
.orElseGet(() -> {
throw new BookNotFoundException(id);
});
}
@DeleteMapping("/books/{id}")
void deleteBook(@PathVariable Long id) {
repository.deleteById(id);
}
}
Валидация бинов (Hibernate Validator)
-
Валидация бобов будет включена автоматически, если в пути класса доступна какая-либо реализация JSR-303 (например, Hibernate Validator). По умолчанию Spring Boot получит и загрузит Hibernate Validator автоматически.
-
Ниже будет передан POST-запрос, нам нужно реализовать валидацию bean на объекте книги, чтобы убедиться, что такие поля, как название, автор и цена не пусты.
@PostMapping("/books")
@ResponseStatus(HttpStatus.CREATED)
Book newBook(@Valid @RequestBody Book newBook) {
return repository.save(newBook);
}
Аннотируйте боб с помощью аннотаций javax.validation.constraints.*.
- Book.java
package com.example.springrestsecurity;
import com.example.springrestsecurity.error.validator.Author;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@Entity
public class Book {
@Id
@GeneratedValue
private Long id;
@NotEmpty(message = "Please provide a name")
private String name;
@Author
@NotEmpty(message = "Please provide a author")
private String author;
@NotNull(message = "Please provide a price")
@DecimalMin("1.00")
private BigDecimal price;
public Book() {
}
public Book(Long id, String name, String author, BigDecimal price) {
this.id = id;
this.name = name;
this.author = author;
this.price = price;
}
public Book(String name, String author, BigDecimal price) {
this.name = name;
this.author = author;
this.price = price;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", name='" + name + ''' +
", author='" + author + ''' +
", price=" + price +
'}';
}
}
Добавьте @Valid в @RequestBody. Готово, теперь валидация боба включена.
- BookController.java
package com.example.springrestsecurity;
import com.example.springrestsecurity.error.BookNotFoundException;
import com.example.springrestsecurity.error.BookUnSupportedFieldPatchException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import java.util.List;
import java.util.Map;
@RestController
@Validated
public class BookController {
@Autowired
private BookRepository repository;
@GetMapping("/books")
List<Book> findAll() {
return repository.findAll();
}
@PostMapping("/books")
@ResponseStatus(HttpStatus.CREATED)
Book newBook(@Valid @RequestBody Book newBook) {
return repository.save(newBook);
}
@GetMapping("/books/{id}")
Book findOne(@PathVariable @Min(1) Long id) {
return repository.findById(id)
.orElseThrow(() -> new BookNotFoundException(id));
}
@PutMapping("/books/{id}")
Book saveOrUpdate(@RequestBody Book newBook, @PathVariable Long id) {
return repository.findById(id)
.map(x -> {
x.setName(newBook.getName());
x.setAuthor(newBook.getAuthor());
x.setPrice(newBook.getPrice());
return repository.save(x);
})
.orElseGet(() -> {
newBook.setId(id);
return repository.save(newBook);
});
}
@PatchMapping("/books/{id}")
Book patch(@RequestBody Map<String, String> update, @PathVariable Long id) {
return repository.findById(id)
.map(x -> {
String author = update.get("author");
if (!StringUtils.isEmpty(author)) {
x.setAuthor(author);
// better create a custom method to update a value = :newValue where id = :id
return repository.save(x);
} else {
throw new BookUnSupportedFieldPatchException(update.keySet());
}
})
.orElseGet(() -> {
throw new BookNotFoundException(id);
});
}
@DeleteMapping("/books/{id}")
void deleteBook(@PathVariable Long id) {
repository.deleteById(id);
}
}
Попробуйте снова отправить POST-запрос на конечную точку REST. Если валидация боба не пройдет из-за отсутствия полей данных, это вызовет исключение MethodArgumentNotValidException. По умолчанию Spring отправит обратно HTTP-статус 400 Bad Request, но без подробного описания ошибки.
Приведенный выше ответ на ошибку не является дружественным, мы можем поймать MethodArgumentNotValidException и переопределить ответ следующим образом:
- CustomGlobalExceptionHandler.java
package com.example.springrestsecurity.error;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
// Let Spring BasicErrorController handle the exception, we just override the status code
@ExceptionHandler(BookNotFoundException.class)
public void springHandleNotFound(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.NOT_FOUND.value());
}
@ExceptionHandler(BookUnSupportedFieldPatchException.class)
public void springUnSupportedFieldPatch(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.METHOD_NOT_ALLOWED.value());
}
// @Validate For Validating Path Variables and Request Parameters
@ExceptionHandler(ConstraintViolationException.class)
public void constraintViolationException(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.BAD_REQUEST.value());
}
// error handle for @Valid
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", new Date());
body.put("status", status.value());
//Get all errors
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(x -> x.getDefaultMessage())
.collect(Collectors.toList());
body.put("errors", errors);
return new ResponseEntity<>(body, headers, status);
}
}
Валидация переменных пути
- Мы также можем применить аннотации javax.validation.constraints.* к переменной пути или даже непосредственно к параметру запроса.
- Примените @Validated на уровне класса и добавьте аннотации javax.validation.constraints.* к переменным пути, как показано ниже:
BookController.java
package com.example.springrestsecurity;
import com.example.springrestsecurity.error.BookNotFoundException;
import com.example.springrestsecurity.error.BookUnSupportedFieldPatchException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import java.util.List;
import java.util.Map;
@RestController
@Validated
public class BookController {
@GetMapping("/books/{id}")
Book findOne(@PathVariable @Min(1) Long id) { //jsr 303 annotations
return repository.findById(id)
.orElseThrow(() -> new BookNotFoundException(id));
}
//...
}
Сообщение об ошибке по умолчанию хорошее, просто код ошибки 500 не подходит.
Если @Validated не сработает, это вызовет ConstraintViolationException, мы можем переопределить код ошибки следующим образом:
- CustomGlobalExceptionHandler.java
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import java.io.IOException;
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(ConstraintViolationException.class)
public void constraintViolationException(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.BAD_REQUEST.value());
}
//..
}
Пользовательский валидатор
Мы создадим пользовательский валидатор для поля author, позволяющий сохранять в базе данных только 4 авторов.
- Author.java
package com.example.springrestsecurity.error.validator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = AuthorValidator.class)
@Documented
public @interface Author {
String message() default "Author is not allowed.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- AuthorValidator.java
package com.example.springrestsecurity.error.validator;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.List;
public class AuthorValidator implements ConstraintValidator<Author, String> {
List<String> authors = Arrays.asList("Santideva", "Marie Kondo", "Martin Fowler", "toptech");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return authors.contains(value);
}
}
- Book.java
package com.example.springrestsecurity;
import com.example.springrestsecurity.error.validator.Author;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@Entity
public class Book {
@Id
@GeneratedValue
private Long id;
@NotEmpty(message = "Please provide a name")
private String name;
@Author
@NotEmpty(message = "Please provide a author")
private String author;
@NotNull(message = "Please provide a price")
@DecimalMin("1.00")
private BigDecimal price;
public Book() {
}
public Book(Long id, String name, String author, BigDecimal price) {
this.id = id;
this.name = name;
this.author = author;
this.price = price;
}
public Book(String name, String author, BigDecimal price) {
this.name = name;
this.author = author;
this.price = price;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", name='" + name + ''' +
", author='" + author + ''' +
", price=" + price +
'}';
}
}
Проверьте это. Если пользовательский валидатор не сработает, то сработает исключение MethodArgumentNotValidException
curl -v -X POST localhost:8080/books
-H "Content-type:application/json"
-d "{"name":"Spring REST tutorials", "author":"abc","price":"9.99"}"
{
"timestamp":"2019-02-20T13:49:59.971+0000",
"status":400,
"errors":["Author is not allowed."]
}
Безопасность Spring
Создайте новый класс @Configuration и расширьте его WebSecurityConfigurerAdapter. В приведенном ниже примере мы будем использовать аутентификацию HTTP Basic для защиты конечных точек REST.
- SpringSecurityConfig.java
package com.example.springrestsecurity.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("{noop}password").roles("USER")
.and()
.withUser("admin").password("{noop}password").roles("USER", "ADMIN");
}
// Secure the endpoins with HTTP Basic authentication
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/books/**").hasRole("USER")
.antMatchers(HttpMethod.POST, "/books").hasRole("ADMIN")
.antMatchers(HttpMethod.PUT, "/books/**").hasRole("ADMIN")
.antMatchers(HttpMethod.PATCH, "/books/**").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE, "/books/**").hasRole("ADMIN")
.and()
.csrf().disable()
.formLogin().disable();
}
}
Spring Boot
Обычное приложение Spring Boot для запуска конечных точек REST и вставки 3 книг в базу данных H2 для демонстрации.
package com.example.springrestsecurity;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import java.math.BigDecimal;
@SpringBootApplication
public class SpringRestSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringRestSecurityApplication.class, args);
}
@Profile("demo")
@Bean
CommandLineRunner initDatabase(BookRepository repository) {
return args -> {
repository.save(new Book("A Guide to the Bodhisattva Way of Life", "Santideva", new BigDecimal("15.41")));
repository.save(new Book("The Life-Changing Magic of Tidying Up", "Marie Kondo", new BigDecimal("9.69")));
repository.save(new Book("Refactoring: Improving the Design of Existing Code", "Martin Fowler", new BigDecimal("47.99")));
};
}
}
Демо
- Обычные GET и POST вернут 401, все конечные точки защищены, требуется аутентификация.
- Отправьте GET-запрос с логином
user
. - Попробуйте отправить POST-запрос с логином ‘user’, он вернет ошибку 403, Forbidden. Это происходит потому, что пользователь не имеет права отправлять POST-запрос.
Просмотрите конфигурацию Spring Security еще раз. Чтобы отправить POST, PUT, PATCH или DELETE запрос, нам нужен администратор
- SpringSecurityConfig.java
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/books/**").hasRole("USER")
.antMatchers(HttpMethod.POST, "/books").hasRole("ADMIN")
.antMatchers(HttpMethod.PUT, "/books/**").hasRole("ADMIN")
.antMatchers(HttpMethod.PATCH, "/books/**").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE, "/books/**").hasRole("ADMIN")
.and()
.csrf().disable()
.formLogin().disable();
}
}
- Попытайтесь отправить POST-запрос с логином администратора
Исходный код
https://github.com/java-cake/spring-boot/tree/main/springrestsecurity