ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] Spring Boot에서 아임포트 다날 본인인증 연동(Rest Api)
    Spring 2023. 9. 21. 23:44
    반응형

    서버에서 본인인증을 처리하는 방법은 여러가지가 있다.

    1) 이메일을 통한 본인인증

    2) SMS를 통한 본인인증

    3) PG사 제공 본인인증 서비스 이용

     

    그 중에 3번 즉, iamport의 다날 PG사를 이용한 본인인증 서비스를 Spring Boot에서 Rest Api 방식으로 연동하는 방법을 작성해본다.

     

    개발 환경:

    💡 Spring Boot 2.5.5

    💡 Java 1.8

    💡 JPA

    💡 Maven

    💡 WebView Target: Unity

     

    📌 아임포트 관리자 콘솔에서 가맹점 식별코드와 Api Key, Api Secret Key를 발급 받았다는 전제하에 진행한다.

     

    먼저, 동작 순서는 아래와 같다.

    1. 클라이언트에서 '본인인증하기' 클릭시 서버에 웹뷰 인증값을 요청

    2. 서버에서 인증토큰(인증값 UUID, 만료일시)를 생성하여 리턴

    3. 리턴 받은 인증토큰을 담아 서버에 웹뷰 페이지 요청

    4. 클라이언트가 인증에 성공하면 아임포트 서버에서 imp_uid(랜덤값)를 응답 받는다.

    5. 서버에 imp_uid를 담아서 본인인증 결과 조회 API를 호출한다.


    📌 시작하기

     

    POM.XML 추가

    - JAVA 사용자를 위한 아임포트 REST API 연동 모듈 추가

    <repositories>
        <repository>
            <id>jitpack.io</id>
            <url>https://jitpack.io</url>
        </repository>
    </repositories>
    <dependency>
        <groupId>com.github.iamport</groupId>
        <artifactId>iamport-rest-client-java</artifactId>
        <version>0.2.14</version>
    </dependency>

    인증토큰을 담을 CertificationValues.Class 생성

    - certificationValue: 랜덤 UUID 값

    - expiredDt: 인증 유효일시

    @Entity @Table(name = "CERTIFICATION_VALUES")
    @Getter @Setter
    public class CertificationValues {
    
        @Id
        @Column
        private String certificationValue;
    
        @Column
        private Date expiredDt;
    
    }

     

    application.properties에 api key, api secret key 설정

    #import key
    project.import.rest-api-key=[Your Api Key]
    project.import.rest-api-secret-key=[Your Api Secret Key]

    1. WebView 인증값 조회 API 작성

    - @ApiOperation, @ApiResponses는 Swagger용 Annotation이며 본인의 프로젝트에 필요시 사용하면 된다.

    - @RestController 대신 @Controller와 @ResponseBody를 쓴 이유는 웹뷰 페이지를 Return하는 API도 있기 때문이다.
       (테스트 프로젝트이기 때문, 본인의 프로젝트에선 Controller를 나눠서 사용하면 된다.)

     

    ImportRestController.class

    import com.siot.IamportRestClient.IamportClient;
    import com.siot.IamportRestClient.exception.IamportResponseException;
    import io.swagger.annotations.*;
    import org.springframework.beans.factory.annotation.Value;
    ...
    
    
    @Slf4j
    @Api(tags = {"Import API"})
    @Controller
    @RequiredArgsConstructor
    @RequestMapping("api/fo/import")
    public class ImportRestController {
    
        @Value("${project.import.rest-api-key}")
        private String api_key;
    
        @Value("${project.import.rest-api-secret-key}")
        private String api_secret;
    
        private final ImportRestService importRestService;
    
        @ResponseBody
        @GetMapping("certification-value")
        @ApiOperation("웹뷰 인증 값 조회")
        @ApiResponses({
                @ApiResponse(code = SwaggerApiStatus.SUCCESS_CODE, message = SwaggerApiStatus.SUCCESS_MESSAGE, response = String.class)
        })
        public String getCertificationValue() {
            return importRestService.getCertificationValue();
        }

    - @Value Annotation으로 properties에 있는 key 값을 주입한다.

     

     

    ImportRestService.class

    @Transactional(readOnly = true)
    @RequiredArgsConstructor
    @Service
    public class ImportRestService {
    
        private final CertificationValuesRepository certificationValuesRepository;
        
        @Transactional(rollbackFor = Exception.class)
        public String getCertificationValue() {
            CertificationValues certificationValues = new CertificationValues();
            String certificationVal = UUID.randomUUID().toString().replace("-", "");
            Date now = new Date();
    
            certificationValues.setCertificationValue(certificationVal);
            certificationValues.setExpiredDt(new Date(now.getTime() + 2L * 60L * 1000L));
    
            certificationValuesRepository.save(certificationValues);
            return certificationVal;
        }

    - 인증 값은 randomUUID 값을 설정한다.

    - 인증 만료 시간은 2분으로 설정한다.

    - database에 해당 인증 정보를 저장한다.


    2. 아임포트 본인인증 웹뷰 페이지 조회 API

    BaseResDto.class

    @Getter @Setter
    public class BaseResDto {
        private int resultCode = ResultCode.SUCCESS.getResultCode();
        private String resultMessage = ResultCode.SUCCESS.getResultMessage();
    }

    - Error Code와 Message를 저장할 DTO를 생성한다.

     

     

     

    ResultCode.class

    public enum ResultCode {
        SUCCESS(200, ResultMessage.SUCCESS),
        UNAUTHORIZED(401, ResultMessage.UNAUTHORIZED),
        INTERNAL_ERROR(500, ResultMessage.INTERNAL_ERROR),
        ACCESS_NO_AUTH(1_000, ResultMessage.ACCESS_NO_AUTH),
        CERTIFICATION_NOT_EXIST(1_001, ResultMessage.CERTIFICATION_NOT_EXIST),
        CERTIFICATION_VALUE_NOT_EXIST(1_002, ResultMessage.CERTIFICATION_VALUE_NOT_EXIST),
        VALID_NOT_CERTIFICATION_VALUE(1_003, ResultMessage.VALID_NOT_CERTIFICATION_VALUE),
        CERTIFICATION_VALUE_EXPIRED(1_004, ResultMessage.CERTIFICATION_VALUE_EXPIRED),
        ;
    
        private final int resultCode;
        private final String resultMessage;
    
        ResultCode(int resultCode, String resultMessage) {
            this.resultCode = resultCode;
            this.resultMessage = resultMessage;
        }
    
        public int getResultCode() {
            return resultCode;
        }
    
        public String getResultMessage() {
            return resultMessage;
        }
    
        public interface ResultMessage {
            String SUCCESS = "완료 되었습니다.";
            String UNAUTHORIZED = "인증에 실패하였습니다.";
            String ACCESS_NO_AUTH = "접근 권한이 없습니다.";
    
            String CERTIFICATION_NOT_EXIST = "해당 본인인증 내역이 존재하지 않습니다.";
            String CERTIFICATION_VALUE_NOT_EXIST = "존재하지 않는 인증 값 입니다.";
            String VALID_NOT_CERTIFICATION_VALUE = "유효하지 않은 인증 값 입니다.";
            String CERTIFICATION_VALUE_EXPIRED = "인증 값이 만료 되었습니다.";
            String INTERNAL_ERROR = "시스템 오류가 발생하였습니다. 다시 시도해주세요.";
        }
    }

    - Error Code, Message를 저장해 놓은 Enum Class이다.

    - 에러 발생시 Http 500 에러가 아닌 Http 200으로 응답 하고 에러 처리를 해주기 위함이다.
    - 해당 포스팅에선 Exception을 정의해서 Handling을 하진 않고 단순 Return 목적으로만 사용한다.
    - Exception을 정의해서 Glabal Exception Handling을 원한다면 해당 포스팅을 참고하길 바란다.

     

     

    ImportRestController.class

    @ApiOperation("아임포트 본인인증 웹뷰 페이지 조회")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "certificationVal", value = "text", required = true, paramType = "query")
    })
    @ApiResponses({
            @ApiResponse(code = SwaggerApiStatus.SUCCESS_CODE, message = SwaggerApiStatus.SUCCESS_MESSAGE, response = String.class)
    })
    @GetMapping("certification-page")
    public String getImportWebView(@RequestParam String certificationVal, Model model) {
        return importRestService.getImportWebView(certificationVal, model);
    }

    - 인증 토큰 값을 parameter로 받는다.

     

     

    ImportRestService.class

    @Transactional(rollbackFor = Exception.class)
    public String getImportWebView(String certificationVal, Model model) {
    	BaseResDto baseResDto = new BaseResDto();
    
        if (StringUtils.isBlank(certificationVal)) {
            baseResDto.setResultCode(ResultCode.ACCESS_NO_AUTH.getResultCode());
            baseResDto.setResultMessage(ResultCode.ACCESS_NO_AUTH.getResultMessage());
            model.addAttribute("baseResDto", baseResDto);
    
            return "importError";
        }
    
        Optional<CertificationValues> certificationValues = certificationValuesRepository.findById(certificationVal);
    
        if (!certificationValues.isPresent()) {
            baseResDto.setResultCode(ResultCode.CERTIFICATION_VALUE_NOT_EXIST.getResultCode());
            baseResDto.setResultMessage(ResultCode.CERTIFICATION_VALUE_NOT_EXIST.getResultMessage());
            model.addAttribute("baseResDto", baseResDto);
    
            return "importError";
        }
    
        if (!new Date().before(certificationValues.get().getExpiredDt())) {
            baseResDto.setResultCode(ResultCode.CERTIFICATION_VALUE_EXPIRED.getResultCode());
            baseResDto.setResultMessage(ResultCode.CERTIFICATION_VALUE_EXPIRED.getResultMessage());
            model.addAttribute("baseResDto", baseResDto);
    
            return "importError";
        }
    
        certificationValuesRepository.deleteById(certificationValues.get().getCertificationValue());
    
        return "importWebView";
    }

    - parameter로 전달 받은 certificationValueValidation 하는 Method이다.

    - 예외 케이스는 3가지로 분기 했다. 케이스는 아래와 같다.

     

    1. 전달 받은 인증 값이 null이나 빈 값일때 (Controller 단에서 @Valid Annotation으로 처리할 수도 있다.)

    2. Database에 해당 인증 값이 존재하지 않을때

    3. 조회한 인증 정보가 만료 됐을때

     

    케이스를 통과하지 못했다면 Error 페이지에 error 정보를 담아 return한다.

    케이스를 통과했다면 Database에서 일회성 인증 정보를 삭제하고 Webview 페이지를 return 한다.


    3. 아임포트 본인인증 결과 조회 API

     

    CertificationResDto.class

    import com.siot.IamportRestClient.response.Certification;
    import com.siot.IamportRestClient.response.IamportResponse;
    import com.project.common.dto.BaseResDto;
    import lombok.Getter;
    import lombok.Setter;
    
    @Getter @Setter
    public class CertificationResDto extends BaseResDto {
        private IamportResponse<Certification> certificationResponse;
    }

    - 본인인증 결과를 담을 Dto를 생성한다.

     

     

    ImportRestController.class

    @ResponseBody
    @ApiOperation("아임포트 본인인증 결과 조회")
    @ApiResponses({
            @ApiResponse(code = SwaggerApiStatus.SUCCESS_CODE, message = SwaggerApiStatus.SUCCESS_MESSAGE, response = BaseResDto.class)
    })
    @PostMapping("certificate")
    public BaseResDto certificate(@RequestBody String impUid) {
        CertificationResDto certificationResDto = new CertificationResDto();
    
        try {
            IamportClient client = new IamportClient(imp_key, imp_secret);
            certificationResDto.setCertificationResponse(client.certificationByImpUid(impUid.substring(8)));
    
            return certificationResDto;
        } catch (IamportResponseException e) {
            log.error(e.getMessage());
    
            switch (e.getHttpStatusCode()) {
                case 401 :
                    // 401 토큰 만료
                    certificationResDto.setResultCode(ResultCode.UNAUTHORIZED.getResultCode());
                    certificationResDto.setResultMessage(ResultCode.UNAUTHORIZED.getResultMessage());
                    break;
                case 404 :
                    // impUid에 해당하는 본인인증 내역이 존재 하지 않음
                    certificationResDto.setResultCode(ResultCode.CERTIFICATION_NOT_EXIST.getResultCode());
                    certificationResDto.setResultMessage(ResultCode.CERTIFICATION_NOT_EXIST.getResultMessage());
                    break;
                case 500 :
                    // 서버 응답 오류
                    certificationResDto.setResultCode(ResultCode.INTERNAL_ERROR.getResultCode());
                    certificationResDto.setResultMessage(ResultCode.INTERNAL_ERROR.getResultMessage());
                    break;
            }
    
            return certificationResDto;
        } catch (IOException e) {
            // 서버 연결 실패
            e.printStackTrace();
    
            certificationResDto.setResultCode(ResultCode.INTERNAL_ERROR.getResultCode());
            certificationResDto.setResultMessage(ResultCode.INTERNAL_ERROR.getResultMessage());
            return certificationResDto;
        }
    }

    - 공식 가이드엔 나와 있지 않았는데 ImportClient Class를 까보면 apiKeyapiSecret를 parameter로 받는 생성자가 있다.
       가이드에 나와 있는 복잡한 방식이 아닌 해당 방법으로 쉽게 ImportClient를 생성 할 수 있다!

    - WebView 페이지에서 사용자가 본인인증에 성공하면 impUid 값을 return 받을 수 있으며,
      이 값을 해당 API에 Parameter로 넘긴 상태이다.
    - ImportClientcertificationByImpUid() Method에 impUid 값을 넣어서 호출하면 인증한 사용자의 정보를 조회할 수 있다.

     

     

    참고로, 아임포트 본인인증 결과 조회 API 에서는 받을 수 있는 정보가 한정 되어 있다.

    추가적인 정보까지 조회를 원한다면 콘솔에서 요청을 해야 한다.

     

     

     

     

    Reference

    http://gnujava.com/board/article_view.jsp?&article_no=400&menu_cd=71&board_no=49&table_cd=EPAR11&table_no=11

    https://docs.iamport.kr/tech/mobile-authentication

    https://api.iamport.kr/#!/certifications/getCertification

    https://github.com/iamport/iamport-rest-client-java

    반응형

    댓글

Designed by Tistory.