import { ApiUrls, ErrorType } from '../auth/consts' import { SimpleStorage, RequestFunction } from '../oauth2client/interface' import { AuthClientRequestOptions } from '../oauth2client/models' import { defaultStorage } from '../oauth2client/oauth2client' import { isMp } from '../utils/mp' import { SDKAdapterInterface } from '@cloudbase/adapter-interface' import { openURIWithCallback } from './captcha-dom' import { Auth } from '../auth/apis' export interface CaptchaOptions { env: string; clientId: string request: RequestFunction storage: SimpleStorage // 打开网页并通过URL回调获取 CaptchaToken,针对不通的平台,该函数可以自定义实现, 默认集成浏览器端认证 openURIWithCallback?: OpenURIWithCallbackFuction adapter?: SDKAdapterInterface & { isMatch?: () => boolean } oauthInstance?: Auth } type OpenURIWithCallbackFuction = (url: string) => Promise export interface CaptchaToken { captcha_token: string expires_in: number expires_at?: Date | null } export interface CaptchaRequestOptions extends AuthClientRequestOptions { withCaptcha?: boolean } export interface GetCaptchaResponse { captcha_token?: string expires_in?: number url?: string } export class Captcha { private config: CaptchaOptions private tokenSectionName: string /** * constructor * @param {CaptchaOptions} opts */ constructor(opts: CaptchaOptions) { if (!opts.openURIWithCallback) { opts.openURIWithCallback = this.getDefaultOpenURIWithCallback() } if (!opts.storage) { opts.storage = defaultStorage } this.config = opts this.tokenSectionName = `captcha_${opts.clientId || opts.env}` } public isMatch() { return this.config?.adapter?.isMatch?.() || isMp() } /** * request http like simple fetch api, exp:request('/v1/user/me', {withCredentials:true}) * @param {string} url * @param {AuthClientRequestOptions} options */ public async request(url: string, options?: CaptchaRequestOptions): Promise { if (!options) { options = {} } if (!options.method) { options.method = 'GET' } const state = `${options.method}:${url}` let reqURL = url if (options.withCaptcha) { reqURL = await this.appendCaptchaTokenToURL(url, state, false) } let resp: T try { resp = await this.config.request(reqURL, options) } catch (err) { if (err.error === ErrorType.CAPTCHA_REQUIRED || err.error === ErrorType.CAPTCHA_INVALID) { url = await this.appendCaptchaTokenToURL(url, state, err.error === ErrorType.CAPTCHA_INVALID) return this.config.request(url, options) } return Promise.reject(err) } return resp } private getDefaultOpenURIWithCallback(): OpenURIWithCallbackFuction { return (url: string) => openURIWithCallback(url, this.config.oauthInstance) } /** * getCaptchaToken 获取captchaToken */ private async getCaptchaToken(forceNewToken: boolean, state: string): Promise { if (!forceNewToken) { // 如果本地存在,则直接返回 const captchaToken = await this.findCaptchaToken() if (captchaToken) { return captchaToken } } /** * https://iwiki.woa.com/p/4010699417 */ const captchaDataResp = await this.config.request<{ data: string type: 'image' token: string expires_in: number }>(ApiUrls.CAPTCHA_DATA_URL, { method: 'POST', body: { state, redirect_uri: '', }, withCredentials: false, }) const captchaTokenUrl = `${captchaDataResp.data}?state=${encodeURIComponent(state)}&token=${encodeURIComponent(captchaDataResp.token,)}` const captchaToken = await this.config.openURIWithCallback(captchaTokenUrl) this.saveCaptchaToken(captchaToken) return captchaToken.captcha_token } private async appendCaptchaTokenToURL(url: string, state: string, forceNewToken: boolean): Promise { const captchaToken = await this.getCaptchaToken(forceNewToken, state) if (url.indexOf('?') > 0) { url += `&captcha_token=${captchaToken}` } else { url += `?captcha_token=${captchaToken}` } return url } private async saveCaptchaToken(token: CaptchaToken) { token.expires_at = new Date(Date.now() + (token.expires_in - 10) * 1000) const tokenStr: string = JSON.stringify(token) await this.config.storage.setItem(this.tokenSectionName, tokenStr) } private async findCaptchaToken(): Promise { const tokenStr: string = await this.config.storage.getItem(this.tokenSectionName) if (tokenStr !== undefined && tokenStr !== null) { try { const captchaToken = JSON.parse(tokenStr) if (captchaToken?.expires_at) { captchaToken.expires_at = new Date(captchaToken.expires_at) } const isExpired = captchaToken.expires_at < new Date() if (isExpired) { return null } return captchaToken.captcha_token } catch (error) { await this.config.storage.removeItem(this.tokenSectionName) return null } } return null } }