突如、WebMoneyのAPIが、SSLPeerUnverifiedExceptionを投げ始めた。

TLS1.2に移行したということです。 (WebMoneyは、仕様を公開してないので、詳しいことは言えないが)

すでに知られていることですが、java7は、デフォルトではTLS1.2をサポートしていません。 TLS1.2限定のサーバにリクエストを送信したら、以下のようなExceptionがスローされました。

javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
        at sun.security.ssl.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:421)
        at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:128)
        at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:397)
        at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:148)
        at org.apache.http.impl.conn.AbstractPoolEntry.open(AbstractPoolEntry.java:149)
        at org.apache.http.impl.conn.AbstractPooledConnAdapter.open(AbstractPooledConnAdapter.java:121)
        at org.apache.http.impl.client.DefaultRequestDirector.tryConnect(DefaultRequestDirector.java:573)
        at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:425)
        at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:820)
        at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:754)
        at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:732)

TSL1.2の必須化は、IT業界では世界的な動きとしてあり、有名な話だけど、なぜか、本件は、アナウンスが届かなかった。 何も情報がなかったので、サーバ証明書の問題かなと思ってしまいました。

TLS1.2に、対応するには、いくつか方法があります。

  1. デフォルトで1.2をサポートしているから、java8以上にする。可能ならそうしよう。もう7はEOLだろう?
  2. JVMの引数に、-Djdk.tls.client.protocols=TLSv1.1,TLSv1.2,,TLSv1.2-Dhttps.protocols=TLSv1.1,TLSv1.2,TLSv1.3をつけて実行しよう。
  3. これで駄目なケースもあるようだ。プログラムの修正が必要。

私の場合、3のパターンで。 Apache CommonsのHttpClientを使用しているので、以下のような修正をしました。

            SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
            sslContext.init(null, null, new SecureRandom());
            SSLSocketFactory sf = new SSLSocketFactory(sslContext);
            Scheme httpsScheme = new Scheme("https",  443, sf);
            SchemeRegistry schemeRegistry = new SchemeRegistry();
            schemeRegistry.register(httpsScheme);
            ClientConnectionManager cm = new SingleClientConnManager(schemeRegistry);
            DefaultHttpClient client = new DefaultHttpClient(cm);

HttpClientのバージョンよっては、こちらの修正方法になります。 というかググるとこればかり出てきます。

SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
  SSLContexts.createDefault(),
  new String[] { "TLSv1.2", "TLSv1.3" },
  null,
  SSLConnectionSocketFactory.getDefaultHostnameVerifier());

CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();

よく見かけるこのやり方は、おそらく全体的に適用されるので、気をつける必要があると思う。

            SSLContext ctx = SSLContext.getInstance("TLSv1.2");
            ctx.init(null, null, null);
            SSLContext.setDefault(ctx);

PayPay Open Payment API(OPA)の突合ファイルをOpenCSVで読み込んでみた。

PayPay OPAでは、前日の取引データなどが、突合ファイルとして生成され、HTTP GETで取得することができます。 こちらのPayPayのディベロッパサイトに、sampleの突合ファイルがありますので、取得して読み込んでみました。

Web Cashier - PayPay Open Payment API Documentation

読み込みに使用した突合ファイル: transaction_000000000000008181_20200130000000_20200130235959.csv

決済番号,加盟店ID,屋号,店舗ID,店舗名,端末番号/PosID,取引ステータス,取引日時,取引金額,レシート番号,支払い方法,マーチャント決済ID
00000000000000000001,000000000000008181,テスト加盟店,test01,テスト01,00001,取引完了,"2020-01-30 23:58:30",150,000-0001,PayPay残高,0001-001
00000000000000000002,000000000000008181,テスト加盟店,test01,テスト01,00001,返金完了,"2020-01-30 23:55:14",-300,000-0002,PayPay残高,0001-002
00000000000000000003,000000000000008181,テスト加盟店,test01,テスト01,00001,取引完了,"2020-01-30 23:49:54",100,000-0003,PayPay残高,0001-003
00000000000000000004,000000000000008181,テスト加盟店,test01,テスト01,00001,取引完了,"2020-01-30 23:47:09",100,000-0004,PayPay残高,0001-004
00000000000000000005,000000000000008181,テスト加盟店,test01,テスト01,00001,取引完了,"2020-01-30 23:45:11",200,000-0005,PayPay残高,0001-005

突合ファイルは、CSV形式だということで、OSSライブラリで読み込みたいと思います。

opencsv –

CSVのライブラリは、いくつかありますが、多くの導入実績があり、 今後も開発が継続していく可能性が高そうな、OpenCSVをチョイスしました。

OpenCSVは、直接、CSVファイルのレコードとJava Beanクラスをバインディングする機能があります。 クラスのフィールド変数にアノテーションを付与して、カラムとの対応付けを定義します。

    /**
     * 突合ファイルのレコードクラス.
     */
    public class ReconciliationRecord implements Serializable {

        private static final long serialVersionUID = 1L;
        /**
         * 決済番号(paymentId).
         */
        @CsvBindByName(column = "決済番号")
        private String orderId;
        /**
         * 加盟店ID.
         */
        @CsvBindByName(column = "加盟店ID")
        private String merchantId;

        /**
         * 屋号.
         */
        @CsvBindByName(column = "屋号")
        private String brandName;
        /**
         * 店舗ID.
         */
        @CsvBindByName(column = "店舗ID")
        private String storeId;
        /**
         * 店舗名.
         */
        @CsvBindByName(column = "店舗名")
        private String storeName;
        /**
         * 端末番号/PosID.
         */
        @CsvBindByName(column = "端末番号/PosID")
        private String terminalId;
        /**
         * 取引ステータス.
         */
        @CsvCustomBindByName(column = "取引ステータス", converter = StatusConverter.class)
        private Status transactionStatus;
        /**
         * 取引日時.
         */
        @CsvBindByName(column = "取引日時")
        @CsvDate("yyyy-MM-dd HH:mm:ss")
        private Date acceptedAt;
        /**
         * 取引金額.
         */
        @CsvBindByName(column = "取引金額")
        private Long amount;
        /**
         * レシート番号.
         */
        @CsvBindByName(column = "レシート番号")
        private String orderReceiptNumber;
        /**
         * 支払い方法.
         */
        @CsvBindByName(column = "支払い方法")
        private String methodOfPayment;
        /**
         * マーチャント決済ID.
         */
        @CsvBindByName(column = "マーチャント決済ID")
        private String merchantPaymentId;

        public ReconciliationRecord() {
        }

        @Override
        public String toString() {
            return new StringJoiner(", ", ReconciliationRecord.class.getSimpleName() + "[", "]")
                    .add("orderId='" + orderId + "'")
                    .add("merchantId='" + merchantId + "'")
                    .add("brandName='" + brandName + "'")
                    .add("storeId='" + storeId + "'")
                    .add("storeName='" + storeName + "'")
                    .add("terminalId='" + terminalId + "'")
                    .add("transactionStatus=" + transactionStatus)
                    .add("acceptedAt=" + acceptedAt)
                    .add("amount=" + amount)
                    .add("orderReceiptNumber='" + orderReceiptNumber + "'")
                    .add("methodOfPayment='" + methodOfPayment + "'")
                    .add("merchantPaymentId='" + merchantPaymentId + "'")
                    .toString();
        }

        public String getOrderId() {
            return orderId;
        }

        public void setOrderId(String orderId) {
            this.orderId = orderId;
        }

        public String getMerchantId() {
            return merchantId;
        }

        public void setMerchantId(String merchantId) {
            this.merchantId = merchantId;
        }

        public String getBrandName() {
            return brandName;
        }

        public void setBrandName(String brandName) {
            this.brandName = brandName;
        }

        public String getStoreId() {
            return storeId;
        }

        public void setStoreId(String storeId) {
            this.storeId = storeId;
        }

        public String getStoreName() {
            return storeName;
        }

        public void setStoreName(String storeName) {
            this.storeName = storeName;
        }

        public String getTerminalId() {
            return terminalId;
        }

        public void setTerminalId(String terminalId) {
            this.terminalId = terminalId;
        }

        public Status getTransactionStatus() {
            return transactionStatus;
        }

        public void setTransactionStatus(Status transactionStatus) {
            this.transactionStatus = transactionStatus;
        }

        public Date getAcceptedAt() {
            return acceptedAt;
        }

        public void setAcceptedAt(Date acceptedAt) {
            this.acceptedAt = acceptedAt;
        }

        public Long getAmount() {
            return amount;
        }

        public void setAmount(Long amount) {
            this.amount = amount;
        }

        public String getOrderReceiptNumber() {
            return orderReceiptNumber;
        }

        public void setOrderReceiptNumber(String orderReceiptNumber) {
            this.orderReceiptNumber = orderReceiptNumber;
        }

        public String getMethodOfPayment() {
            return methodOfPayment;
        }

        public void setMethodOfPayment(String methodOfPayment) {
            this.methodOfPayment = methodOfPayment;
        }

        public String getMerchantPaymentId() {
            return merchantPaymentId;
        }

        public void setMerchantPaymentId(String merchantPaymentId) {
            this.merchantPaymentId = merchantPaymentId;
        }

        enum Status {

            COMPLETED, FAILED, REFUND_COMPLETED, REFUND_FAILED, UNKNOWN;

            private static final String STRING_COMPLETED = "取引完了";
            private static final String STRING_FAILED = "取引失敗";
            private static final String STRING_REFUND_COMPLETED = "返金完了";
            private static final String STRING_REFUND_FAILED = "返金失敗";

            static Status statusOf(final String value) {
                Status status = null;
                switch (value) {
                    case STRING_COMPLETED:
                        status = COMPLETED;
                        break;
                    case STRING_FAILED:
                        status = FAILED;
                        break;
                    case STRING_REFUND_COMPLETED:
                        status = REFUND_COMPLETED;
                        break;
                    case STRING_REFUND_FAILED:
                        status = REFUND_FAILED;
                        break;
                    default:
                        status = UNKNOWN;
                        break;

                }
                return status;
            }
        }
    }

固定値をenum型に変換したかったので、OpenCSVのAbstractBeanFieldクラスを継承して変換処理を実装しています。 そして、この変換処理を適用したいフィールド変数に対して、 @CsvCustomBindByName(column = "取引ステータス", converter = StatusConverter.class)というように指定します。

    /**
     * 取引ステータスをString<->enumに変換します.
     */
    public class StatusConverter extends AbstractBeanField {

        public StatusConverter() {
            super();
        }

        @Override
        protected ReconciliationRecord.Status convert(String value)
                throws CsvDataTypeMismatchException, CsvConstraintViolationException {
            return ReconciliationRecord.Status.statusOf(value);
        }
    }

特に、PayPayのサイトには、記載されてないが、ファイルのエンコーディングは、SHIFT_JISっぽいですね。 csvToBeanクラスを生成して、突合ファイルを読み込みしてみます。

File file = new File("/PATH/TO/transaction_000000000000008181_20200130000000_20200130235959.csv");

try (Reader reader = new InputStreamReader(new FileInputStream(file), "SHIFT_JIS")) {

            CsvToBean<ReconciliationRecord> csvToBean = new CsvToBeanBuilder(reader).withType(ReconciliationRecord.class).build();

            csvToBean.stream().parallel().forEach(new Consumer<ReconciliationRecord>() {

                @Override
                public void accept(ReconciliationRecord reconciliationRecord) {
                    log.debug(reconciliationRecord);
                }

            });
        }

結果

ReconciliationRecord[orderId='00000000000000000001', merchantId='000000000000008181', brandName='テスト加盟店', storeId='test01', storeName='テスト01', terminalId='00001', transactionStatus=COMPLETED, acceptedAt=Thu Jan 30 23:58:30 JST 2020, amount=150, orderReceiptNumber='000-0001', methodOfPayment='PayPay残高', merchantPaymentId='0001-001']
ReconciliationRecord[orderId='00000000000000000002', merchantId='000000000000008181', brandName='テスト加盟店', storeId='test01', storeName='テスト01', terminalId='00001', transactionStatus=REFUND_COMPLETED, acceptedAt=Thu Jan 30 23:55:14 JST 2020, amount=-300, orderReceiptNumber='000-0002', methodOfPayment='PayPay残高', merchantPaymentId='0001-002']
ReconciliationRecord[orderId='00000000000000000003', merchantId='000000000000008181', brandName='テスト加盟店', storeId='test01', storeName='テスト01', terminalId='00001', transactionStatus=COMPLETED, acceptedAt=Thu Jan 30 23:49:54 JST 2020, amount=100, orderReceiptNumber='000-0003', methodOfPayment='PayPay残高', merchantPaymentId='0001-003']
ReconciliationRecord[orderId='00000000000000000004', merchantId='000000000000008181', brandName='テスト加盟店', storeId='test01', storeName='テスト01', terminalId='00001', transactionStatus=COMPLETED, acceptedAt=Thu Jan 30 23:47:09 JST 2020, amount=100, orderReceiptNumber='000-0004', methodOfPayment='PayPay残高', merchantPaymentId='0001-004']
ReconciliationRecord[orderId='00000000000000000005', merchantId='000000000000008181', brandName='テスト加盟店', storeId='test01', storeName='テスト01', terminalId='00001', transactionStatus=COMPLETED, acceptedAt=Thu Jan 30 23:45:11 JST 2020, amount=200, orderReceiptNumber='000-0005', methodOfPayment='PayPay残高', merchantPaymentId='0001-005']

ふと、これを見て、acceptedAtは、タイムゾーンまで含んでいない。 JSTであってるだろうかという疑問がでてきました。

他のAPIの日時パラメタは、エポックタイムスタンプだったり、協定世界時 (UTC) だったりするので、UTCかもしれないと思いつつ、 ファイルのエンコーディングSHIFT_JISっぽいし、カラム名が日本語だし、JSTの可能性も否めない。

そこで、別の種類の突合ファイルをPayPayのディベロッパサイトからダウンロードして見てみたところ。

preauth_transaction_000000000000008181_20200130000000_20200130235959.csv

"orderId","merchantId","brandName","storeId","storeName","terminalId","transactionStatus","acceptedAt","amount","orderReceiptNumber","methodOfPayment","merchantPaymentId"
"3456789012345678901","234567890123456789","〇〇加盟店","01234567","","0000","COMPLETED","2020-09-23T00:00:26+09:00","480","","wallet","1447142183_202009230000250894_2"
"3456789012345678902","234567890123456789","〇〇加盟店","01234567","","0000","CANCELED","2020-09-23T07:45:03+09:00","1848","","wallet","1480206255_202009230745030768_6"
"3455979365068570624","234567890123456789","〇〇加盟店","01234567","","0000","EXPIRED","2020-09-23T13:00:00+09:00","1738","","wallet","1000014012_202009302301040213_0"
"3456423834054230016","234567890123456789","〇〇加盟店","01234567","","0000","FAILED","2020-09-23T13:24:54+09:00","20","","wallet","pp_0cb694dc-5340-44ef-a173-6034de590bc5"
"3456789012345678903","234567890123456789","〇〇加盟店","01234567","","0000","REFUNDED","2020-09-23T16:53:25+09:00","1325","","wallet","1000191023_202008261653230283_4"
"3456789012345678904","234567890123456789","〇〇加盟店","01234567","","0000","AUTHORIZED","2020-09-23T23:31:43+09:00","2924","","wallet","1489917170_202009232331310185_0"

こっちは、+09:00がついています。 ファイル名からみると、1/30のデータっぽいけど、レコードは、9/23です。 レコードが、1/30なら確定的だったけど、これは参考にならないかもしれない。

しかしながら、単純に、+09:00を落としただけなんじゃないかという気が、、、

PayPay Open Payment API(OPA)とSpring Framework(旧)の相性問題

github.com

PayPay決済のSDKを追加したところ、Tomcatが起動しないトラブルが発生しました。

org.springframework.beans.factory.BeanDefinitionStoreException: Failed to read candidate component class: file [************.class]; nested exception is java.lang.ArrayIndexOutOfBoundsException: 3145
    at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.findCandidateComponents(ClassPathScanningCandidateComponentProvider.java:237)
    at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:204)
    at org.springframework.context.annotation.ComponentScanBeanDefinitionParser.parse(ComponentScanBeanDefinitionParser.java:84)
    at org.springframework.beans.factory.xml.NamespaceHandlerSupport.parse(NamespaceHandlerSupport.java:73)
    at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1335)
    at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1325)
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:135)
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:93)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.registerBeanDefinitions(XmlBeanDefinitionReader.java:493)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:390)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:334)
    at org.springframework.beans.

原因としては、古いバージョンのSpringフレームワークが、Java8以降の新しい言語機能を使用して作られたものを解釈できないということ。

www.infoq.com

そして、PayPayのSDKが依存しているライブラリhibernate-validatorの中で、Stream APIが使われているということ。

githubの履歴をさかのぼって調べてみると、hibernate-validatorのバージョン6以降から、使われ始めている気がする。

f:id:ryu-htn:20210221210406p:plain

そこで、以下のように、PayPay SDKの依存関係からhibernate-validatorを除外して、改めて、バージョン5系のhibernate-validatorを追加することで、エラーにならずに、Tomcatが起動しました。

でも、バージョン下げて、正常に動くのかどうか、それが怖いですね。

        <dependency>
            <groupId>jp.ne.paypay</groupId>
            <artifactId>paypayopa</artifactId>
            <version>1.0.1</version>
           <exclusions>
                <exclusion>
                    <groupId>org.hibernate.validator</groupId>
                    <artifactId>hibernate-validator</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.hibernate.validator</groupId>
                    <artifactId>
                        hibernate-validator-annotation-processor
                    </artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <!-- <groupId>org.hibernate.validator</groupId> -->
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>5.3.4.Final</version>

        </dependency>

        <dependency>
            <!-- <groupId>org.hibernate.validator</groupId> -->
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator-annotation-processor</artifactId>
            <version>5.3.4.Final</version>
        </dependency>

追記 正常に動かないことがわかりました。 hibernate-validator 5.3.4.Finalにはないクラスを使用しているので、実行時に、ClassNotFoundExceptionが発生します。 正攻法でいくなら、Spring Frameworkのバージョンアップ、または、APIを独自実装するしかありません。 しかし、あくまで裏の手でいきたいなら、PayPay決済のSDKを修正してしまうこともできます。 hibernate-validatorのSDK上での用途は、パラメータのバリデーション処理です。 パラメータを正しく設定できているなら、不要の処理なので、ライブラリとバリデーション処理を削除してしまっても動作上は問題ないと思われます。