修改应用内充值,记录中的账号不再作为购买者, 增加passport token的验证

This commit is contained in:
CounterFire2023 2024-09-27 16:36:35 +08:00
parent 959740af19
commit 252368e51e
12 changed files with 269 additions and 28 deletions

49
docs/inapp.md Normal file
View File

@ -0,0 +1,49 @@
#
## 说明
```
1. 如无特殊说明, 所有接口返回 json, 顶级结构如下, 接口 Response 的数据结构说明只包含 data 部分
```JSON
{
"errcode": 0, //0:成功 100: 所有未定义的错误, 其他根据errmsg判断
"errmsg": "", //错误描述, 一般在code=0时, 该字段为空
"data": {}, // 数据
}
```
## 接口列表
### 1. 验证订单信息
1. Method: POST
2. URI: google: /native_pay/google/verify
apple: /native_pay/apple/verify
3. HOST: 测试: https://pay.cebggame.com/v2
生产: https://pay.cebggame.com/v0
> header 参数
> url param 参数
| 字段 | 说明 |
| --------- | -------------------- |
| list | 通过queryPurchase或buyProduct获取的数据中的data对象 |
5. Response: JSON
```js
[
{
productId: 'google或apple后台获取的productId',
gameOrderId: '游戏订单号',
orderId: 'google或apple返回的orderId',
status: '订单状态',
}
]
```
status: 0:默认状态, 未支付 3: 已支付, 未确认 9: 已成功支付和确认

View File

@ -28,6 +28,7 @@
"fast-rbac": "^2.0.1",
"fastify": "^4.8.1",
"fastify-plugin": "^4.2.1",
"get-jwks": "^9.0.2",
"google-auth-library": "^8.5.2",
"googleapis": "^120.0.0",
"jose": "^4.14.4",

@ -1 +1 @@
Subproject commit b982e43cd717e09acafba79f40c63e2572dbef96
Subproject commit cf4af92b819c06a6263a7b06075154b1db5cff49

View File

@ -0,0 +1,14 @@
import { singleton } from 'zutils'
@singleton
export class ReadOnlyCache {
map: Map<string, any> = new Map()
public getData(key: string) {
return this.map.get(key)
}
public setData(key: string, value: any) {
this.map.set(key, value)
}
}

View File

@ -7,10 +7,18 @@ import { BaseController, router, ZError, role, ROLE_ANON } from 'zutils'
class ApplePayController extends BaseController {
@router('post /pay/apple/verify')
@router('post /native_pay/apple/verify')
async verifyApplePay(req, res) {
logger.db('applepay_verify', req)
const user = req.user
const { list } = req.params
const { openId } = req.user
let { list } = req.params
if (typeof list === 'string') {
try {
list = JSON.parse(list)
} catch (err) {
throw new ZError(10, 'purchase data is empty')
}
}
if (!list || !list.length) {
throw new ZError(10, 'purchase data is empty')
}
@ -23,15 +31,16 @@ class ApplePayController extends BaseController {
if (!tx) {
continue
}
await InAppRecord.addRecord(user.id, tx, 'apple')
await InAppRecord.addRecord(openId, tx, 'apple')
logger.info('getTransactionInfo:: ', tx)
if (tx.transactionReason === 'PURCHASE' && tx.inAppOwnershipType === 'PURCHASED') {
status = ApplePayStatus.SUCCESS
}
if (status === ApplePayStatus.SUCCESS) {
let record = await AppleInApp.insertOrUpdate(
{ account: user.id, outOrderId: tx.transactionId },
{ outOrderId: tx.transactionId },
{
account: openId,
productId: tx.productId,
gameOrderId: tx.appAccountToken,
country: tx.storefront,

View File

@ -7,10 +7,19 @@ import { BaseController, router, ZError } from 'zutils'
class GooglePayController extends BaseController {
@router('post /pay/google/verify')
@router('post /native_pay/google/verify')
async verifyGooglePay(req, res) {
logger.db('googlepay_verify', req)
const user = req.user
const { list } = req.params
const { openId } = req.user
let { list } = req.params
// check if list is string
if (typeof list === 'string') {
try {
list = JSON.parse(list)
} catch (err) {
throw new ZError(10, 'purchase data is empty')
}
}
if (!list || !list.length) {
throw new ZError(10, 'purchase data is empty')
}
@ -23,7 +32,7 @@ class GooglePayController extends BaseController {
throw new ZError(20, 'fetch purchase data failed')
}
let info = infoRes.data
await InAppRecord.addRecord(user.id, info, 'google')
await InAppRecord.addRecord(openId, info, 'google')
let status = GooglePayStatus.PENDING
if (info.purchaseState === 0 && info.consumptionState === 0) {
status = GooglePayStatus.PURCHASED
@ -31,8 +40,9 @@ class GooglePayController extends BaseController {
if (status === GooglePayStatus.PURCHASED) {
try {
let record = await GoogleInApp.insertOrUpdate(
{ account: user.id, outOrderId: info.orderId },
{ outOrderId: info.orderId },
{
account: openId,
purchaseToken: sub.token,
productId: sub.id,
gameOrderId: info.obfuscatedExternalAccountId,

View File

@ -18,16 +18,16 @@ export enum ApplePayStatus {
@dbconn('pay')
@index({ gameOrderId: 1 }, { unique: true, partialFilterExpression: { gameOrderId: { $exists: true } } })
@index({ account: 1, outOrderId: 1 }, { unique: true })
@index({ outOrderId: 1 }, { unique: true })
@index({ account: 1 }, { unique: false })
@modelOptions({
schemaOptions: { collection: 'apple_inapp_record', timestamps: true },
schemaOptions: { collection: 'apple_inapp_record_202410', timestamps: true },
options: { allowMixed: Severity.ALLOW },
})
export class AppleInAppClass extends BaseModule {
// 用户账号
@prop({ required: true })
public account: string
public account: string // 这里只表示谁来消耗这个订单, 不代表购买者
// 渠道返回的订单id
// transactionId
@prop()

View File

@ -18,16 +18,16 @@ export enum GooglePayStatus {
@dbconn('pay')
@index({ gameOrderId: 1 }, { unique: true, partialFilterExpression: { gameOrderId: { $exists: true } } })
@index({ account: 1, outOrderId: 1 }, { unique: true })
@index({ outOrderId: 1 }, { unique: true })
@index({ account: 1 }, { unique: false })
@modelOptions({
schemaOptions: { collection: 'google_inapp_record', timestamps: true },
schemaOptions: { collection: 'google_inapp_record_202410', timestamps: true },
options: { allowMixed: Severity.ALLOW },
})
export class GoogleInAppClass extends BaseModule {
// 用户账号
@prop({ required: true })
public account: string
public account: string // 这里只表示谁来消耗这个订单, 不代表购买者
// 渠道返回的订单id
@prop()
public outOrderId: string

View File

@ -1,6 +1,7 @@
import { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'
import fastifyPlugin from 'fastify-plugin'
import { Account } from 'modules/Account'
import { verifyCombinedToken } from 'utils/jwt.utils'
declare module 'fastify' {
interface FastifyRequest {
@ -33,16 +34,23 @@ const apiAuthPlugin: FastifyPluginAsync<ApiAuthOptions> = async function (fastif
if (!request.token) {
return reply.send({ errcode: 11, errmsg: 'need login' })
}
//@ts-ignore
const data = this.jwt.verify(request.token)
if (!data || !data.id) {
return reply.send({ errcode: 10, errmsg: 'need login' })
if (request.url.indexOf('native_pay/') >= 0) {
const data = await verifyCombinedToken(request.token)
data.openId = data.openId || data.openid || data.passport?.zkevm_eth_address || data.passport?.eth_key
request.user = data
} else {
//@ts-ignore
const data = this.jwt.verify(request.token)
if (!data || !data.id) {
return reply.send({ errcode: 10, errmsg: 'need login' })
}
let account = await Account.findById(data.id)
if (!account) {
return reply.send({ errcode: 10, errmsg: 'need login' })
}
request.user = account
}
let account = await Account.findById(data.id)
if (!account) {
return reply.send({ errcode: 10, errmsg: 'need login' })
}
request.user = account
} catch (err) {
return reply.send({ errcode: 401, errmsg: 'need auth' })
}

91
src/utils/jwt.utils.ts Normal file
View File

@ -0,0 +1,91 @@
import { ReadOnlyCache } from 'common/ReadOnlyCache'
import { createSigner, createVerifier } from 'fast-jwt'
import buildGetJwks from 'get-jwks'
const domain = 'https://auth.immutable.com'
const privateKey = `-----BEGIN PRIVATE KEY-----
${process.env.REFRESH_TOKEN_SECRET_PRIVATE}
-----END PRIVATE KEY-----
`
const publicKey = `-----BEGIN PUBLIC KEY-----
${process.env.REFRESH_TOKEN_SECRET_PUBLIC}
-----END PUBLIC KEY-----
`
const REFRESH_TOKEN_EXPIRES_IN = 30 * 24 * 60 * 60 * 1000
const privateKeyWallet = `-----BEGIN PRIVATE KEY-----
${process.env.API_TOKEN_SECRET_PRIVATE}
-----END PRIVATE KEY-----
`
const publicKeyWallet = `-----BEGIN PUBLIC KEY-----
${process.env.API_TOKEN_SECRET_PUBLIC}
-----END PUBLIC KEY-----
`
const WALLET_TOKEN_EXPIRES_IN = 30 * 24 * 60 * 60 * 1000
const getJwks = buildGetJwks({ })
export const generateRefreshToken = (data: any) => {
const signSync = createSigner({
algorithm: 'EdDSA',
expiresIn: REFRESH_TOKEN_EXPIRES_IN,
key: privateKey,
})
return signSync(data)
}
export const verifyRefreshToken = (token: string) => {
const verifier = createVerifier({
algorithms: ['EdDSA'],
key: publicKey,
})
return verifier(token)
}
export const generateWalletToken = (data: any) => {
const signSync = createSigner({
algorithm: 'EdDSA',
expiresIn: WALLET_TOKEN_EXPIRES_IN,
key: privateKeyWallet,
})
return signSync(data)
}
export const verifyWalletToken = (token: string) => {
const verifier = createVerifier({
algorithms: ['EdDSA'],
key: publicKeyWallet,
})
return verifier(token)
}
export const verifyPassportToken = (token: string) => {
const verifyWithPromise = createVerifier({
key: async function (header) {
let publicKey = new ReadOnlyCache().getData('passport_public_key')
if (!publicKey) {
publicKey = await getJwks.getPublicKey({
kid: header.kid,
alg: header.alg,
domain,
})
new ReadOnlyCache().setData('passport_public_key', publicKey)
}
return Promise.resolve(publicKey)
},
})
return verifyWithPromise(token)
}
export const verifyCombinedToken = (token: string) => {
let tokenSuffix = token.split('.')[3]
tokenSuffix = tokenSuffix || 'passport'
token = token.replace(`.${tokenSuffix}`, '')
if (tokenSuffix === 'passport') {
return verifyPassportToken(token)
} else {
return verifyWalletToken(token)
}
}

View File

@ -1,10 +1,10 @@
{
"apps": [
{
"name": "wallet-svr",
"name": "pay-svr-release",
"script": "npm",
"args": "run prod:api",
"cwd": "/data/apps/wallet-svr",
"cwd": "/home/kingsome/code/pay-svr",
"max_memory_restart": "1024M",
"log_date_format" : "YYYY-MM-DD HH:mm Z",
"watch": true,
@ -15,13 +15,36 @@
"tasks"
],
"instances": 1,
"exec_mode": "cluster",
"exec_mode": "fork",
"env": {
"NODE_ENV": "production"
},
"env_production": {
"NODE_ENV": "production"
}
},
{
"name": "pay-svr-test-v0",
"script": "npm",
"args": "run dev:api",
"cwd": "/home/kingsome/code/pay-svr",
"max_memory_restart": "1024M",
"log_date_format" : "YYYY-MM-DD HH:mm Z",
"watch": true,
"ignore_watch": [
"node_modules",
"logs",
"fixtures",
"tasks"
],
"instances": 1,
"exec_mode": "fork",
"env": {
"NODE_ENV": "development"
},
"env_production": {
"NODE_ENV": "development"
}
}
]
}

View File

@ -1560,6 +1560,19 @@ elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.4:
minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1"
elliptic@^6.5.7:
version "6.5.7"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b"
integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==
dependencies:
bn.js "^4.11.9"
brorand "^1.1.0"
hash.js "^1.0.0"
hmac-drbg "^1.0.1"
inherits "^2.0.4"
minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1"
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
@ -2277,6 +2290,15 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2:
has-symbols "^1.0.3"
hasown "^2.0.0"
get-jwks@^9.0.2:
version "9.0.2"
resolved "https://registry.yarnpkg.com/get-jwks/-/get-jwks-9.0.2.tgz#9364efb7a48b126a8df88e67757c413d6abf747d"
integrity sha512-zn2OvElozYtckpYJvgRWMOMhEkW8KgFp+lN0B7Q6SXPZg/CFfeiPoh73Wbhacj4fYXDJxkxbcwI9j+/cubpzSQ==
dependencies:
jwk-to-pem "^2.0.4"
lru-cache "^10.0.0"
node-fetch "^2.6.1"
get-stream@^5.1.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
@ -2904,6 +2926,15 @@ jwa@^2.0.0:
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jwk-to-pem@^2.0.4:
version "2.0.6"
resolved "https://registry.yarnpkg.com/jwk-to-pem/-/jwk-to-pem-2.0.6.tgz#0810c03307e873d5c81faeb650408fa3ae91eb9c"
integrity sha512-zPC/5vjyR08TpknpTGW6Z3V3lDf9dU92oHbf0jJlG8tGOzslF9xk2UiO/seSx2llCUrNAe+AvmuGTICSXiYU7A==
dependencies:
asn1.js "^5.3.0"
elliptic "^6.5.7"
safe-buffer "^5.0.1"
jwks-rsa@^2.1.3:
version "2.1.5"
resolved "https://registry.npmmirror.com/jwks-rsa/-/jwks-rsa-2.1.5.tgz#bb7bf8c5767836bc273bf5b27870066aca39c1bb"
@ -3052,6 +3083,11 @@ lowercase-keys@^3.0.0:
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2"
integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==
lru-cache@^10.0.0:
version "10.4.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@ -3388,7 +3424,7 @@ node-addon-api@^2.0.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32"
integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==
node-fetch@^2.6.12:
node-fetch@2, node-fetch@^2.6.1, node-fetch@^2.6.12:
version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==