웹 애플리케이션에 Basic Authentication을 적용했을 때 REST 호출시 Basic Authentication이 적용되도록 해야 합니다. Spring의 RestTemplate
에는 Interceptor를 추가할 수 있도록 되어 있어서 다음과 같이 Basic Authentication을 자동으로 할 수 있도록 지원합니다.
RestTemplate restTemplate = new RestTemplate(); restTemplate.getInterceptors().add(new BasicAuthorizationInterceptor("username", "password"));
Apache HttpClient에서 Basic Authentication을 사용하려면 https://www.baeldung.com/httpclient-4-basic-authentication을 참고하십시오.
RestTemplate
으로 HTTPS가 적용되어 있는 사이트를 호출하는 경우 인증서에 대한 유효성 검사가 이루어져야 하지만 이를 무시하고 HTTPS로 호출하고자 하는 경우 다음과 같이 코드를 작성할 수 있습니다.
import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.TrustStrategy; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; import javax.net.ssl.SSLContext; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.X509Certificate; public class Tester { public static void main(String[] args) throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException { TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true; SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom() .loadTrustMaterial(null, acceptingTrustStrategy) .build(); SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext); CloseableHttpClient httpClient = HttpClients.custom() .setSSLSocketFactory(csf) .build(); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setHttpClient(httpClient); RestTemplate restTemplate = new RestTemplate(requestFactory); ResponseEntity<String> forEntity = restTemplate.getForEntity("https://www.amazon.com/", String.class); System.out.println(forEntity.getBody()); } }
이 코드가 동작하려면 Maven POM에 다음을 추가해야 합니다.
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.12</version> </dependency>
자기 스스로가 CA가 되어 인증서를 자기 서명을 하여 웹 서버에 배포하는 경우 Self Signed Certification을 허용할 수 있도록 RestTemplate
을 구성할 필요가 있습니다.
import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.TrustSelfSignedStrategy; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; import javax.net.ssl.SSLContext; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; public class Tester { public static void main(String[] args) throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException { TrustSelfSignedStrategy strategy = new TrustSelfSignedStrategy(); // 중요!! SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom() .loadTrustMaterial(null, strategy) .build(); SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext); CloseableHttpClient httpClient = HttpClients.custom() .setSSLSocketFactory(csf) .build(); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setHttpClient(httpClient); RestTemplate restTemplate = new RestTemplate(requestFactory); ResponseEntity<String> forEntity = restTemplate.getForEntity(URL, String.class); } }
Spring JPA 또는 Domain Object를 REST로 서비스를 할 때 클라이언트쪽으로 전달시 JSON으로 변환을 하게 됩니다. JPA의 경우 복합키를 표현하는 경우 다음과 같이 @Embeddable
annotation을 써서 표현할 수 있습니다.
@Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) @Embeddable @NoArgsConstructor public class PayId implements Serializable { /** * 결제번호 (비즈니스적으로 유의미한 번호) */ @EqualsAndHashCode.Include @Column private Long payNumber; @EqualsAndHashCode.Include @Column private Long paySeq; public PayId(Long payNumber, Long paySeq) { this.payNumber = payNumber; this.paySeq = paySeq; } }
그러나 이렇게 사용하는 경우 다음과 같이 Entity에 PK로 정의를 해야 하며 이 경우 JSON으로 변환하면 계층 구조를 갖도록 생성이 됩니다.
@EqualsAndHashCode(onlyExplicitlyIncluded = true) @Entity @NoArgsConstructor public class PayDetailId implements Serializable { @EqualsAndHashCode.Include private PayId payId; @EqualsAndHashCode.Include @Column private Long payDetailId; public PayDetailId(PayId payId, Long payDetailId) { this.payId = payId; this.payDetailId = payDetailId; } }
예를 들면 이렇게 변환이 됩니다. 만약 payNumber
를 payId
아래에 두지 않고 payId
와 같은 레벨에 두고자 한다면 @JsonUnwrapped
annotation이 제격입니다.
{ "payId" : { "payNumber": 100, "paySeq" : 5000 }, "payDetailId": 1 }
다음과 같이 펼쳐서 전송하고자 하는 경우 다음과 같이 @JsonUnwrapped
annotation을 추가합니다.
@EqualsAndHashCode(onlyExplicitlyIncluded = true) @Entity @NoArgsConstructor public class PayDetailId implements Serializable { @EqualsAndHashCode.Include @JsonUnwrapped private PayId payId; @EqualsAndHashCode.Include @Column private Long payDetailId; public PayDetailId(PayId payId, Long payDetailId) { this.payId = payId; this.payDetailId = payDetailId; } }
그러면 아래와 같이 자동 생성됩니다.
{ "payNumber": 100, "paySeq": 5000, "payDetailId": 1 }
다음은 Spring Data REST를 사용하기 위해서 Domain을 하나 생성한 예제입니다.
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; import org.springframework.data.rest.core.annotation.RepositoryRestResource; import org.springframework.data.rest.core.annotation.RestResource; import org.springframework.web.bind.annotation.CrossOrigin; @CrossOrigin @RepositoryRestResource(collectionResourceRel = "users", path = "users") public interface UserRepository extends CrudRepository<WebsiteUser, Long> { @Override @RestResource(exported = false) void delete(WebsiteUser entity); @Override @RestResource(exported = false) void deleteAll(); @Override @RestResource(exported = false) void deleteAll(Iterable<? extends WebsiteUser> entities); @Override @RestResource(exported = false) void deleteById(Long aLong); @RestResource(path = "byEmail", rel = "customFindMethod") WebsiteUser findByEmail(@Param("email") String email); }
Spring Data REST에서는 Domain Class에 대해서 CRUD를 수행하는 CrudRepository
를 구현했을 때 자동으로 REST API를 생성합니다. 이때 특정한 HTTP Method만 금지시키고자 하는 경우 다음과 같이 코드를 작성할 수 있습니다.
import org.springframework.context.annotation.Configuration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.ExposureConfiguration; import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer; @Configuration public class RestConfig implements RepositoryRestConfigurer { @Override public void configureRepositoryRestConfiguration(RepositoryRestConfiguration repositoryRestConfiguration) { ExposureConfiguration config = repositoryRestConfiguration.getExposureConfiguration(); config.forDomainType(WebsiteUser.class).withItemExposure((metadata, httpMethods) -> httpMethods.disable(HttpMethod.PATCH)); } }
HAL Browser, HAL Explorer에 대해서
Spring Data REST에서는 기존에 HAL Browser를 제공하였으나 deprecated 상태이며 대체로 HAL Explorer를 사용하도록 권고하고 있습니다.
Spring Data REST를 사용할 때 개발을 편리하게 하기 위해서 HAL Explorer를 사용할 수 있습니다. HAL Explorer를 Maven POM에 다음을 추가합니다.
<dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-rest-hal-explorer</artifactId> <version>3.3.0.RELEASE</version> </dependency>
Spring Boot Application을 실행하고 /로 접근하면 다음의 화면을 볼 수 있습니다.
이 이슈는 보통 Spring Data REST에 Swagger를 같이 사용하고자 하는 경우 발생합니다.
Spring Data REST에 Swagger를 추가하면 spring-plugin-core-1.2.0.RELEASE.jar
파일에 대한 충돌 문제가 발생합니다. 이 문제는 Swagger의 버전이 낮아서 생기는 문제이긴 하나 현재 Swagger 3.0 Snapshot 버전을 통해서 해결할 수 있습니다.
우선 이 문제를 해결하기 위해서 Maven POM에 다음의 Repository를 추가합니다.
<repositories> <repository> <id>jcenter-snapshots</id> <name>jcenter</name> <url>http://oss.jfrog.org/artifactory/oss-snapshot-local/</url> </repository> </repositories>
그리고 난 후 다음과 같이 Swagger Snapshot 버전을 Maven POM에 추가합니다.
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>3.0.0-SNAPSHOT</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>3.0.0-SNAPSHOT</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-data-rest</artifactId> <version>3.0.0-SNAPSHOT</version> </dependency>
이제 Spring Boot Configuration에 다음을 추가합니다. 참고로 버전업이 되면서 Annotation 명이 @EnableSwagger2WebMvc
으로 변경되었습니다.
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.data.rest.configuration.SpringDataRestConfiguration; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc; @Configuration @EnableSwagger2WebMvc @Import(SpringDataRestConfiguration.class) public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2).select().apis(RequestHandlerSelectors.any()) .paths(PathSelectors.any()).build(); } }
이제 Spring Boot Application을 실행하고 http://localhost:8080/swagger-ui.html로 접속해봅니다.