Skip to content

rwsbillyang/wxlogin

Repository files navigation

1. wxlogin

wechat(weixin) oauth login helper library, based on react + weUI v1.0.0 + react-router-manage

npm i @rwsbillyang/wxlogin

1.1. Features

  1. 支持微信OAuth
  2. 支持企业微信OAuth
  3. 支持用户名密码登录
  4. 支持扫码登录,包括使用微信或企业微信
  5. 支持可配置的登录方式,包括哪个公众号和企业微信、登录方式,获取用户信息方式
  6. 获取用户信息方式,包括不获取,没有则获取,没有头像昵称则获取,根据配置获取,强行获取等方式
  7. 支持自定义的路径权限控制
  8. 支持微信js sdk自定义分享
  9. 支持通过js-sdk自动config注入验证,获取网络状态等
  10. 支持登录认证信息的存储方式:localStorage、sessionStorage, or both

1.2. Usage

1.2.1. Config Routes

//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)

1.2.2. Get parameters in page

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"]

    //...
}

1.2.3. use wx js-sdk in page

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="请稍候..." /> )
        
}

2. 内部实现

2.1. 路径

2.1.1. 路径query参数

各参数可以如下,都是可选的

/***
 * 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" 
  }

2.1.2. 特殊路径

默认配置下,当路径中包含某些特征符(下面的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

2.1.3. 普通路径

当路径中不包含任何上面配置中的特殊路径时,就是普通路径。普通路径往往不需要任何特殊权限,主要用于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

2.2. oauth

当sessionStorage或localStorage中具备用户ID等用户信息,或auth登录信息后,直接跳转到目的页面,否则进入登录认证流程,登录认证成功后,会写入缓存中,以备下次校验使用。

注意:公众号后台中回调域名需设置为后端服务器api域名,因为回调的是api服务器,不是前端域名

2.2.1. 微信oauth

  • 第一阶段

跳转到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链接

2.2.2. 企业微信oauth

  • 第一阶段

跳转到WxOauthLoginPageWork,并记录下来源from路径,便于结束后跳回

检查是否具备corpId或suiteId参数,都没有则提示出错

拼接微信open connect url,并跳转过去

  • 第二阶段

根据第一阶段拼接的url回调参数redirect_uri,腾讯回调redirect_uri,后端将获取到的openID/userId/corpId等结果信息,重定向到前端的WxOauthNotifyWork页面

  • 第三阶段

前端的WxOauthNotifyWork解析出query结果参数,作为guest信息保存到缓存中; 若是特殊路径,需权限,则进行login登录

登录前检查缓存中是否有scanQrcodeId信息,有的话则为扫码登录,后端据此将登录后的auth认证信息,通过wssocket发送给前端的桌面socket链接,桌面端登录成功。

2.2.3. 用户名密码登录

不指定loginType且非微信环境,自动进入用户名和密码登录

页面为UserPwdLoginPage,使用的是系统级账号,需存在该账户才能登录成功

2.2.4. 扫码登录

  • 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,桌面端收到后跳转到目的界面。

2.3. wx share

集成了微信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)

About

wechat(weixin) oauth login

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published