串接綠界與OpenID(學校單位使用ID)

Ivan Chiou
The Messiah Code 神碼技術共筆
12 min readOct 5, 2023

目前在PAIA教育系統上,我們需要與校級單位經常在使用的OpenID串接。並依照不同的使用者身份,給予不同的金鑰購買價格,來串接後面的綠界金流。

在django的路由設定上,先設定好openid auth與openid handle response的兩個路由。

path('openid/auth', include('api.openid_auth_urls'))
path('openid', include('api.openid_token_urls'))

然後Auth function內容如下,與open id提供的API認證完畢後返回(redirect_uri)至openid handle response的路由(path(‘openid’))

class OpenIDAuthView(generics.RetrieveAPIView):
def get(self, request, *args, **kwargs):
state = random.randint(0, 9999999)
s = str(state)
b = s.encode('UTF-8')
nonce = base64.b64encode(b)
response = HttpResponse('', status=302)
if DEBUG:
redirect_uri = f"http://{request.META['HTTP_HOST']}/openid"
else:
redirect_uri = f"https://{request.META['HTTP_HOST']}/openid"
response[
'Location'] = f"{OPENID_HOST}/oidc/v1/azp?response_type=code&client_id={OPENID_CLIENT_ID}&redirect_uri={redirect_uri}&scope=openid2+openid+email+profile+eduinfo&state={state}&nonce={nonce}"
return response

openid handle response路由處理取得到的openid token,並將openid提供的老師、學生資料存到使用者資料庫。

class OpenIDTokenView(generics.RetrieveAPIView):
serializer_class = OpenIDSerializer

def get(self, request, *args, **kwargs):
headers = {
'Content-type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
}
data = {}
data['code'] = str(request.GET['code'])
data['redirect_uri'] = f"https://{request.META['HTTP_HOST']}/openid"
data['grant_type'] = 'authorization_code'
data['client_id'] = OPENID_CLIENT_ID
data['client_secret'] = OPENID_CLIENT_SECRET

resp = requests.post(
f"{OPENID_HOST}/oidc/v1/token", data=data, headers=headers)

if 'access_token' in resp.json():
headers = {
'Content-type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
'Authorization': f"Bearer {resp.json()['access_token']}"
}

resp = requests.get(
f"{OPENID_HOST}/moeresource/api/v1/oidc/eduinfo", headers=headers)
edu_info = resp.json()

school_id = edu_info['schoolid']
classinfo = edu_info['classinfo']
if 'titles' in edu_info:
positions = edu_info['titles']
for position in positions:
if 'titles' in position:
titles = position['titles']
for title in titles:
if title == '導師' or title == '教師':
user.profile.role = Profile.ROLE_TEACHER
user.profile.save()
return render(
request,
OIDC_TEMPLATES["success"],
return_data,
)

最後在透過綠界(ecpay)提供API去購買VIP金鑰的時候,就可以依照OpenID提供的使用者身份,給予不同的金鑰與價格。

購買流程如下:

@user_router.post(
'',
summary="使用者升級VIP付款",
response={
httpStatus.OK: str
}
)
def payment_user_vip(request):
"""
# 使用者升級VIP付款
"""
user = request.auth

# 確認商品資訊
product = Product.objects.filter(category__name=ProductCategory.CATEGORY_KEY, vip_payment=True).first()

if product:
host = get_host(request)
customer_name = user.first_name + user.last_name
print(f"{host}/api/v2/user/me/vip_paid")
order_params = {
'MerchantTradeNo': datetime.now().strftime("NO%Y%m%d%H%M%S"),
'StoreID': ECPAY_MERCHANT_ID,
'MerchantTradeDate': datetime.now().strftime("%Y/%m/%d %H:%M:%S"),
'PaymentType': 'aio',
'TotalAmount': product.price,
'TradeDesc': product.description[:200],
'ItemName': f"{product.name}-{customer_name}-VIP升級",
'ReturnURL': f"{host}/api/v2/user/me/vip_paid_to_ecpay",
'OrderResultURL': f"{host}/api/v2/user/me/vip_paid",
'ChoosePayment': 'Credit',
'ClientBackURL': f"{request.META['HTTP_REFERER']}profile",
'ItemURL': f"{request.META['HTTP_REFERER']}profile",
'Remark': '交易備註',
'ChooseSubPayment': '',
'NeedExtraPaidInfo': 'Y',
'DeviceSource': '',
'IgnorePayment': '',
'PlatformID': '',
'InvoiceMark': 'N',
'CustomField1': str(user.id),
'CustomField2': f"{request.META['HTTP_REFERER']}profile",
'CustomField3': f"{product.name}-{customer_name}-VIP升級",
'CustomField4': f"{product.id}",
'EncryptType': 1,
}

extend_params_1 = {
'BindingCard': 0,
'MerchantMemberID': '',
}

extend_params_2 = {
'Redeem': 'N',
'UnionPay': 0,
}

inv_params = {
'CustomerID': user.id, # 客戶編號
'CustomerName': customer_name,
}

# 建立實體
ecpay_payment_sdk = module.ECPayPaymentSdk(
MerchantID=ECPAY_MERCHANT_ID,
HashKey=ECPAY_HASH_KEY,
HashIV=ECPAY_HASH_IV
)

# 合併延伸參數
order_params.update(extend_params_1)
order_params.update(extend_params_2)

# 合併發票參數
order_params.update(inv_params)

try:
# 產生綠界訂單所需參數
final_order_params = ecpay_payment_sdk.create_order(order_params)

# 產生 html 的 form 格式
action_url = ECPAY_ACTION_URL + "/Cashier/AioCheckOut/V5" # 測試環境
html = ecpay_payment_sdk.gen_html_post_form(action_url, final_order_params)
return httpStatus.OK, UserVIPPaymentResultSchema(
**user.__dict__,
html_result=html
)
except Exception as error:
print('An exception happened: ' + str(error))
raise HttpError(httpStatus.NOT_FOUND, Detail(detail=str(error)))
else:
return httpStatus.NOT_FOUND, Detail(detail="此VIP金鑰商品不存在")

購買完畢後,依照使用者身份處理金鑰。

@user_router.post(
'vip_paid',
summary="VIP已付款",
response={
httpStatus.OK: str
}
)
def paid_user_vip(request):
"""
# VIP已付款
"""
ecpay_payment_sdk = module.ECPayPaymentSdk(
MerchantID=ECPAY_MERCHANT_ID,
HashKey=ECPAY_HASH_KEY,
HashIV=ECPAY_HASH_IV
)
res = request.POST.dict()
back_check_mac_value = request.POST.get('CheckMacValue')
order_id = request.POST.get('MerchantTradeNo')
trade_number = request.POST.get('TradeNo')
uid = request.POST.get('CustomField1')
return_url = request.POST.get('CustomField2')
order_name = request.POST.get('CustomField3')
product_id = request.POST.get('CustomField4')
rtnmsg = request.POST.get('RtnMsg')
rtncode = request.POST.get('RtnCode')
check_mac_value = ecpay_payment_sdk.generate_check_value(res)
if check_mac_value == back_check_mac_value and rtnmsg == 'Succeeded' and rtncode == '1':
user = User.objects.get(pk=uid)
product = Product.objects.filter(id=product_id).first()

if product and user:
# 依照user role取得商品庫存並建立新的金鑰
item = ProductItem.objects.filter(product=product, key__role=user.profile.role, key__status=Key.STATUS_AVAILABLE).first()
createNewKey(user, (datetime.now()+item.key.expired_at))

# 自動產生Order
order = Order.objects.create(id=order_id, product=product, name=order_name, trade_number=trade_number, total_amount=product.price, creator=user)
order.status = Order.STATUS_DELIVERED
order.save()
return redirect(return_url)
else:
raise HttpError(httpStatus.NOT_FOUND, "Product or User is not existed")

最終完成帳戶升級。

--

--

Ivan Chiou
The Messiah Code 神碼技術共筆

Rich experience in multimedia integration, cross-functional collaboration, and dedicated to be a mentor for young developers.