Skip to content

Commit

Permalink
feat: 添加生成 Token 的 feature
Browse files Browse the repository at this point in the history
  • Loading branch information
XGHeaven committed Mar 5, 2019
1 parent 2c3aa2c commit 438efeb
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 2 deletions.
80 changes: 80 additions & 0 deletions src/exts/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { OperateObjectParams } from '..'
import { NosBaseClient } from '../client'
import { createHmac } from 'crypto'

export interface CreateTokenParams extends OperateObjectParams {
/**
* 过期时间,单位毫秒。(NOS 只要求秒级,但是为了减少转化过程,只需要传入毫秒即可)
*/
expires: number
/**
* 对象的最小上传大小
*/
objectSizeMin?: number
/**
* 对象的最大上传大小
*/
objectSizeMax?: number
/**
* 对象类型限制,可以传入 `'image/png;image/jpg'` 字符串或者 `['image/png', 'image/jpg']` 数组
*/
mimeLimit?: string | string[]
/**
* 是否允许覆盖上传,默认允许
*/
overwrite?: boolean
}

export interface Token {
/**
* 上传凭证
*/
putPolicy: string
/**
* 签名
*/
sign: string
}

export class NosClientAuthExt extends NosBaseClient {
/**
* 创建上传凭证,返回 Token 对象,里面有 `putPolicy` 和 `sign` 字段,前端可以通过此构建上传 Token
* @param params
*/
createToken(params: CreateTokenParams): Token {
const putPolicyObject = {
Bucket: params.bucket || this.options.defaultBucket,
Object: params.objectKey,
Expires: Math.floor(params.expires / 1000),
ObjectSizeMin: params.objectSizeMin,
ObjectSizeMax: params.objectSizeMax,
MimeLimit: params.mimeLimit,
OverWrite: params.overwrite
}

if (putPolicyObject.MimeLimit && typeof putPolicyObject.MimeLimit !== 'string') {
putPolicyObject.MimeLimit = putPolicyObject.MimeLimit.join(';')
}

const putPolicyString = JSON.stringify(putPolicyObject)
const putPolicy = Buffer.from(putPolicyString, 'utf8').toString('base64')

const ciper = createHmac('sha256', this.options.accessSecret)
ciper.update(putPolicy, 'utf8')
const sign = ciper.digest('base64')

return {
putPolicy,
sign
}
}

/**
* 创建上传凭证字符串,对 `createToken` 的简单包装,直接返回 accessKey:sign:putPolicy 字符串
* @param params
*/
createTokenString(params: CreateTokenParams): string {
const { putPolicy, sign } = this.createToken(params)
return `${this.options.accessKey}:${sign}:${putPolicy}`
}
}
6 changes: 4 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NosBaseClient } from './client'
import { NosClientAuthExt } from './exts/auth'
import { NosClientBucketExt } from './exts/bucket'
import { NosClientMultipartUploadExt } from './exts/multipart-upload'
import { NosClientObjectExt } from './exts/object'
Expand All @@ -10,9 +11,10 @@ export * from './type/multipart-upload'
export * from './type/bucket'
export * from './type/callback'
export * from './lib/error'
export { Token, CreateTokenParams } from './exts/auth'

export class NosClient extends NosBaseClient {}

export interface NosClient extends NosClientBucketExt, NosClientObjectExt, NosClientMultipartUploadExt {}
export interface NosClient extends NosClientBucketExt, NosClientObjectExt, NosClientMultipartUploadExt, NosClientAuthExt {}

applyMixins(NosClient, [NosClientBucketExt, NosClientObjectExt, NosClientMultipartUploadExt])
applyMixins(NosClient, [NosClientBucketExt, NosClientObjectExt, NosClientMultipartUploadExt, NosClientAuthExt])
106 changes: 106 additions & 0 deletions test/exts/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { NosClient, Token } from '../../src'
import { cleanClient, newClient, randomObjectKey, uploadUseToken } from '../helpers/client'

let client: NosClient
let bucket: string

beforeAll(async () => {
client = await newClient()
bucket = client.options.defaultBucket as string
})

afterAll(async () => {
await cleanClient(client)
})

function joinToken(token: Token) {
return `${client.options.accessKey}:${token.sign}:${token.putPolicy}`
}

describe('createToken', () => {
it('should create token success', async () => {
const objectKey = randomObjectKey()
const token = client.createToken({
objectKey,
expires: Date.now() + 3600 * 1000,
})

const ret = await uploadUseToken(`${client.options.accessKey}:${token.sign}:${token.putPolicy}`, bucket, objectKey, 'xxxxxx')
expect(ret).toBeTrue()

await expect(client.isObjectExist({objectKey})).resolves.toBeTrue()
})

it('should limit size', async () => {
const objectKey = randomObjectKey()
const minSize = 4
const maxSize = 16
const token = client.createToken({
objectKey,
expires: Date.now() + 3600 * 1000,
objectSizeMin: minSize,
objectSizeMax: maxSize,
})

await expect(uploadUseToken(joinToken(token), bucket, objectKey, 'x'.repeat(3))).resolves.toBeFalse()
await expect(uploadUseToken(joinToken(token), bucket, objectKey, 'x'.repeat(10))).resolves.toBeTrue()
await expect(uploadUseToken(joinToken(token), bucket, objectKey, 'x'.repeat(20))).resolves.toBeFalse()
})

it('should mime limit works', async () => {
const objectKey = randomObjectKey('.jpg')
const token = client.createToken({
objectKey,
expires: Date.now() + 3600 * 1000,
mimeLimit: ['image/jpg', 'image.png']
})

await expect(uploadUseToken(joinToken(token), bucket, objectKey, 'x')).resolves.toBeFalse()
await expect(uploadUseToken(joinToken(token), bucket, objectKey, 'x', {
'content-type': 'image/jpg'
})).resolves.toBeTrue()
await expect(uploadUseToken(joinToken(token), bucket, objectKey, 'x', {
'content-type': 'text/plain'
})).resolves.toBeFalse()
})

it('should overwrite=true works', async () => {
const objectKey = randomObjectKey()
const token = client.createToken({
objectKey,
expires: Date.now() + 3600 * 1000,
overwrite: true
})

for (let i = 0; i < 2; i++) {
await expect(uploadUseToken(joinToken(token), bucket, objectKey, 'x')).resolves.toBeTrue()
}
})

it('should overwrite=false works', async () => {
const objectKey = randomObjectKey()
const token = client.createToken({
objectKey,
expires: Date.now() + 3600 * 1000,
overwrite: false
})

await expect(uploadUseToken(joinToken(token), bucket, objectKey, 'x')).resolves.toBeTrue()
await expect(uploadUseToken(joinToken(token), bucket, objectKey, 'x')).resolves.toBeFalse()
})
})

describe('createTokenString', () => {
it('should equal with createToken', () => {
const objectKey = randomObjectKey()
const expires = Date.now()
const token = client.createToken({
objectKey,
expires
})

const tokenString = client.createTokenString({objectKey, expires})

expect(tokenString).toEqual(`${client.options.accessKey}:${token.sign}:${token.putPolicy}`)
})
})
28 changes: 28 additions & 0 deletions test/helpers/client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import fetch from 'node-fetch'
import { NosClient } from '../../src'
import * as dotenv from 'dotenv'
import 'jest-extended'
dotenv.config()

// 缓存的直传最优上传节点
let cacheEndpoint = ''

export function randomBucketName() {
return `nos-node-sdk-test-bucket-${Date.now()}${Math.floor(Math.random() * 900 + 100)}`
}
Expand Down Expand Up @@ -91,3 +95,27 @@ export async function putRandomObject(
})
}
}

export async function uploadUseToken(token: string, bucket: string, objectKey: string, body: Buffer | string, headers: any = {}): Promise<boolean> {
let endpoint = cacheEndpoint
if (!endpoint) {
const resp = await fetch('http://lbs-eastchina1.126.net/lbs?version=1.0&bucketname=' + bucket)
const data = await resp.json()
endpoint = cacheEndpoint = data.upload[0]
}

const resp = await fetch(`${endpoint}/${bucket}/${objectKey}?offset=0&complete=true&version=1.0`, {
method: 'POST',
headers: {
'host': 'nos-eastchina1.126.net',
'x-nos-token': `UPLOAD ${token}`,
'content-length': body.length.toString(),
...headers
},
body
})

const data = await resp.json()

return !data.errCode
}

0 comments on commit 438efeb

Please sign in to comment.