一转眼已经到第18天了,照这个速度可能没办法完成一个网站,今天要来赶进度!
首先要勘误
在nonce取值的部分,
我原本是直接return取得nonce的api回传的value,
但回传的是json格式
所以要把先前写的getNonce()方法小改一下,加上parse json的部分
另外getHashID()的return应该要转大写,补上:
return hashID.toUpperCase();
再来是getSign()的部分我忘了被加密的内容要先加上nonce跟Hash ID
以上我在
[Day 15] - 初探永丰银行线上收款API - 丰收款 - HASH ID计算(2) +IV计算
[Day 17] - 初探永丰银行线上收款API - 丰收款 - Sign值计算(2)
做了更新
另外纪录一下今天踩到的一颗雷,
在测试时,发现在相同nonce、hash id、内文的情况下,我算出的sign值竟然跟永丰提供的加解密计算机算出的结果不一样...
我用加解密计算机产生的request去发送api,
明明json的顺序、大小写都没影响呀...
但是相同的内容,回到我的程序,api却回
E0000 – 安全签章错误
测了老半天才发现
ObjectMapper会将我原本写好的物件名称转成小写,
例如Amount会变成amount,
所以我的内文会变这样
{"amount":55000,
"backendURL":"http://yahoo.com.tw",
"cardParam":{"autoBilling":"Y"},
"currencyID":"TWD",
"orderNo":"C2021000000022",
"payType":"C",
"prdtName":"信用卡订单123",
"returnURL":"http://google.com",
"shopNo":"NA9999_999"}
我原本以为没影响,因为把以上内文放到加解密计算机得出的request,是可以成功建立订单的
但是事情没我想得这麽简单
把内文输入到加解密计算机後
它在计算sign会进行以下动作
在步骤2,并不只是单纯的将各栏位转成 PropertyName=Value 的格式
而且还有将各栏位的PropertyName转为规格书中定义的名称,
也就是我原本给的amount,会被它转为Amount,
经过这样一转,SHA256的值也就不一样了
因为之前没发现加解密计算机有多做这一动,所以我还以为大小写不影响
永丰api那边,我猜也是会在message解密後,将内文栏位转为正确的大小写,再拿来计算sign
并比对我们给的sign跟它算出的sign是否相同
因此我们在计算sign时,要先想到永丰会以大小写正确的内文栏位名称来计算sign
简而言之,我的栏位名称要跟规格书上的大小写一致啦
这边我参考stackoverflow上的做法
增加一个QpayPropNamingStrategy.java
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.AnnotatedField;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
public class QpayPropNamingStrategy extends PropertyNamingStrategy {
@Override
public String nameForField(MapperConfig<?> config, AnnotatedField field, String defaultName) {
return convert(field.getName());
}
@Override
public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
return convert(method.getName().toString());
}
@Override
public String nameForSetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
return convert(method.getName().toString());
}
private String convert(String input) {
return input.substring(3);
}
}
这边大概是藉由取得方法名称,把前面的get去掉,得出第一个字是大写的名字,我觉得还满聪明的
接着在使用ObjectMapper前,先套用mapper.setPropertyNamingStrategy(new MyPropertyNamingStrategy());
就行了
现在大小写都一致了,
{"Amount":55000,
"BackendURL":"http://yahoo.com.tw",
"CardParam":{"AutoBilling":"Y"},
"CurrencyID":"TWD",
"OrderNo":"C000043000022",
"PayType":"C",
"PrdtName":"信用卡订单123",
"ReturnURL":"http://google.com",
"ShopNo":"NA00001_001"}
这样终於能成功建立订单了
回顾一下,整个orderCreate方法是长这样
public String orderCreate(OrderCreateReq orderCreateReq){
//1.nonce
String nonce=getNonce();
//2.hashID
String hashID=getHashID();
//3.iv
String iv = getIV(nonce);
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(new QpayPropNamingStrategy());
Map<String, Object> map =
mapper.convertValue(orderCreateReq, new TypeReference<TreeMap<String, Object>>() {});
//remove null
map.values().removeIf(Objects::isNull);
map.forEach((k, v) -> {
if(v.getClass().equals(java.util.LinkedHashMap.class)){
LinkedHashMap<Object,Object>m =(LinkedHashMap<Object, Object>) v;
m.values().removeIf(Objects::isNull);
map.replace(k, v, m);
}
});
String jsonContent ="";
try {
jsonContent = new ObjectMapper().writeValueAsString(map);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
//4.sign
String sign = getSign(map,nonce,hashID);
//5.message
String message = "";
try {
message= encryptUtil.encrypt(jsonContent, hashID, iv);
} catch (IOException | GeneralSecurityException e) {
e.printStackTrace();
}
//send to api
Map<String,String> request = new HashMap<String,String>();
request.put("Version", "1.0.0");
request.put("ShopNo", "NA0040_001");
request.put("APIService", "OrderCreate");
request.put("Sign", sign);
request.put("Nonce", nonce);
request.put("Message", message);
String reqJson="";
try {
reqJson = new ObjectMapper().writeValueAsString(request);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
String res="";
try {
res=util.post("https://apisbx.sinopac.com/funBIZ/QPay.WebAPI/api/Order", reqJson);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return res;
}
发送订单到API後,api一样会回覆json字串,
也就是上面返回的res,
但api返回的字串也是有加密的,
接下来要进行response的解密跟sign验证
为了要AES解密,到先前创建的EncryptUtil.java增加一个decrypt方法
大概是这样
public String decrypt(String hexContent, String key,String iv) throws IOException,GeneralSecurityException, DecoderException {
byte[] raw = key.getBytes("UTF-8");
SecretKeySpec keySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ips = new IvParameterSpec(iv.getBytes("UTF-8"));
cipher.init(Cipher.DECRYPT_MODE,keySpec,ips);
byte[] encrypted = Hex.decodeHex(hexContent.toCharArray());
byte[] origin = cipher.doFinal(encrypted);
String string = new String(origin);
return string;
}
回到orderCreate方法
这边为了方便我直接将api回传的response转换成Map,
将sign、nonce、message取出
message一样是以AES CBC加密,
为了解密需使用一样的Key(Hash ID)、IV(由回传的NONCE来计算)
如果用request使用的IV,会没办法顺利解密
...前略
String res="";
try {
res=util.post("https://apisbx.sinopac.com/funBIZ/QPay.WebAPI/api/Order", reqJson);
} catch (IOException e) {
e.printStackTrace();
}
Map<String, String> resMap =new HashMap<String,String>();
try {
resMap = mapper.readValue(res, new TypeReference<HashMap<String, String>>() {});
} catch (JsonProcessingException e) {
e.printStackTrace();
}
String resSign =resMap.get("Sign");
String resNonce =resMap.get("Nonce");
String resMessage =resMap.get("Message");
System.err.println(resMessage);
String resiv= getIV(resNonce);
try {
resMessage=encryptUtil.decrypt(resMessage, hashID, resiv);
} catch (IOException | GeneralSecurityException | DecoderException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
...
取得解密後的res,到这边暂停,
先去建立一个OrderCreateRes.java,定义好资料类别
import lombok.Data;
@Data
public class OrderCreateRes {
public enum PayType { A, C };
public enum Status {S,F};
@Data
public class ATMParam{
private String AtmPayNo;
private String WebAtmURL;
private String OtpUrl;
}
@Data
public class CardParam{
private String CardPayURL;
}
private String OrderNo;
private String ShopNo;
private String TSNo;
private Integer Amount;
private Status Status;
private String Description;
private String Param1;
private String Param2;
private String Param3;
private PayType PayType;
private ATMParam ATMParam;
private CardParam CardParam;
}
回到orderCreate
在取得解密完成的resMessage後,因为这也是json字串,所以我先转成Map,使其可以适用先前写的getSign方法
Map<String, Object> resMessageMap =new TreeMap<String,Object>();
try {
resMessageMap = mapper.readValue(resMessage, new TypeReference<TreeMap<String, Object>>() {});
} catch (JsonProcessingException e) {
e.printStackTrace();
}
取得自行计算的Sign来跟api回传时附带的Sign做比较,如果一样就表示内容没被窜改
String resForUser = "";
String signCheck = getSign(resMessageMap, resNonce, hashID);
if(signCheck.equals(resSign)){
System.out.println("Sign Check OK:"+signCheck);
}else{
resForUser="永丰回传签章验证失败"+"\nA:"+signCheck+"\nresSign:"+resSign;
return resForUser;
}
接着,就可以把解密完的内容套用至刚刚定义好的资料型别OrderCreateRes,做个简单的if判断,先回传必要资讯就好,到时候有需要再做调整
OrderCreateRes orderCreateRes = new OrderCreateRes();
try {
orderCreateRes = mapper.readValue(resMessage, OrderCreateRes.class);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
if(orderCreateRes.getATMParam()!=null){
resForUser = orderCreateRes.getATMParam().getAtmPayNo();
}else if(orderCreateRes.getCardParam()!=null){
resForUser = orderCreateRes.getCardParam().getCardPayURL();
}else if(orderCreateRes.getDescription()!=null){
resForUser = orderCreateRes.getDescription();
}else {
resForUser = "不明错误,请联系网站负责人";
}
return resForUser;
传送信用卡订单,会回传一个信用卡付款页面url
大概就是这样了,原本今天要赶进度的
结果花在debug的时间比我想像的还要长,这边解决後,那边又发现有问题,串api果然不是轻松的事...
另外,我自己觉得在程序中用了很多Map到近乎有点滥用,之後有时间的话再想想有没有更好的做法
今天就先到此结束!
目前待办清单有:
1.继续实作收款api的其他功能
2.JWT调整(过期前应该要不断重发新的JWT给client,不然client端每30分钟要重新登入一次)
3.前端React的撰写
4.用React写的前端页面串接汇率api
5.把收款API的Log纪录到DB
6.整理code、把参数放到DB、改用Spring IOC的写法...etc
明天可能会挑其中一个来做
<<: [Day20] - Django-REST-Framework Serializers 介绍
>>: [DAY 28] 用google sheet 做简易UI介面(3/3)
前言 今天我们一样使用上篇的格线布局作为范例 假设我们现在想在容器(Container)中放入三个元...
第五天 各位点进来的朋友,你们好阿 小的不才只能做这个系列的文章,但还是希望分享给点进来的朋友,知道...
Nginx 直接写在 config 内 location / { allow 127.0.0.0/2...
今日题目 题目连结:747. Largest Number At Least Twice of Ot...
今日来介绍 function 的语法结构 function 里面有三个很特别的保留字分别是 pure...