Blog from May, 2020

웹 애플리케이션에 Basic Authentication을 적용했을 때 REST 호출시 Basic Authentication이 적용되도록 해야 합니다. Spring의 RestTemplate 에는 Interceptor를 추가할 수 있도록 되어 있어서 다음과 같이 Basic Authentication을 자동으로 할 수 있도록 지원합니다.

RestTemplate restTemplate = new RestTemplate();

restTemplate.getInterceptors().add(new BasicAuthorizationInterceptor("username", "password"));

(tick) 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로 접속해봅니다.