wechat(weixin) oauth login helper library, based on react + weUI v1.0.0 + react-router-manage
npm i @rwsbillyang/wxlogin
- 支持微信OAuth
- 支持企业微信OAuth
- 支持用户名密码登录
- 支持扫码登录,包括使用微信或企业微信
- 支持可配置的登录方式,包括哪个公众号和企业微信、登录方式,获取用户信息方式
- 获取用户信息方式,包括不获取,没有则获取,没有头像昵称则获取,根据配置获取,强行获取等方式
- 支持自定义的路径权限控制
- 支持微信js sdk自定义分享
- 支持通过js-sdk自动config注入验证,获取网络状态等
- 支持登录认证信息的存储方式:localStorage、sessionStorage, or both
//MyRoutes.ts
export const kfRoutes: RouteTypeI[] = [
//...
{
name: 'messageList',
path: '/wx/admin/work/kf/messageList',
component: MessageList,
beforeEnter: beforeEnter,
title: "对话记录"
},
{
name: 'customerInfo',
component: WxCustomerInfoPage,
path: '/wx/admin/work/kf/customerInfo', //用于聊天工具栏
beforeEnter: beforeEnter,
//code: ["admin"], //优先使用code
meta:{ "needWxOauth": NeedWxOauth.Yes },
title: "工具栏-对话记录"
},
]
//App.tsx
const routerConfig = defineRouterConfig({
basename: '/',
autoDocumentTitle: true,
//LoadingComponent: ()=><div>loading</div>
routes: kfRoutes.concat(wxUserLoginRoutes), // 请查看下方路由配置 routes
})
export const App = () => <MRouter routerConfig={routerConfig}>
{(children) => children}
</MRouter>
注意:必须添加进wxlogin中的路由:yourAppRoutes.concat(wxUserLoginRoutes)
import { parseUrlQuery } from "@rwsbillyang/wxlogin"
//...
// https://gitee.com/wx_504ae56474/react-jwchat/
export const MessageList: React.FC<{externalId?: string}> = (props) => {
const query = parseUrlQuery()
const externalId = props.externalId || query["externalId"]
//...
}
import { ErrMsg, LoadingToast, useWxJsSdk, WxJsStatus } from '@rwsbillyang/wxlogin';
import React, { useEffect, useState } from 'react';
import { MessageList } from './MessageList';
export const WxCustomerInfoPage: React.FC = (props: any) => {
const [msg, setMsg] = useState<string|undefined>()
const [externalId, setExternalId] = useState<string|undefined>()
const {status, errMsg} = useWxJsSdk()
useEffect(()=>{
if(status === WxJsStatus.WxWorkAgentConfigDone){
getContext()
}else{
console.log("ignore status: "+status)
if(status < 0){
console.log("something wrong: " + errMsg)
}
}
}, [status])
console.log("status="+status+", msg="+msg + ", url="+ window.location.href)
const getContext = () => {
console.log("invoke getContext...")
wx.invoke('getContext', {
}, function (res) {
if (res.err_msg == "getContext:ok") {
const entry = res.entry; //返回进入H5页面的入口类型,目前有normal、contact_profile、single_chat_tools、group_chat_tools、chat_attachment
//const shareTicket = res.shareTicket; //可用于调用getShareInfo接口
if ("single_kf_tools" === entry || "single_chat_tools" == entry) {
wx.invoke('getCurExternalContact', {
}, function (res) {
if (res.err_msg == "getCurExternalContact:ok") {
const external = res.userId//返回当前外部联系人userId
if(external){
console.log("external="+external)
//const url = `/wx/admin/work/kf/customerInfo?externalId=${external}`
//window.location.href = url
setExternalId(external)
}else{
setMsg("getCurExternalContact:ok, but no externalId")
}
} else {
const err = "getCurExternalContact: res.err_msg=" + res.err_msg
console.log(err)
setMsg(err)
}
});
}else{
console.log("TODO: entry="+entry)
setMsg("TODO: entry="+entry)
}
} else {
const err = "getContext: res.err_msg=" + res.err_msg
console.log(err)
setMsg(err)
}
});
}
return externalId? <MessageList externalId={externalId}/>
: (errMsg || msg ? <ErrMsg errMsg={errMsg || msg}/>: <LoadingToast text="请稍候..." /> )
}
各参数可以如下,都是可选的
/***
* securedRoute保护的route,通过checkAdmin提取的参数,然后传递给各LoginComponent
*/
export interface LoginParam {
appId?: string //公众号
corpId?: string //企业微信
suiteId?: string //企业微信ISV
agentId?: string //企业微信
from?: string //需要登录的页面
owner?: string //用于公众号 用于判断用户设置是否获取用户信息
needUserInfo: number // 用于公众号 或企业微信, default value: NeedUserInfoType.Force_Not_Need = 0
authStorageType?: number //authBean存储类型, default value: StorageType.BothStorage
}
示例如下:
/wx/admin/home?corpId=ww5f4c472a66331eeb&agentId=1000005&loginType=" + LoginType.ACCOUNT
/cms/admin/home?appId=wxc94c93e0938a84bb
注意,LoginParam中的owner参数,需要在路由中指定字符串为:uId,如
/n/:uId/d/:id
LoginType通常可以为account、wechat、wxWork
//登录类型,与后端保持一致
export const LoginType = {
ACCOUNT: "account", //账户密码
MOBILE: "mobile", // 验证码,暂不支持
WECHAT: "wechat", //微信登录, default value
WXWORK: "wxWork", //企业微信登录
SCAN_QRCODE: "scanQrcode", //扫码或企业微信扫码登录,根据指定的参数appId/corpId&agentId
WXWORK_SUITE: "wxWork_isv",
WXMINI : "wxMini"
}
默认配置下,当路径中包含某些特征符(下面的characters指定的)时,需要用户具备roles roles为数组,只要登录后的用户含有任一个role,及具备访问权限
/**
* 根据该配置是否需要登录, 会被自定义的permitRoles覆盖
*/
adminPathRoles: [
{
characters: "/super/admin/",
roles: ["root"]
},
{
characters: "/admin/",
roles: ["root", "admin"]
},
{
characters: "/dev/",
roles: ["dev"]
},
{
characters: "/user/",
roles: ["root", "admin", "user"]
},
]
不指定,将使用上面的默认配置,可通过下面方式指定自己的权限配置:
WxLoginConfig.adminPathRoles = your_customize_roles_array
当路径中不包含任何上面配置中的特殊路径时,就是普通路径。普通路径往往不需要任何特殊权限,主要用于oauth认证,认证完后也不需再登录。
拦截它,主要用户获取用户的唯一识别ID(如openId),或提示用户授权,获取其信息。
如需要进行oauth或login的路径,需要指定beforeEnter:
export const kfRoutes: RouteTypeI[] = [
{
name: 'customerInfo',
component: WxCustomerInfoPage,
path: '/wx/admin/work/kf/customerInfo', //用于聊天工具栏
beforeEnter: beforeEnter,
//code: ["admin"], //优先使用code
meta:{ "needWxOauth": NeedWxOauth.Yes },
title: "工具栏-对话记录"
},
]
其它属性可参考react-router-manage
meta在特殊路径中无意义,对于普通路径,有三种类型的值:
/**
* 仅适用于无需roles特权时的普通路径,是否需微信登录
*/
export const NeedWxOauth = {
Yes: 2, //是
OnlyWxEnv: 1, //仅仅微信环境下
No: 0 //直接跳走,无需微信登录
}
- Yes:表示会进入oauth认证流程
- OnlyWxEnv:仅浏览器类型是微信或企业微信会进入认证流程
- No:不进入认证流程,与不用securedRoute保护唯一区别是,securedRoute会设置corpParams参数,作为usecache的缓存空间
当进入oauht认证流程时,会检查缓存(localStorage或sessionStorage)中是否有相关的auth信息:
- 若已存在登录信息,则直接跳转到要登录的页面,无需进入登录页面。特殊路径会检查是否具备权限,普通路径检查是否具备用户信息,如是否有openId。
- 若不存在登录信息,则进入相应的登录界面:
- 若路径中指定了loginType,则按指定进入登录界面;若未指定,则根据浏览器类型进行判断:
- 若是微信环境,进入WxOauthLoginPageOA进行oauth认证
- 若是企业微信环境,进入WxOauthLoginPageWork进行oauth认证
- 未指定,则进入用户名/密码登录界面:UserPwdLoginPage
- 若路径中指定了loginType,则按指定进入登录界面;若未指定,则根据浏览器类型进行判断:
当sessionStorage或localStorage中具备用户ID等用户信息,或auth登录信息后,直接跳转到目的页面,否则进入登录认证流程,登录认证成功后,会写入缓存中,以备下次校验使用。
注意:公众号后台中回调域名需设置为后端服务器api域名,因为回调的是api服务器,不是前端域名
- 第一阶段
跳转到WxOauthLoginPageOA页面后,将首先检查使用的公众号appId是否存在,若不存在,在页面上提示出错后结束oauth过程。
接着,记录下from,便于后面跳回去
最后,根据NeedUserInfoType拼接微信open connect路径,一种是无需用户授权的静默登录(只能拿取到openId),一种是需用户授权(可获得昵称、头像、性别、地区等信息)
typescript
* 前后端共同协调完成下列策略,后端已在ktorKit中实现
* 若已有登录信息,NeedIfNo...NeedByUserSettings则不生效,因为发现已登录则跳过直接进入
* */
export const NeedUserInfoType = {
Force_Not_Need : 0, // 明确不需要
NeedIfNo : 1, //尽量不获取,有fan记录(不管有没有头像或昵称)就不获取,没有fan记录时则获取
NeedIfNoNameOrImg : 2, //尽量不获取,有fan记录、且有头像和昵称则不获取,但没有头像或名称获取
NeedByUserSettings : 3, //由后端用户配置是否获取,后端用户 由参数owner指定
ForceNeed : 4 //直接进入step2,用户授权获取信息操作
}
第一种,明确不需要用户授权,只获取openId即结束。
最后一种,直接拼接成用户授权的url,然后跳转过去。
中间几种,拼接成无需授权的url,然后服务器端根据needUserInf参数决定是否再进入获取用户授权的流程
- 第2阶段
后端接收到腾讯的通知(拼接url时,redirect_uri参数确定回调url,包括needUserInfo等编码进)后,解析出url路径中提供的参数,确定是否需要获取用户信息,并编码进query参数中,重定向到前端页面路径(页面WxOauthNotifyOA)中。
在WxOauthNotifyOA中,解析出query参数,包括是openId、是否进入获取用户信息的标志(needEnterStep2 === '2'’),若需进入,需要再次拼接成需要用户授权的腾讯open connect url,进入用户授权流程。
此阶段会将用户的openId等信息存储到缓存中。
- 第3阶段
再次检查目的url是否特殊路径,是否需要admin等特殊权限,若是,则需根据openId信息,调用login进行账户登录;
若是首次登录,后端会创建一个wxOaAccount账户,必要的话,可以绑定到全局系统账户上
若是登录之前,检测到缓存中,有scanQrcodeId信息,则为扫码登录,后端据此将登录后的auth认证信息,通过wssocket发送给前端的桌面socket链接
- 第一阶段
跳转到WxOauthLoginPageWork,并记录下来源from路径,便于结束后跳回
检查是否具备corpId或suiteId参数,都没有则提示出错
拼接微信open connect url,并跳转过去
- 第二阶段
根据第一阶段拼接的url回调参数redirect_uri,腾讯回调redirect_uri,后端将获取到的openID/userId/corpId等结果信息,重定向到前端的WxOauthNotifyWork页面
- 第三阶段
前端的WxOauthNotifyWork解析出query结果参数,作为guest信息保存到缓存中; 若是特殊路径,需权限,则进行login登录
登录前检查缓存中是否有scanQrcodeId信息,有的话则为扫码登录,后端据此将登录后的auth认证信息,通过wssocket发送给前端的桌面socket链接,桌面端登录成功。
不指定loginType且非微信环境,自动进入用户名和密码登录
页面为UserPwdLoginPage,使用的是系统级账号,需存在该账户才能登录成功
- step1 当PC端,明确指定loginType为scanQrcode时,PcShowQrcodePage页面与server建立wssocket,得到一个sessionId,将loginParam等信息编码进url,生成一个二维码,手机端微信或企业微信扫码后,解析出loginParam参数。
Step2 手机端扫码,进入WxScanQrcodeLoginConfirmPage,进行登录确认。 若用户取消了登录,则调用server,告知用户取消了,server再通过ws socket通知桌面端,用户取消了操作。
若用户确认登录,则设置好scanQrcodeId到缓存中,跳转到WxScanQrcodeLoginDonePage页面,该页面由wxlogin中的下面路由防护:
{
name: 'scanQrcodeLoginDone',
path: '/wx/scanLogin/user/done',
component: WxScanQrcodeLoginDonePage,
beforeEnter: beforeEnter,
meta:{ "needWxOauth": NeedWxOauth.Yes }
},
并将登录参数传递给WxScanQrcodeLoginDonePage,将按照前述的微信oauth流程进行登录。 前述的oauth流程中若检测到scanQrcodeId,则传递给后端,后端将auth结果信息发送给ws socket,桌面端收到后跳转到目的界面。
集成了微信js sdk的使用,需在html页面中提前引入js-sdk文件
调用useWxJsSdk将进行config注入,可使用微信js-sdk,返回状态信息和网络状态
export function useWxJsSdk(jsapiList: string[] = defaultJsApiList): { status, networkType }
返回结果为:{ status, networkType } //状态信息,以及网络状态
设置自定义分享:
/**
* 设置转发时的自定义分享信息 依赖于wxInit初始化结束、click返回的objectId、article查询完毕
* 初始化转发自定义分享信息,article为空则直接返回
*/
export function setRelayShareInfo(status: number, mId: string, title: string, brief?: string, img?: string)