From 52d2d12062d8a34768ddc03b06424183d721d105 Mon Sep 17 00:00:00 2001 From: liuhuapiaoyuan <278780765@qq.com> Date: Mon, 11 Nov 2024 02:06:51 +0800 Subject: [PATCH] =?UTF-8?q?WIP:=20chore(wechatmp)=EF=BC=9A=E5=9F=BA?= =?UTF-8?q?=E6=9C=AC=E5=A4=84=E7=90=86=E5=AE=8C=E6=88=90=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E5=85=AC=E4=BC=97=E5=8F=B7=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/launch.json | 36 +++ apps/example/package.json | 2 + .../src/app/api/auth/wechatmp/route.ts | 2 + apps/example/src/auth.ts | 16 +- bun.lockb | Bin 269248 -> 269232 bytes package.json | 4 +- packages/wechatmp-kit/package.json | 4 +- .../src/service/MessageService.ts | 2 + packages/wechatmp-kit/src/utils.ts | 21 -- packages/wechatmp/package.json | 4 +- packages/wechatmp/src/index.ts | 254 ++++++++++++++++-- packages/wechatmp/src/lib/CaptchaManager.ts | 93 +++++++ packages/wechatmp/src/lib/Handle.ts | 0 13 files changed, 385 insertions(+), 53 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 apps/example/src/app/api/auth/wechatmp/route.ts create mode 100644 packages/wechatmp/src/lib/CaptchaManager.ts create mode 100644 packages/wechatmp/src/lib/Handle.ts diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2627b8f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,36 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Next.js: debug server-side", + "type": "node-terminal", + "request": "launch", + "command": "bun run example:dev" + }, + { + "name": "Next.js: debug client-side", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000" + }, + { + "name": "Next.js: debug full stack", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/.bin/next", + "runtimeArgs": [ + "--inspect" + ], + "skipFiles": [ + "/**" + ], + "serverReadyAction": { + "action": "debugWithEdge", + "killOnServerStop": true, + "pattern": "- Local:.+(https?://.+)", + "uriFormat": "%s", + "webRoot": "${workspaceFolder}" + } + } + ] +} \ No newline at end of file diff --git a/apps/example/package.json b/apps/example/package.json index f74a8a6..66ed962 100644 --- a/apps/example/package.json +++ b/apps/example/package.json @@ -24,6 +24,8 @@ "next": "15.0.1", "next-auth": "beta", "next-auth-oauth": "^1", + "@next-auth-oauth/wechatmp": "*", + "wechatmp-kit": "*", "react": "19.0.0-rc-cae764ce-20241025", "react-dom": "19.0.0-rc-cae764ce-20241025", "tailwind-merge": "^2.5.2" diff --git a/apps/example/src/app/api/auth/wechatmp/route.ts b/apps/example/src/app/api/auth/wechatmp/route.ts new file mode 100644 index 0000000..153d59d --- /dev/null +++ b/apps/example/src/app/api/auth/wechatmp/route.ts @@ -0,0 +1,2 @@ +import { wechatMpProvder } from '@/auth' +export const { GET, POST } = wechatMpProvder diff --git a/apps/example/src/auth.ts b/apps/example/src/auth.ts index 02a5ab8..e3e0190 100644 --- a/apps/example/src/auth.ts +++ b/apps/example/src/auth.ts @@ -5,10 +5,22 @@ import { prisma } from '@/lib/db' import { AuthService } from '@/service/auth.service' import { Gitee } from '@next-auth-oauth/gitee' import Github from 'next-auth/providers/github' - +import Wehcatmp from '@next-auth-oauth/wechatmp' +import { WechatMpApi } from 'wechatmp-kit' import { AuthConfig } from './auth.config' export const authAdapter = PrismaAdapter(prisma) +export const wechatMpProvder = Wehcatmp({ + wechatMpApi: new WechatMpApi({ + appId: process.env.AUTH_WECHATMP_APPID!, + appSecret: process.env.AUTH_WECHATMP_APPSECRET!, + }), + endpoint: 'http://localhost:3000/api/auth/wechatmp', + /** + * 通过消息回复 + */ + checkType: 'QRCODE', +}) export const authService = new AuthService() export const { handlers, @@ -23,7 +35,7 @@ export const { listAccount, } = AdavanceNextAuth({ ...AuthConfig, - providers: [Gitee, Github], + providers: [Gitee, Github, wechatMpProvder], adapter: authAdapter, userService: authService, autoBind: true, diff --git a/bun.lockb b/bun.lockb index fe1239c4e8f6205fdf66651e178f1ad3e82f245c..69b7b692f7fa2ec456893376ecb6964be85cefdd 100644 GIT binary patch delta 15854 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 WZ=yA69^?xtbJbSGdCh}5c>V*ca~hri delta 16019 zcmeI33shCrzK8eT;t4WJ%MvtvWe(amt>@-CaAQ1WP(aQQ2(H! z`6{SLlM1m*YGp!(k4gp2-VZrOPdh@j2lGFC^l3REDdSun^!F8U`e_42>^?}!NT3@pF z{1>jT4=x@Yvb#m|eIAb|yg1_YBf-Tj+wX1>d{U3odo`O=*`@fSsI;Crt!GB&^gky! zCo`#~w_}d?oNlTWmnTg*qd0T)lsj|cM)xlc-qkw9AI30~ygd9Nr6xPCJc)WH*^Hb` z`p9fDlw9KK%gA8rYsltgA*ue)x%TH>t|zsS{>Bc)uHhx}FEqU3>Sd$`dX40t=XIBF zkea}ouD+c-gZd%oKP0s@%^>QZI{)|dH}-36(14%QK{Gx~YK5;{{TsJp)0Fy_2jzFJ zU8&;tuKv$b+dJxa9gmU{e{*%Eia)umb6GFR@@J_d^_y!~s`xt(dP+{v;4bBZUEQyO zLd2gqSN}6ywsHNMrk=tG=anjUaCN1MosQN0fkIB zk9G7YHE?%Q6FHAmzn-pLDc{@G|5>VEpT>UdstUfG+5_GEGBG^pnU8I3K9<_drLL~j z>X(yxO(c*l$V5{8l3d%pZ4 zp6L8!QWKv->P(nHYNDB>`enJC<=W?xT9hrx6+X7F6;>h6BX_%wdr1wjkJO@6eZOme z-}$Dg_CwAqHK9*P?a*OT?KLj{N@}~cI{s?-n$)7y9(_-0fS+7@oy&TcKa>3P{Oalr zs<0@v!DfuEaf3+JPb4+oDJ1_qr@Hi?PC*^da3Wl-EJ_v6S=@ zH@h6-@-|ZE)^O6td$H#p3fjO3QUj!rT9opsq||6vA4jU+cv6RUk{bV;)Hr_EPuZG! z4jH8L-&{k})CO{$SE{&_2l-{BHoQvGkZKPO;`nR8 z6E*rDQXN|0wP)?!if579b~o2wsr92=b|)OfvIzYARZ1v>sJU(AC#M7s`s z$q?!ToWGLPkr+(2A@3)({EyTBzctXmFpf^XNv^*?Q1~~c|G#F^bOJg_rZPi4x6@tD zAoUbI?q=RJbxvft_F1l7sd46!`fOS1yi()lH`aOnj}`trbq+k`Rw!`!45^7cOKQW< zJHOuf7hG;2wJ6o^C0B2n`uy>fxCW&L-stK|)k|Gnsrn{Y|7WRwueg57Q>ed1YR}&$ z)m}+9o_~8NXla`2@D5(@1J!PY1FoM^?FU`H@BF`#n#c!1p4%E{^pP8&X=(!>OJ)O(l!})lsCWcgog+GW#Q1xyW8Kr{P?k+*w8E&F!wAa8_KOH@h3-ht>~S$7~Z??BW_bh4Oa zM07GDKN-=*Y9tOzB;1MUYI%1ea_>YmNOZIKp@_Jlh@zp0D65yKlSsY`(cKE~LKNJE z2pxv#VM)UfiNg?O5Tmz7Fvl!&++ae<}WjTmt^qEe!dh2Mh+y9beR z58`60kSLevaW5jq((gs2-HSLR(a)mpLqy(($i5E|Yt<4}60!FqF0-ury+geH?PJB| z7Bhn8=n*vMkDz&g)kqwcNJv2pw7e8VZVIA7;%bY301@{9qUZrcywyw8NhFU%46?$J zh=P%b&<7C-mh>PZ@j*nH#Pt@EiU>|cq^2Tnuu_SQ5)ls}ZnBhz5F;K!R7%`p;iC{? zqYxRR5JRj&qFkcKXhf2wk4B`8MjVp3-J-@IBF7-I#~_leTB1rK_F=?O%X%1*`7okZ zVwlB@MMRH9JfdhkBE{+@>LijML5#G* zM-T;%AVMb~QY~o$B5?wuOk$LUq#=US5UFX1F;*(EQ6eH8G1gMj5hKzOl@jAEd?F%j zA|hiVVuDpjluPuOgh;paNr<#bh(i*SENU_$axx-&G9tsOC8{K1GZ22u%0OghAZjJ1 zTFj$}=tmLxk0Pd9jl^Mz1V3Vi<@pi0enf-BOpBj_h?{~anu5r(dWkxTf`Eo3?(cse3=I%2++N^F#fcnpzaDUTsWJcg*0SY+Wd5MeVA z88Z-O6%yqVJswBoS^DFMw8s&LB$is#Ohn{NMD|QXzEw+9NyKI%R#;XhA~O?FE3wjI zvJlZ(i2N+XYO9erERiq^vDWftA#!IS8YG^!_}PfK*@&Xqh(fEEsFO&ZgLu{o=O7B^ zAVRYdMV6F}NX$l*NvyMwClJ9;AX1+|ykMmg8zmyKH`wXD;Bi?5xD@7y#P^W)e=<_u{ns%mX(9Z%t6#jY_*t$ zi0Flg{Dp|utw!RoM8YD(Hp^Rt$X$eJkl1eVixF{)5k-p;6;>}%Cy{K39adTV;Q2_(w8C9mLU#F9JHu>L}Wf9J0Ee#swJu&3UERpae;&aP;5|R5PqCw(Ii(iR|TZt%IiKwx9i8_hoRfr>2xC&9Q3K6;* zQEN%75s9l2WfI?5$QnfO8bsrphtEAJsPZ9qDmsR&^yTYyVqtFdT+JN zLQE~j>$R9?=o0-5BL5jgGpms}ERpamqPgWgi^zQz(I9bx#XpCLdk#_b93sT(CF&%S zix4fWun195ga~~e5o$@#^J#Xny`VV7Le|k7ybhnbj^Ff_Zp(;HAKACOVmjuzm6DWg|8zDUPpw!fk?2VHxP+$Aj%}Jw~%d! z;BAQ1ZHODJRAQq<#G8nlEagqah&K_H61Q0Rc0|~AM8DL|QrG zki_j4Re^}CKx9`SlC4^zN+R|x#8At63z7L2qE=#<#q2;t??B}5K-_IL5{D%c-bUPO zd2b_f-$pb@+;8!fh`35bQ6(b9>LuzVl6N9TTH#Ja!A?ZzE<~y&?Ls8(LX=62vXI?~ z;N6JS-H0((DzQ-_Vh>`hrR+hB*n_B)7;oY4Ai~~3WW0lzU=k(su7_F5ZRV=0FihAQ6@3hLJlH= z4~7`zxlhTIFTHi%f9GSS zS2GEVFdlpT*~5um&wTx`(U16p*yj#g;}izHZPH#+`Oekzrj z`g1wU9M@4d31}egWwvwrnLCxTmM2IRPlubFn@4IRx`{5-IXw+>`i*~q3QFg6*Wkzc zy;e((li`$08;!@a(7CqMUvX}cbM2@<|G4rUjqlwetj)6Sm-#h@|t+XT+< z=}P5nxAJP&@oZc-%ALtI&UK?4<=k55B5^N+27b!9D9R0mvAxlXwV&)> z)`^%855P!x5cE>L3x>lzkO0@iji8ro4D|Jl^mzJFxCClBGGBx4-aHBP8a^3LftJt; zbStQr^g$5l7SdngN6>w$y63e9J_OxJ`vK@qUA^`f+8FblW|`(2Z2j|mxqjV+@ELpo zx*KHyRPd+-w<3e1rVwQ0xo+;1cK$ec&R{jcSj;1W1E)mCok8C@XLm6y=t?*hiZsXA1Y>Qwq=r^p+a0f>`8IqtkTmTnB8wi88 z&<@UoQ=v8JCg2lD-Bo-o#9Pr)-;f@<=b|&T2i*no71YAl@GX1?-@^}Z)H*Hmwec^f z_#~`^Rj?Y?z*<-e%Rn~`9RS_oaxR^;0$;&*&`WLPQTP%52D-VvHO%9kR-f~qFve5(_3#+X z0DW9H-mgpkLZ6RdE}RQN;PLXAR7T!L2Yt?uhOv+c;gCT!Z)^g zXB~Z=r0jy{A+jrsfHR>TEcV#TYkYl6j;`@_^!neVS3QKe9`}>?f#C*#*6s!a;WD@c z`axf~5cG|xH+cowi##9BhO?jxbc6`#0PUeObb{ZHE&kutsEz!M0W{c;(35%((1z7^ z4n#pDbO${J=fMS__KTnoTny0=1NzAC2$xch1x;2vq`t~4m0rFssl(-)|>W$hw0`wciN0K&Za1B~lmycfc6x zw~^P9N#rBsP}e4ZJ1FmjWVjdZhT(9J%M?;O@*t$bNO-{2O%mCmL#%KElEp41h9&V?~B+J+VIe)cfMaiA~H6UaxPC*@n=QOfXabs?Chb3$ho-aAWCDAnHg#MG23jHD z0(+#s>JwN;J`Z%1R)RLR6rO|?upG2=%YZh2~UF&_COwK>MGq zBd1AgeUS>bG4lUC2uS46kF;Ld?uFio$^wg*vn5_0%d$-9aCWP}h&R(4~Tb&qz z6}4CD{2^%Y4?r8x>v}H5pChN6;jWf+leoE@K@{Vh3mfUy|jZ348{hf+q3_d<>dM;}How|AE63 z*qDxno^PEndZWTd;%M( zp}Y<>8FiBXD`>-i94s)%5!yDxZ1@3m0)7wQ!SkRuu=Sw#fUjT{d<);e*LwbIDQIvF zs6kW?WYeRv)3r5q+DrMLpdRYr7tj~GiEss^gD&nKf$@+6ztQJD@^0NJKa9d%Fce}y zSADf9tj0*ToXo`w~yoWOWGSCs}3-T%l+GAb0zw?(jY13abw650EIvP*??}TK~h1uASc72&eDdH^Gf?16&7#K!aZEGQp+XV9>>TpsW9)yQr6a>zg zG|Ce|XVE11g?*e$X2Tqq41u%CrT_QY$Z7B>Oof>+9cF>9W-{S%m;y84G4MkMXdSJq z_0+DiQeI=I93aq7;|Kaq)%o9ag}?;VS^RNQfeC4yz+^O$CqR3r!2>Bb50vvk7cJtA zrm3%$l94acgXPJRt;N2?w%*<)ZFl-ww?&{mDZOFHM^@hqDi{$X9pvf)jTs63DkgZ@7CEMBrwe#Bgu%Pp; mWq8mQ`zS0ZG9) { if (!this.aesMode) { diff --git a/packages/wechatmp-kit/src/utils.ts b/packages/wechatmp-kit/src/utils.ts index f212031..42b6d00 100644 --- a/packages/wechatmp-kit/src/utils.ts +++ b/packages/wechatmp-kit/src/utils.ts @@ -53,24 +53,3 @@ export function renderXML(data: Record) { xmls.push('') return xmls.join('') } -const t1 = parseWehcatMessageXML< - Record ->(` - - - - 1348831860 - 1348831860 - - - - 1234567890123456 - xxxx - xxxx - - `) -console.log({ t1 }) -const t2 = renderXML(t1) -console.log({ t2 }) -const t3 = parseWehcatMessageXML(t2) -console.log({ t3 }) diff --git a/packages/wechatmp/package.json b/packages/wechatmp/package.json index 929d2e8..5530856 100644 --- a/packages/wechatmp/package.json +++ b/packages/wechatmp/package.json @@ -10,6 +10,7 @@ "docs" ], "scripts": { + "dev": "tsc --declaration --emitDeclarationOnly && bun build ./src/index.ts --target=node --outdir=dist --watch", "build": "bun build ./src/index.ts --target=node --outdir=dist && tsc --declaration --emitDeclarationOnly", "patch:version": "npm version patch", "release": "bun run build && npm publish && npm version patch" @@ -31,7 +32,8 @@ }, "license": "MIC", "peerDependencies": { - "next-auth": "beta" + "next-auth": "beta", + "next": "^15" }, "dependencies": { "wechatmp-kit": "*" diff --git a/packages/wechatmp/src/index.ts b/packages/wechatmp/src/index.ts index ad664b5..318da90 100644 --- a/packages/wechatmp/src/index.ts +++ b/packages/wechatmp/src/index.ts @@ -7,6 +7,8 @@ import type { UserinfoEndpointHandler, } from 'next-auth/providers' import { WechatMpApi } from 'wechatmp-kit' +import { CaptchaManager } from './lib/CaptchaManager' + export type WechatPlatformConfig = { /** * 验证类型 "MESSAGE"|"QRCODE" @@ -14,7 +16,13 @@ export type WechatPlatformConfig = { * QRCODE 临时二维码 * @default "MESSAGE" */ - type: 'MESSAGE' | 'QRCODE' + checkType: 'MESSAGE' | 'QRCODE' + + /** + * 二维码图片地址 + * checkType为MESSAGE时必须配置此参数 + */ + qrcodeImageUrl?: string /** * 认证账号必须提供 * 提供二维码创建工具, @@ -22,9 +30,11 @@ export type WechatPlatformConfig = { wechatMpApi: WechatMpApi /** - * 二维码验证页面 + * 页面接口,包含: + * - 二维码展示页面 + * - 微信消息回调页面 */ - qrcodePage?: string + endpoint: string } export type WechatMpProfile = { @@ -37,11 +47,24 @@ export type WechatMpProfile = { */ unionid: string } +function checkPrint() { + // @ts-expect-error printFlag + if (global.printFlag === false) { + // @ts-expect-error printFlag + global.printFlag = true + return false + } + return true +} -type WechatMpResult

= { - options?: OAuthUserConfig

& WechatPlatformConfig +type WechatMpResult = { + GET: (req: Request) => Promise + POST: (req: Request) => Promise } +function isBlank(str?: string) { + return str === undefined || str === null || str.trim() === '' +} /** * 微信公众号平台(验证码登录) * [体验账号申请](https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login) @@ -51,32 +74,44 @@ type WechatMpResult

= { */ export default function WeChatMp

( options: OAuthUserConfig

& WechatPlatformConfig, -): OAuth2Config

& WechatMpResult

{ - const { wechatMpApi } = options ?? {} +): OAuth2Config

& WechatMpResult { + const { wechatMpApi, checkType, endpoint, qrcodeImageUrl } = Object.assign( + { + endpoint: process.env.AUTH_WECHATMP_ENDPOINT, + checkType: 'MESSAGE', + }, + options ?? {}, + ) + const captchaManager = new CaptchaManager() + + // 验证MESSAGE + if (checkType === 'MESSAGE' && isBlank(qrcodeImageUrl)) { + throw new Error('checkType为MESSAGE时,必须配置qrcodeImageUrl') + } - const message = wechatMpApi.getMessageService('', '') + const messageServicde = wechatMpApi.getMessageService( + process.env.AUTH_WECHATMP_TOKEN!, + process.env.AUTH_WECHATMP_AESKEY!, + ) + // 检验endpoint是否是完整的http + const endpointUrl = new URL(endpoint) // 跳转页面,也就是二维码 const authorization: AuthorizationEndpointHandler = { - url: 'http://localhost:3000/auth/qrcode', + url: endpointUrl.toString(), params: { - appid: clientId, + client_id: wechatMpApi.appId, response_type: 'code', - state: 'wechatmp', + action: 'qrcode', }, } // 从callback中获得state,code 然后进一步获取 - const token: TokenEndpointHandler = () => { - return { - url: 'http://localhost:3000/wechatmp/token', - async request({ code }: { code: string }) { - // 通过code获取openid,并注销code - return { - access_token: 'access_token 从缓存中获得token', - } - }, - } + const token: TokenEndpointHandler = { + url: endpoint, + params: { + action: 'token', + }, } const profile = (profile: WechatMpProfile) => { @@ -103,20 +138,189 @@ export default function WeChatMp

( } } + const userinfo: UserinfoEndpointHandler = { + url: 'http://localhost:3000/auth/qrcode2', + async request({ tokens }: { tokens: { access_token: string } }) { + return { + openid: tokens.access_token, + } + }, + } + + async function GET(request: Request): Promise { + const link = new URL(request.url) + const action = link.searchParams.get('action') + const redirectUri = link.searchParams.get('redirect_uri') + // 微信消息验证 + const timestamp = link.searchParams.get('timestamp') + const nonce = link.searchParams.get('nonce') + const signature = link.searchParams.get('signature') + const echo = link.searchParams.get('echostr') + if (timestamp && nonce && signature && echo) { + if (messageServicde.checkSign({ timestamp, nonce, signature })) { + return new Response(echo) + } + return new Response('验证失败', { status: 405 }) + } + if (action === 'qrcode') { + const code = await captchaManager.generate() + let imgLink = qrcodeImageUrl + if (checkType === 'QRCODE') { + const t = await messageServicde.createPermanentQrcode(code) + imgLink = `https://zddydd.com/qrcode/build?label=&logo=0&labelalignment=center&foreground=%23000000&background=%23ffffff&size=300&padding=10&logosize=50&labelfontsize=14&errorcorrection=medium&text=${encodeURI(t.url)}` + } + const html = ` + + + 微信公众号登录 + + +

+

请使用微信扫描二维码登录

+ +
+ + + + + ` + return new Response(html, { + headers: { + 'Content-Type': 'text/html', + }, + }) + } + + return Response.json({ data: 1 }) + } + async function POST(request: Request): Promise { + // 微信消息验证 + const link = new URL(request.url) + + const action = link.searchParams.get('action') + if (action === 'token') { + const data = await request.formData() + const valid = await captchaManager.validCode( + data.get('code')?.toString() ?? '', + ) + if (valid?.openid) { + return Response.json({ + scope: 'openid', + access_token: valid.openid, + token_type: 'bearer', + }) + } + return Response.json({ + error: 'invalid_grant', + error_description: '验证码错误', + }) + } else if (action === 'check') { + const { code } = await request.json() + const valid = await captchaManager.validCode(code) + if (valid?.openid) { + return Response.json({ type: 'success' }) + } + return Response.json({ type: 'fail' }) + } + + const timestamp = link.searchParams.get('timestamp') + const nonce = link.searchParams.get('nonce') + const signature = link.searchParams.get('signature') + const echo = link.searchParams.get('echostr') + if (timestamp && nonce && signature && echo) { + if (messageServicde.checkSign({ timestamp, nonce, signature })) { + return new Response(echo) + } + return new Response('验证失败', { status: 405 }) + } + // 获得xml消息报 + const msg_signature = request.headers.get('msg_signature') + const body = await request.text() + const message = messageServicde.parserInput(body, { + timestamp, + nonce, + signature: msg_signature ?? signature, + }) + let content = '' + if (message.MsgType == 'event') { + content = message.EventKey.replace('qrscene_', '') + } else if (message.MsgType == 'text') { + content = message.Content.trim() + } + + const status = await captchaManager.complted(content, { + openid: message.FromUserName, + }) + const result = messageServicde.renderMessage({ + ToUserName: message.FromUserName, + FromUserName: message.ToUserName, + CreateTime: Math.floor(Date.now() / 1000), + MsgType: 'text', + Content: status ? '登录成功' : '登录失败,请重新获得验证码', + }) + + return new Response(result, { + headers: { + 'Content-Type': 'application/xml', + }, + }) + } + + if (!checkPrint()) { + console.log('[auth.js/微信公众号登录插件]') + console.log('请注意以下参数') + console.log(`微信端消息回调:${endpoint}`) + console.log(`微信端消息验证类型:${options.checkType}`) + } + return { + GET, + POST, account, + clientId: wechatMpApi.appId, + clientSecret: 'TEMP', id: 'wechatmp', - name: '微信公众号关注登录', - type: 'oauth', + name: '微信公众号登录', + type: 'oauth' as const, style: { logo: '/providers/wechatOfficialAccount.svg', bg: '#fff', text: '#000', }, - checks: ['none'], + userinfo, + checks: ['none'] as ['none'], authorization, token, - userinfo, profile, } } diff --git a/packages/wechatmp/src/lib/CaptchaManager.ts b/packages/wechatmp/src/lib/CaptchaManager.ts new file mode 100644 index 0000000..657267f --- /dev/null +++ b/packages/wechatmp/src/lib/CaptchaManager.ts @@ -0,0 +1,93 @@ +export type CaptchaManagerConfig = { + /** + * 过期时间 + * @default 60000 (1min) + */ + expireTime: number + /** + * 验证码长度 + * @default 6 + */ + length: number +} +export class CaptchaManager< + T = { + openid: string + unionid?: string + }, +> { + private options: CaptchaManagerConfig + private cache: Map = new Map() + + constructor(options?: CaptchaManagerConfig) { + this.options = options || { + expireTime: 60000, + length: 6, + } + } + + /** + * 生成验证码 + */ + generate(code?: string): string { + this.cleanupExpired() + const captcha = + code ?? + Math.random() + .toString() + .substring(2, this.options.length + 2) + this.cache.set(captcha, { expireAt: Date.now() + this.options.expireTime }) + return captcha + } + + /** + * 更新验证码绑定的数据 + * @param captcha + * @param data + */ + complted(captcha: string, data: T) { + if (this.cache.has(captcha)) { + const entry = this.cache.get(captcha) + if (entry && entry.expireAt > Date.now()) { + entry.data = data + return true + } + } + return false + } + + /** + * 获取验证码绑定的数据 + * @param captcha + */ + data(captcha: string) { + const entry = this.cache.get(captcha) + if (entry && entry.expireAt > Date.now()) { + return entry.data + } + } + /** + * 校验验证码 + * @param captcha + * @returns + */ + async validCode(captcha: string): Promise { + const entry = this.cache.get(captcha) + if (entry && entry.expireAt > Date.now()) { + return entry.data + } + throw new Error('验证码不存在') + } + + /** + * 清理过期的验证码 + */ + private cleanupExpired(): void { + const now = Date.now() + for (const [captcha, entry] of this.cache.entries()) { + if (entry.expireAt <= now) { + this.cache.delete(captcha) + } + } + } +} diff --git a/packages/wechatmp/src/lib/Handle.ts b/packages/wechatmp/src/lib/Handle.ts new file mode 100644 index 0000000..e69de29