스프링 REST + 스프링 보안 예제

89284 단어 springspringboot

프로젝트 디렉토리





메이븐



스프링 보안을 위한 spring-boot-starter-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] ------------------------------------------------------------------------


스프링 컨트롤러



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);
    }
}


Bean 유효성 검사(Hibernate 유효성 검사기)


  • 어떤JSR-303 구현(예: Hibernate Validator)이 클래스 경로에서 사용 가능한 경우 빈 유효성 검사가 자동으로 활성화됩니다. 기본적으로 Spring Boot는 Hibernate Validator를 자동으로 가져와 다운로드합니다.
  • 아래의 POST 요청이 전달됩니다. 이름, 저자 및 가격과 같은 필드가 비어 있지 않은지 확인하기 위해 book 개체에 대한 bean 유효성 검사를 구현해야 합니다.

  •     @PostMapping("/books")
        @ResponseStatus(HttpStatus.CREATED)
        Book newBook(@Valid @RequestBody Book newBook) {
            return repository.save(newBook);
        }
    


    javax.validation.constraints.* 주석으로 빈에 주석을 답니다.
  • 책.자바

  • 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);
        }
    }
    


    REST 끝점에 POST 요청을 다시 보내십시오. 누락된 데이터 필드로 인해 Bean 유효성 검사가 실패하면 MethodArgumentNotValidException이 트리거됩니다. 기본적으로 Spring은 HTTP 상태 400 잘못된 요청을 다시 보내지만 오류 세부 정보는 보내지 않습니다.

    위의 오류 응답은 친숙하지 않습니다. 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());
        }
    
        //..
    }
    




    맞춤 검사기



    작성자 필드에 대한 사용자 지정 유효성 검사기를 만들어 4명의 작성자만 데이터베이스에 저장할 수 있도록 합니다.
  • 작성자.자바

  • 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);
        }
    }
    


  • 책.자바

  • 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."]
    }
    


    스프링 시큐리티



    새 @Configuration 클래스를 만들고 WebSecurityConfigurerAdapter를 확장합니다. 아래 예에서는 HTTP 기본 인증을 사용하여 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();
        }
    }
    


    스프링 부트



    REST 끝점을 시작하고 데모를 위해 H2 데이터베이스에 3권의 책을 삽입하는 일반적인 Spring Boot 애플리케이션입니다.

    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을 반환하고 모든 끝점이 보호되며 인증이 필요합니다.

  • user 로그인과 함께 GET 요청을 보냅니다.

  • '사용자' 로그인으로 POST 요청을 보내려고 하면 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

    좋은 웹페이지 즐겨찾기