一、前言
公司作为企业微信第三方应用服务商,想要获取到授权企业的通讯录信息中的用户信息和部门信息,以及授权企业的成员可以扫码进行登录。
二、流程梳理
1.后台配置
红色箭头标记的是最基本的需要的东西。
可信域名:设置可信域名后支持应用的OAuth2授权、JSSDK调用、通讯录名称转义等大部分操作都必须在这个域名下发起才能成功。
指令回调URL:系统将会把此应用的授权变更事件、ticket参数、通讯录变更信息等推送给此URL
TOKEN:主要是用来进行校验回调请求的合法性。
EncodingAESKey:回调消息加解密参数,是AES密钥的Base64编码,用于解密回调消息内容对应的密文。后续所有托管的企业产生的回调消息都使用该值来解密。
这里的TOKEN和EncodingAESKey主要是用来校验数据回调URL和指令回调URL是否能够被企业微信服务器调用成功。
校验URL
企业微信验证URL是否能够被调用是GET请求,各项信息的通知是相同URL下的POST请求。也就是需要用配置的URL
写一个GET用来校验,一个POST请求接受信息。
校验信息
router.GET("/wx/cmdback", func(c *gin.Context) {/*数据回调验证*///企业微信加密签名msgSignature := cast.ToString(c.Query("msg_signature"))//时间戳timestamp := cast.ToInt64(c.Query("timestamp"))//随机数nonce := cast.ToString(c.Query("nonce"))//加密字符串echostr, err := url.PathUnescape(c.Query("echostr"))if err != nil {fmt.Println("url解码失败")return}//企业微信api 详情可以看企业微信文档wxcpt := wxbizmsgcrypt.NewWXBizMsgCrypt(model.Token, model.EncodingAesKey, model.CorpId, wxbizmsgcrypt.XmlType)echoStr, cryptErr := wxcpt.VerifyURL(msgSignature, cast.ToString(timestamp), nonce, echostr)if nil != cryptErr {fmt.Println("verifyUrl fail", cryptErr)return}fmt.Println("verifyUrl success echoStr")c.String(util.Success, string(echoStr))//返回解密出来的字符串给企业微信校验})
接收信息
router.POST("/wx/cmdback", func(c *gin.Context) {/*指令回调URL: 微信服务器推送suite_ticket以及安装应用时推送auth_code时。*///企业微信加密签名msgSignature := cast.ToString(c.Query("msg_signature"))//时间戳timestamp := cast.ToString(c.Query("timestamp"))//随机数nonce := cast.ToString(c.Query("nonce"))// post请求的密文数据defer c.Request.Body.Close()con, _ := ioutil.ReadAll(c.Request.Body) //获取post的数据// 访问应用和企业回调传不同的IDwxcpt := wxbizmsgcrypt.NewWXBizMsgCrypt(model.Token, model.EncodingAesKey, model.SuitId, wxbizmsgcrypt.XmlType)msg, cryptErr := wxcpt.DecryptMsg(msgSignature, timestamp, nonce, con)if nil != cryptErr {fmt.Println("DecryptMsg fail", cryptErr)return}fmt.Println(string(msg))var content MsgContentxml.Unmarshal(msg, &content)var changeContent ChangeContentxml.Unmarshal(msg, &changeContent)fmt.Println(changeContent)//业务逻辑,根据信息需要进行的业务逻辑'''c.String(util.Success, "success")//需要返回"success"不然企业微信认为此次请求错误
})
2.安装应用
- 企业首先需要安装第三方应用和授权,企业有两种方式进行安装授权。从第三方服务商发起授权比应用商店授权相比,无非是
需要第三方服务商自己生成跳转到微信授权网站的链接
。
- 从第三方服务商
(1) 获取第三方应用凭证(suite_access_token)
企业微信服务器会定时(每十分钟)推送ticket。ticket会实时变更,并用于后续接口的调用。
请求地址: https://qyapi.weixin.qq.com/cgi-bin/service/get_suite_token
请求包体:
{"suite_id":"wwddddccc7775555aaa" ,"suite_secret": "ldAE_H9anCRN21GKXVfdAAAAAAAAAAAAAAAAAA", "suite_ticket": "Cfp0_givEagXcYJIztF6sfbdmIZCmpaR8ZBsvJEFFNBrWmnD5-CGYJ3_NhYexMyw"
}
参数说明:
参数 | 是否必须 | 说明 |
---|---|---|
suite_id | 是 | 以ww或wx开头应用id(对应于旧的以tj开头的套件id) |
suite_secret | 是 | 应用secret |
suite_ticket | 是 | 企业微信后台推送的ticket |
代码实现:
func GetSuiteAccessToken(ticket string) {result, err := util.Post(util.PostSuiteAccessToken, util.WxSuite{SuiteId: model.SuitId, SuiteTicket: ticket, SuiteSecret: model.SuitSecret})if err != nil {fmt.Println("Post Suite Token Fail")}var wxRspSuiteToken util.WxRspSuiteTokenerr = json.Unmarshal([]byte(result), &wxRspSuiteToken)if err != nil {fmt.Println(err)}if wxRspSuiteToken.ErrCode == 0 {model.GetRedis().Set("suiteAccessToken", wxRspSuiteToken.SuiteAccessToken, time.Minute*30)fmt.Println("获取成功,suiteAccessToken==>", cast.ToString(wxRspSuiteToken.SuiteAccessToken))} else {fmt.Println("errCode==>", wxRspSuiteToken.ErrCode)fmt.Println("errMsg==>", wxRspSuiteToken.ErrMsg)}
}
获取到的suite_access_token有效期为2小时,应当进行缓存,接下来各项操作需要携带。
(2) 获取预授权码
请求方式: GET(HTTPS)
请求地址: https://qyapi.weixin.qq.com/cgi-bin/service/get_pre_auth_code?suite_access_token=SUITE_ACCESS_TOKEN
参数说明:
参数 | 是否必须 | 说明 |
---|---|---|
suite_access_token | 是 | 第三方应用access_token,最长为512字节 |
返回结果:
{"errcode":0 ,"errmsg":"ok" ,"pre_auth_code":"Cx_Dk6qiBE0Dmx4EmlT3oRfArPvwSQ-oa3NL_fwHM7VI08r52wazoZX2Rhpz1dEw","expires_in":1200
}
代码实现:
/*预授权码
*/
func GetAuthCode(suiteAccessToken string) string {data, err := util.Get(util.GetAuthCode + "?suite_access_token=" + suiteAccessToken)if err != nil {fmt.Println("预授权码wx请求失败")return ""}var rsp util.WxRspAuthorizationCodeerr = json.Unmarshal(data, &rsp)if err != nil {fmt.Println("预授权码解析失败")return ""}if rsp.ErrCode != 0 {fmt.Println("ErrorCode==>", rsp.ErrCode)fmt.Println("ErrMsg==>", rsp.ErrMsg)return ""}return rsp.PreAuthCode}
*注意:因为是测试应用,还未进行发布,所以要对预授权设置授权,让其支持测试,正式应用不需要此操作。
设置授权配置
该接口可对某次授权进行配置。可支持测试模式(应用未发布时)。
请求方式: POST(HTTPS)
请求地址: https://qyapi.weixin.qq.com/cgi-bin/service/set_session_info?suite_access_token=SUITE_ACCESS_TOKEN
请求包体:
{"pre_auth_code":"xxxxx","session_info":{"appid":[1,2,3],"auth_type":1}
}
参数说明:
参数 | 是否必须 | 说明 |
---|---|---|
suite_access_token | 是 | 第三方应用access_token |
pre_auth_code | 是 | 预授权码 |
session_info | 是 | 本次授权过程中需要用到的会话信息 |
appid | 否 | 允许进行授权的应用id,如1、2、3, 不填或者填空数组都表示允许授权套件内所有应用(仅旧的多应用套件可传此参数,新开发者可忽略) |
auth_type | 否 | 授权类型:0 正式授权, 1 测试授权。 默认值为0。注意,请确保应用在正式发布后的授权类型为“正式授权” |
返回结果:
{"errcode": 0,"errmsg": "ok"
}
代码实现:
PreAuthCode := GetAuthCode(suiteAccessToken)
//应用未发布时。测试
req := map[string]interface{}{"pre_auth_code": PreAuthCode,"session_info": map[string]interface{}{"auth_type": 1,},
}
util.Post("https://qyapi.weixin.qq.com/cgi-bin/service/set_session_info"+"?suite_access_token="+suiteAccessToken, &req)
(3) 获取永久授权码
该API用于使用临时授权码换取授权方的永久授权码,并换取授权信息、企业access_token,临时授权码一次有效。
请求方式: POST(HTTPS)
请求地址: https://qyapi.weixin.qq.com/cgi-bin/service/get_permanent_code?suite_access_token=SUITE_ACCESS_TOKEN
请求包体:
{"auth_code": "auth_code_value"
}
代码实现:
/*永久授权码
*/
func GetPermanentCode(suiteAccessToken string, authCode string) (string, util.AuthCorpInfo) {post, err := util.Post(util.PostPermanentCode+"?suite_access_token="+suiteAccessToken, util.WxAuthCode{AuthCode: authCode})if err != nil {fmt.Println("Post PermanentCode Fail")return "", util.AuthCorpInfo{}}var wxAuthCode util.WxRspAuthCodeerr = json.Unmarshal([]byte(post), &wxAuthCode)if err != nil {fmt.Println("解析失败")return "", util.AuthCorpInfo{}}if wxAuthCode.ErrCode != 0 {fmt.Println(wxAuthCode.ErrCode)fmt.Println(wxAuthCode.ErrMsg)return "", util.AuthCorpInfo{}} else {return wxAuthCode.PermanentCode, wxAuthCode.AuthCorpInfo}
}
当企业成功安装应用后,企业微信会通过第三方应用后台配置的数据回调URL,将企业的各项信息返回,开发者可以根据自己需要存储信息,最重要的存储授权企业的id(corp_id)
和永久授权码(permanent_code)
,后面需要根据指定企业的id,取到相应的永久授权码,从而获取到能获取到指定企业的access_token
。
3.获取通讯录信息
*注意:在企业安装应用时,在同意安装页面记得需要可见范围范围,不然获取不到整个企业的部门和成员信息
(1)获取企业凭证(access_token)
第三方服务商在取得企业的永久授权码后,通过此接口可以获取到企业的access_token。
获取后可通过通讯录、应用、消息等企业接口来运营这些应用。
此处获得的企业access_token与企业获取access_token拿到的token,本质上是一样的,只不过获取方式不同。获取之后,就跟普通企业一样使用token调用API接口。
*注意:因权限问题获取的信息可能不同。如三方无法直接获取用户名称等
请求方式: POST(HTTPS)
请求地址: https://qyapi.weixin.qq.com/cgi-bin/service/get_corp_token?suite_access_token=SUITE_ACCESS_TOKEN
请求包体:
{"auth_corpid": "auth_corpid_value","permanent_code": "code_value"}
参数说明:
参数 | 是否必须 | 说明 |
---|---|---|
auth_corpid | 是 | 授权方corpid |
permanent_code | 是 | 永久授权码,通过get_permanent_code获取 |
返回结果:
{"errcode":0 ,"errmsg":"ok" ,"access_token": "xxxxxx", "expires_in": 7200
}
参数说明:
参数 | 说明 |
---|---|
access_token | 授权方(企业)access_token,最长为512字节 |
expires_in | 授权方(企业)access_token超时时间 |
代码实现:
/*第三方应用获取指定企业凭证*/
func GetAccessToken(companyId int) string {var company model.Companymodel.GetDB().Where("company_id = ?", companyId).First(&company)if company.CompanyId == 0 {fmt.Println("公司匹配失败")}preCode := company.PermanentCodeCorpId := company.CorPidsuitToken := model.GetRedis().Get("suiteAccessToken").Val()wxReq := util.WxReqDepartment{AuthCorpid: CorpId, PermanentCode: preCode}data, err := util.Post(util.PostAccessToken+"?suite_access_token="+suitToken, wxReq)if err != nil {fmt.Println(err)}var wxAccessToken util.WxRspAccessTokenjson.Unmarshal([]byte(data), &wxAccessToken)fmt.Println(wxAccessToken)if wxAccessToken.ErrCode != 0 {fmt.Println(wxAccessToken.ErrMsg)return ""} else {fmt.Println("获取成功,AccessToken==>", wxAccessToken.AccessToken)return wxAccessToken.AccessToken}
}
拿到access_token需要先缓存起来,因为企业微信做了频率校验,超过次数将无法获取access_token,token有效期为两个小时,两个小时内获取到的access_token是一样的。
有了token就可以访问通讯录的各项信息。
(2)获取部门成员详情
请求方式: GET(HTTPS)
请求地址: https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID&fetch_child=FETCH_CHILD
参数说明:
参数 | 必须 | 说明 |
---|---|---|
access_token | 是 | 调用接口凭证 |
department_id | 是 | 获取的部门id |
fetch_child | 否 | 1/0:是否递归获取子部门下面的成员 |
权限说明:
应用须拥有指定部门的查看权限。
返回结果:
{"errcode": 0,"errmsg": "ok","userlist": [{"userid": "userid","name": "name","department": [1],"gender": "0","avatar": "https://rescdn.qqmail.com/node/wwmng/wwmng/style/images/independent/DefaultAvatar$73ba92b5.png","status": 1,"order": [0],"main_department": 1,"is_leader_in_dept": [],"thumb_avatar": "https://rescdn.qqmail.com/node/wwmng/wwmng/style/images/independent/DefaultAvatar$73ba92b5.png","open_userid": "","direct_leader": []},]
}
第三方应用能拿到企业成员的信息不多,详情可以参照企业微信文档https://developer.work.weixin.qq.com/document/path/90337
(3)获取部门列表
请求方式: GET(HTTPS)
请求地址: https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=ACCESS_TOKEN&id=ID
参数说明 :
参数 | 必须 | 说明 |
---|---|---|
access_token | 是 | 调用接口凭证 |
id | 否 | 部门id。获取指定部门及其下的子部门(以及子部门的子部门等等,递归)。 如果不填,默认获取全量组织架构 |
权限说明:
只能拉取token对应的应用的权限范围内的部门列表
返回结果:
{"errcode": 0,"errmsg": "ok","department": [{"id": 2,"name": "广州研发中心","name_en": "RDGZ","department_leader":["zhangsan","lisi"],"parentid": 1,"order": 10},{"id": 3,"name": "邮箱产品部","name_en": "mail","department_leader":["lisi","wangwu"],"parentid": 2,"order": 40}]
}
(4)通讯录转义
当第三方拉取到信息时会发现,部门名和成员名都等于部门id和成员id,无法显示真实的名称,主要是因为企业微信为了保护隐私,第三方将不再直接获取到授权企业的通讯录数据(接口将不再返回人名与部门名)。
所以开发者如果需要展示名称就需要对名称进行转义,有两种方法可以进行转义。
通讯录组件转换名称
这种方法主要是前端实现,后端只需要返回前端需要的相关配置信息。
详情参照企业微信文档 https://developer.work.weixin.qq.com/document/path/91958
通讯录ID转义
需要前后端配合实现,详情参照企业微信文档 https://developer.work.weixin.qq.com/document/path/91889
实现流程
实现步骤
1.需要先创建一个文件(txt,csv,xls,xlsx,doc,docx),并替换模板,效果如下图所示:
要获取部门名称同理,模板如下:
$departmentName=DEPARTMENT_ID$
$userName=USERID$
将你要查的id替换大写英文的部分,只能转义同一公司的id,不然不会进行转义。
2.上传文件到企业微信,这里是在后端进行上传。
client := http.Client{}
bodyBuf := &bytes.Buffer{}
bodyWrite := multipart.NewWriter(bodyBuf)
//上传给企业微信
//创建请求并设置内容类型
fileWrite, err := bodyWrite.CreateFormFile("media", "file.xlsx")
_, err = io.Copy(fileWrite, excel)
if err != nil {log.Println("err")
}
bodyWrite.Close() //要关闭,会将w.w.boundary刷写到w.writer中
// 创建请求
contentType := bodyWrite.FormDataContentType()
req, err := http.NewRequest(http.MethodPost, util.PostMediaFile+"?provider_access_token="+token+"&type=file", bodyBuf)
if err != nil {log.Println("err")
}
// 设置头
req.Header.Set("Content-Type", contentType)
resp, err := client.Do(req)
if err != nil {log.Println("err")
}
defer resp.Body.Close()
var data1 util.WxRspUploadFile
all, err := ioutil.ReadAll(resp.Body)
if err == nil {//解析responsejson.Unmarshal(all, &data1)
}//返回
{"errcode": 0,"errmsg": "ok. Warning: wrong json format. ","type": "file","media_id": "3zwYR2FyKTQZuuCvvVyrCGb9j8yNrUqjwqA97cXHQBoZF46rDsIWX4l4fzZCBDYzp","created_at": "1652335969"
}
素材上传得到media_id,该media_id仅三天内有效
media_id在同一企业内应用之间可以共享
3.异步通讯录id转译
post, err := util.Post(util.PostTransId+"?provider_access_token="+token,map[string]interface{}{"auth_corpid": "ww*******",//授权企业corpid ,也就是需要转义的用户、部门id的所属企业的corpid,安装应用时进行了存储。"media_id_list": []string{data1.MediaId},//上一步上传文件得到的媒体id(media_id),可以上传多个。"output_file_name": "trans_id",//输出的文件名//"output_file_format": "xlsx"//输出格式,若不指定,则输出格式跟输入格式相同。})
fmt.Println(post)
if err != nil {fmt.Println(err)
}var data2 util.WxRspTranId
json.Unmarshal([]byte(post), &data2)
fmt.Println(data2.JobId)//结果
{"errcode": 0,"errmsg": "ok","jobid": "Xq24rw360-NyvhqHnK9ztC_Nr4zeHPyqMbJF-SjwGL0"
}
注:若生成的文件不需要压缩,则 media_id_list列表只能指定一项,同时 output_file_name 不需要传值
4.获取异步任务的结果
get, err := util.Get(util.GetJobResult + "?provider_access_token=" + token + "&jobid=" + data2.JobId + "&debug=1")
var data3 util.WxRspJobResult
json.Unmarshal(get, &data3)
result := cast.ToString(data3.Result["contact_id_translate"].(map[string]interface{})["url"])
fmt.Println(result)//结果
{"errcode": 0,"errmsg": "ok","status": 3,"type": "contact_id_translate","result": {"contact_id_translate": {"url": "https://open.work.weixin.qq.com/wwopen/openData/getTranslateContactOpenData?dataid=j9otnBMVKUDSf4xrAxxxxxxxxxxxxxxxxxxxxxx"}}
}
注:返回的url参数,开发者在使用时以a标签引用,download属性不可指定(浏览器兼容性问题)
5.下载转义后文件,这是最后也是最麻烦的一步,能够下载的条件是:
1.当前浏览器页面进行了授权登录,存在登录态。
可以通过https://open.work.weixin.qq.com/wwopen/openData/debug 来检查当前登录态。
2.需要在配置好的可信域名下进行访问。(不是直接在浏览器中输入打开链接)
满足这两个条件后才会下载id转义后的文件。
4.扫码登录
这里主要讲从第三方进行单点登录,需要构造登录链接以及登录成功回调url大致流程如图所示:
\
(1)构造链接
1、用户进入服务商网站。
2、服务商网站引导用户进入登录授权页。
服务商可以在自己的网站首页中放置“企业微信登录”的入口,引导用户进入登录授权页。网址为:
https://open.work.weixin.qq.com/wwopen/sso/3rd_qrConnect?appid=ww100000a5f2191&redirect_uri=http%3A%2F%2Fwww.oa.com&state=web_login@gyoss9&usertype=admin
3、用户确认并同意授权。
用户进入登录授权页后,需要确认并同意将自己的企业微信和登录账号信息授权给企业或服务商,完成授权流程。
4、授权后回调URI,得到授权码和过期时间。
登录授权发起域名和授权成功回调域名需要在服务商后台进行配置,只能在该域名下才能发起授权和完成授权回调。
参数 | 是否必须 | 说明 |
---|---|---|
appid | 是 | 服务商的CorpID |
redirect_uri | 是 | 授权登录之后目的跳转网址,需要做urlencode处理。所在域名需要与授权完成回调域名一致 |
state | 否 | 用于企业或服务商自行校验session,防止跨域攻击 |
usertype | 否 | 支持登录的类型。admin代表管理员登录(使用微信扫码),member代表成员登录(使用企业微信扫码),默认为admin |
lang | 否 | 自定义语言,支持zh、en;lang为空则从Headers读取Accept-Language,默认值为zh |
代码实现
encode := url.QueryEscape("http://qr-login.xxxxxxxx/wx/returnLogin")//回调地址,需要做urlencode处理
redirect := "https://open.work.weixin.qq.com/wwopen/sso/3rd_qrConnect?appid=" + model.CorpId + "&redirect_uri=" + encode + "&state=" + key + "&usertype=member"
//这里是后端直接跳转到扫码页面
c.Header("Content-Type", "text/html")
c.String(200, "<script type="text/javascript"> window.location.href = ""+redirect+""</script>")
(2)获取登录用户信息
1.先获取服务商凭证(provider_access_token),需要先从服务商后台拿到CorpID和ProviderSecret
2.第三方可通过如下接口,获取登录用户的信息。建议用户以返回信息中的corpid及userid为主键匹配用户。
请求方式: POST(HTTPS)
请求地址: https://qyapi.weixin.qq.com/cgi-bin/service/get_login_info?access_token=PROVIDER_ACCESS_TOKEN
请求包体:
{"auth_code":"xxxxx" //扫码登录成功回调url携带参数}
参数说明:
参数 | 是否必须 | 说明 |
---|---|---|
access_token | 是 | 授权登录服务商的网站时,使用应用提供商的provider_access_token,获取方法参见服务商的凭证 |
auth_code | 是 | oauth2.0授权企业微信管理员登录产生的code,最长为512字节。只能使用一次,5分钟未被使用自动过期 |
代码实现
data, err := util.Post(util.GetLoginInfo+"?access_token="+corpToken, map[string]interface{}{"auth_code": authCode,
})
fmt.Println(data)
if err != nil {fmt.Println("post获取用户信息失败")return
}
var result util.WxRspLoginInfo
json.Unmarshal([]byte(data), &result)
if result.ErrCode != 0 {fmt.Println("获取用户信息失败")return
}
返回结果中可以获取到成员id就可以进行身份校验。