企业微信网页授权登录

背景

本文将介绍若依框架集成企业微信实现网页授权登录,以达到自建应用免登录的体验效果。

开发前准备

调用流程

官方文档:https://developer.work.weixin.qq.com/document/path/91335

OAuth2接入流程

步骤如下:

  1. 用户访问第三方服务,第三方服务通过构造OAuth2链接(参数包括当前第三方服务的身份ID,以及重定向URI),将用户引导到认证服务器的授权页;
  2. 用户选择是否同意授权;
  3. 若用户同意授权,则认证服务器将用户重定向到第一步指定的重定向URI,同时附上一个授权码;
  4. 第三方服务收到授权码,带上授权码来源的重定向URI,向认证服务器申请凭证;
  5. 认证服务器检查授权码和重定向URI的有效性,通过后颁发AccessToken(调用凭证)。

开发前准备

可信域名申请

由于REDIRECT_URL要求必须为可信域名,因此我们需要为我们的项目申请一个域名。如果你的项目已经开发完成,可以直接申请一个二级域名来使用;如果你刚刚开始开发,急需调试此功能,你可以申请一个临时的免费的域名,这种域名完全免费,但是带宽有极大限制,非必要不建议使用这类域名。我这里因为调试需要,申请了免费域名,带宽很小网速很慢。免费的域名申请这里不做介绍,我使用的是Cpolar,大家请自行准备好这一步的域名。

Cpolar界面

构造网页授权链接

这个链接一般为了安全,由前端触发请求后端构造网页授权链接(回调redirect_uri

1
https://open.weixin.qq.com/connect/oauth2/authorize?appid=CORPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect

参数说明

设置可信域名

登录企业微信管控台(你必须是企业管理员):https://work.weixin.qq.com/wework_admin/frame#apps

企业微信管理

配置网页授权和企业微信授权

网页授权和企业微信授权

网页授权

企业微信授权

设置自建应用的启动URL:

启动URL

至此,准备工作已经完成,接下来进入代码开发环节。

代码开发

构造第一次请求

login.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
getOauthUrl() {
let oauthCallback = encodeURI(window.location.protocol + '//' + window.location.host + "/#/callback")
let params = {'oauth_callback': oauthCallback}
oauthUrl(params).then((res) => {
window.location.href = res.data.oauth_url;
let data = {
corp_id: res.data.corp_id,
code: this.queryString('code=')
}
loadingInstance.close();
this.$router.push({
name: 'SingleLogin',
query: data
}).catch(() => {
})
})
},
auth.js
1
2
3
4
5
6
7
export function oauthUrl(query) {
return request({
url: '/h5/oauthUrl',
method: 'get',
params: query
})
}
H5Controller.java
1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping({"/h5/oauthUrl"})
JsonData oauthUrl(HttpServletRequest request, @RequestParam("oauth_callback") String oauthCallback) throws UnsupportedEncodingException {
Map resData = new HashMap();
QywxInnerCompany qywxInnerCompany = qywxInnerCompanyService.getCorpId(1);
String corpId = qywxInnerCompany.getCorpId();
//普通应用
String oauthRedirectUrl = URLEncoder.encode(oauthCallback, "utf-8");
String oauthUrl = qywxInnerService.getOauthUrl(corpId, oauthRedirectUrl);
System.out.println("oauth_url:" + oauthUrl);
resData.put("oauth_url", oauthUrl);
resData.put("corp_id", corpId);
return JsonData.buildSuccess(resData);
}
QywxInnerService.java
1
2
3
public String getOauthUrl(String corpId, String url) {
return String.format(qywxInnerConfig.getOauthUrl(), corpId, url);
}
QywxInnerConfig.java

构造配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
package com.ruoyi.common.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "qywx-inner")
public class QywxInnerConfig {

private String corpId;

private String suiteId;
private String suiteSecret;
private String token;
private String encodingAESKey;
private Integer authType;
private String templateId;
private String approvalFlowId;

private String baseUrl = "https://qyapi.weixin.qq.com/cgi-bin/";

//服务商相关
private String serviceUrl = baseUrl+"service/";
private String suiteTokenUrl = serviceUrl+"get_suite_token";
private String permanentCodeUrl = serviceUrl+"get_permanent_code?suite_access_token=%s";

//获取access_token
//https://open.work.weixin.qq.com/api/doc/90000/90135/91039
private String accessTokenUrl = baseUrl+"gettoken?corpid=%s&corpsecret=%s";

//应用管理
//https://open.work.weixin.qq.com/api/doc/90000/90135/90227
private String agentGetUrl = baseUrl+"agent/get?access_token=%s&agentid=%s";
private String agentSetUrl = baseUrl + "agent/set?access_token=%s";
private String AgentMenuCreateUrl = baseUrl + "menu/create?access_token=%s&agentid=%s";
private String AgentMenuGetUrl = baseUrl + "menu/get?access_token=%s&agentid=%s";
private String AgentMenuDeleteUrl = baseUrl + "/menu/delete?access_token=%s&agentid=%s";

//身份验证 扫码授权登录
//https://open.work.weixin.qq.com/api/doc/90000/90135/91019
private String ssoAuthUrl = "https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=%s&agentid=%s&redirect_uri=%s&state=%s";
//https://open.work.weixin.qq.com/api/doc/90000/90135/91437
//与H5登录接口一样
private String ssoUserInfoUrl = baseUrl+"user/getuserinfo?access_token=%s&code=%s";

//公司相关
//https://open.work.weixin.qq.com/api/doc/90000/90135/90208
private String departmentUrl = baseUrl+"department/list?access_token=%s";
//https://open.work.weixin.qq.com/api/doc/90000/90135/90200
private String userSimplelistUrl = baseUrl+"user/simplelist?access_token=%s&department_id=%s&fetch_child=%s";
//https://open.work.weixin.qq.com/api/doc/90000/90135/90201
private String userDetailListUrl = baseUrl+"user/list?access_token=%s&department_id=%s&fetch_child=%s";
//https://open.work.weixin.qq.com/api/doc/90000/90135/90196
private String userDetailUrl = baseUrl+"user/get?access_token=%s&userid=%s";

//客户联系
//获取配置了客户联系功能的成员列表 https://open.work.weixin.qq.com/api/doc/90000/90135/92571
private String extContactFollowUserListUrl = baseUrl+"externalcontact/get_follow_user_list?access_token=%s";
//获取客户列表 https://open.work.weixin.qq.com/api/doc/90000/90135/92113
private String extContactListUrl = baseUrl+"externalcontact/list?access_token=%s&userid=%s";
//获取客户详情 https://open.work.weixin.qq.com/api/doc/90000/90135/92114
private String extContactDetailUrl = baseUrl+"/externalcontact/get?access_token=%s&external_userid=%s&cursor=%s";
//获取客户群列表 https://open.work.weixin.qq.com/api/doc/90000/90135/92120
private String extContactGroupchatListUrl = baseUrl+"externalcontact/groupchat/list?access_token=%s";
//获取客户群详情 https://open.work.weixin.qq.com/api/doc/90000/90135/92122
private String extContactGroupchatDetailUrl = baseUrl+"externalcontact/groupchat/get?access_token=%s";
//添加新客户欢迎 https://open.work.weixin.qq.com/api/doc/90000/90135/92137
private String extcontactSendWelcomeMsgUrl = baseUrl+"externalcontact/send_welcome_msg?access_token=%s";
//创建企业群发 https://open.work.weixin.qq.com/api/doc/90000/90135/92135
private String extcontactAddMsgTemplateUrl = baseUrl+"externalcontact/add_msg_template?access_token=%s";

//消息推送
//https://open.work.weixin.qq.com/api/doc/90000/90135/90236
private String messageSendUrl= baseUrl+"message/send?access_token=%s";

//素材管理
//https://open.work.weixin.qq.com/api/doc/90000/90135/91054
//type 是 媒体文件类型,分别有图片(image)、语音(voice)、视频(video),普通文件(file)
private String mediaUploadUrl = baseUrl+"media/upload?access_token=%s&type=%s";
private String mediaUploadimgUrl = baseUrl+"media/uploadimg?access_token=%s";
private String mediaGetUrl = baseUrl+"media/get?access_token=%s&media_id=%s";
private String mediaGetJssdkUrl = baseUrl+"media/get/jssdk?access_token=%s&media_id=%s";

//审批
//审批应用 https://work.weixin.qq.com/api/doc/90001/90143/91956
private String oaCopyTemplateUrl ="oa/approval/copytemplate?access_token=%s";
private String oaGetTemplateUrl ="/oa/gettemplatedetail?access_token=%s";
private String oaApplyEventUrl = "oa/applyevent?access_token=%s";
private String oaGetApprovalUrl ="oa/getapprovaldetail?access_token=%s";

//审批流程引擎 https://work.weixin.qq.com/api/doc/90001/90143/93798
private String openApprovalDataUrl = baseUrl+"corp/getopenapprovaldata?access_token=%s";


// H5应用
//scope 是 应用授权作用域。企业自建应用固定填写:snsapi_base
// https://open.work.weixin.qq.com/api/doc/90000/90135/91020
private String oauthUrl = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=#wechat_redirect";
//https://open.work.weixin.qq.com/api/doc/90000/90135/91023
private String oauthUserUrl = baseUrl+"user/getuserinfo?access_token=%s&code=%s";

//https://work.weixin.qq.com/api/doc/90000/90136/90506
private String jsapiTicketUrl = baseUrl+"get_jsapi_ticket?access_token=%s";
private String jsapiTicketAgentUrl = baseUrl+"ticket/get?access_token=%s&type=agent_config";

//家校沟通
//https://work.weixin.qq.com/api/doc/90000/90135/91638
private String extContactMessageSendUrl = baseUrl+"externalcontact/message/send?access_token=%s";
private String extContactSubscribeQrUrl = baseUrl+"externalcontact/get_subscribe_qr_code?access_token=%s";

//此oauth与H5oauth一致 https://work.weixin.qq.com/api/doc/90000/90135/91857
private String schoolOauthUrl = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect";
//https://work.weixin.qq.com/api/doc/90000/90135/91707
private String schoolOauthUserUrl = baseUrl+"user/getuserinfo?access_token=%s&code=%s";

private String schoolUrl = baseUrl+"school/";
//https://work.weixin.qq.com/api/doc/90000/90135/92337
private String schoolUserGetUrl = schoolUrl+"user/get?access_token=%s&userid=%s";
//https://work.weixin.qq.com/api/doc/90000/90135/92343
private String schoolDepartmentListUrl = schoolUrl+"department/list?access_token=%s&id=%s";
//https://work.weixin.qq.com/api/doc/90000/90135/92446
private String schoolUserListUrl = schoolUrl+"user/list?access_token=%s&department_id=%s&fetch_child=%s";

//效率工具
private String calendarAddUrl = baseUrl+"oa/calendar/add?access_token=%s";
private String calendarDetailUrl = baseUrl+"oa/calendar/get?access_token=%s";
private String scheduleAddUrl = baseUrl+"oa/schedule/add?access_token=%s";
private String scheduleListUrl = baseUrl+"oa/schedule/get_by_calendar?access_token=%s";
private String scheduleDetailUrl = baseUrl+"oa/schedule/get?access_token=%s";
private String meetingCreateUrl = baseUrl+"meeting/create?access_token=%s";
private String userMeetingListUrl = baseUrl+"meeting/get_user_meetingid?access_token=%s";
private String meetingCancelUrl = baseUrl+"meeting/cancel?access_token=%s";
private String meetingDetailUrl = baseUrl+"meeting/get_info?access_token=%s";

//小程序应用
//小程序登录流程 https://work.weixin.qq.com/api/doc/90000/90136/92426
//code2Session https://work.weixin.qq.com/api/doc/90000/90136/91507
private String code2sessionUrl = baseUrl+"miniprogram/jscode2session?access_token=%s&js_code=%s&grant_type=authorization_code";


public String getSuiteTokenUrl() {
return suiteTokenUrl;
}


public String getPermanentCodeUrl() {
return permanentCodeUrl;
}

public String getJsapiTicketUrl() {
return jsapiTicketUrl;
}

public String getOauthUrl() {
return oauthUrl;
}

public String getUserSimplelistUrl() {
return userSimplelistUrl;
}

public String getAccessTokenUrl() {
return accessTokenUrl;
}

public String getDepartmentUrl() {
return departmentUrl;
}

public String getUserDetailListUrl() {
return userDetailListUrl;
}

public String getCorpId() {
return corpId;
}

public String getSuiteId() {
return suiteId;
}

public String getSuiteSecret() {
return suiteSecret;
}

public String getToken() {
return token;
}

public String getEncodingAESKey() {
return encodingAESKey;
}

public Integer getAuthType() {
return authType;
}

public String getUserDetailUrl() {
return userDetailUrl;
}

public String getOauthUserUrl() {
return oauthUserUrl;
}


public String getCode2sessionUrl() {
return code2sessionUrl;
}

public void setCorpId(String corpId) {
this.corpId = corpId;
}

public void setSuiteId(String suiteId) {
this.suiteId = suiteId;
}

public void setSuiteSecret(String suiteSecret) {
this.suiteSecret = suiteSecret;
}

public void setToken(String token) {
this.token = token;
}

public void setEncodingAESKey(String encodingAESKey) {
this.encodingAESKey = encodingAESKey;
}

public void setAuthType(Integer authType) {
this.authType = authType;
}

public String getTemplateId() {
return templateId;
}

public void setTemplateId(String templateId) {
this.templateId = templateId;
}

public String getApprovalFlowId() {
return approvalFlowId;
}

public void setApprovalFlowId(String approvalFlowId) {
this.approvalFlowId = approvalFlowId;
}


public String getSsoAuthUrl() {
return ssoAuthUrl;
}


public String getJsapiTicketAgentUrl() {
return jsapiTicketAgentUrl;
}


public String getExtContactFollowUserListUrl() {
return extContactFollowUserListUrl;
}

public String getExtContactListUrl() {
return extContactListUrl;
}

public String getExtContactGroupchatListUrl() {
return extContactGroupchatListUrl;
}

public String getMessageSendUrl() {
return messageSendUrl;
}

public String getMediaUploadUrl() {
return mediaUploadUrl;
}

public String getMediaUploadimgUrl() {
return mediaUploadimgUrl;
}

public String getMediaGetUrl() {
return mediaGetUrl;
}

public String getMediaGetJssdkUrl() {
return mediaGetJssdkUrl;
}

public String getOaCopyTemplateUrl() {
return oaCopyTemplateUrl;
}

public String getOaGetTemplateUrl() {
return oaGetTemplateUrl;
}

public String getOaApplyEventUrl() {
return oaApplyEventUrl;
}

public String getOaGetApprovalUrl() {
return oaGetApprovalUrl;
}

public String getOpenApprovalDataUrl() {
return openApprovalDataUrl;
}

public String getSchoolOauthUrl() {
return schoolOauthUrl;
}

public String getSchoolOauthUserUrl() {
return schoolOauthUserUrl;
}

public String getSchoolUserGetUrl() {
return schoolUserGetUrl;
}

public String getSchoolDepartmentListUrl() {
return schoolDepartmentListUrl;
}

public String getSchoolUserListUrl() {
return schoolUserListUrl;
}

public String getExtContactMessageSendUrl() {
return extContactMessageSendUrl;
}
public String getExtContactSubscribeQrUrl() {
return extContactSubscribeQrUrl;
}
public String getSsoUserInfoUrl() {
return ssoUserInfoUrl;
}

public String getAgentGetUrl() {
return agentGetUrl;
}

public String getAgentSetUrl() {
return agentSetUrl;
}

public String getAgentMenuCreateUrl() {
return AgentMenuCreateUrl;
}

public String getAgentMenuGetUrl() {
return AgentMenuGetUrl;
}

public String getAgentMenuDeleteUrl() {
return AgentMenuDeleteUrl;
}

public String getExtContactDetailUrl() {
return extContactDetailUrl;
}

public String getExtcontactSendWelcomeMsgUrl() {
return extcontactSendWelcomeMsgUrl;
}

public String getExtContactGroupchatDetailUrl() {
return extContactGroupchatDetailUrl;
}

public String getExtcontactAddMsgTemplateUrl() {
return extcontactAddMsgTemplateUrl;
}

public String getCalendarAddUrl() {
return calendarAddUrl;
}

public String getCalendarDetailUrl() {
return calendarDetailUrl;
}

public String getScheduleAddUrl() {
return scheduleAddUrl;
}

public String getScheduleListUrl() {
return scheduleListUrl;
}

public String getScheduleDetailUrl() {
return scheduleDetailUrl;
}

public String getMeetingCreateUrl() {
return meetingCreateUrl;
}

public String getUserMeetingListUrl() {
return userMeetingListUrl;
}

public String getMeetingCancelUrl() {
return meetingCancelUrl;
}

public String getMeetingDetailUrl() {
return meetingDetailUrl;
}
}

构造第二次请求

login.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
getOauthUser() {
let params = {'code': this.queryString('code=')}

oauthUser(params).then((res) => {
this.$store.dispatch("SingleLogin", {
userId: res.data.userid
}).then(() => {
loadingInstance.close();
this.$router.push({path: this.redirect || "/"}).catch(() => {
});
}).catch(() => {
});
});
},
auth.js
1
2
3
4
5
6
7
export function oauthUser(query) {
return request({
url: '/h5/oauthUser',
method: 'get',
params: query
})
}
H5Controller.java
1
2
3
4
5
6
7
8
9
10
@RequestMapping("/h5/oauthUser")
public JsonData oauthCallback(HttpServletRequest request, HttpServletResponse response, @RequestParam("code") String code) throws IOException {
//通过code获取信息
QywxInnerCompany qywxInnerCompany = qywxInnerCompanyService.getCorpId(1);
String corpId = qywxInnerCompany.getCorpId();
Map result = qywxInnerService.getOauthUser(corpId, code);
System.out.println("code-->>" + code);

return JsonData.buildSuccess(result);
}
QywxInnerService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Map getOauthUser(String corpId, String code) {
String accessToken = getAccessToken(corpId);
String getOauthUrl = String.format(qywxInnerConfig.getOauthUserUrl(), accessToken, code);
Map response = RestUtils.get(getOauthUrl);
if (response.containsKey("errcode") && (Integer) response.get("errcode") != 0) {
logger.error(response.toString());
return response;
}
// return response;

//获取通讯录用户详情get
String userId = (String) response.get("UserId");
String url = String.format(qywxInnerConfig.getUserDetailUrl(), accessToken, userId);
Map detaiResponse = RestUtils.get(url);

//获取错误日志
if (detaiResponse.containsKey("errcode") && (Integer) detaiResponse.get("errcode") != 0) {
logger.error(detaiResponse.toString());
}
return detaiResponse;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public String getAccessToken(String corpId) {

String result = "";

QywxInnerCompany qywxInnerCompany = qywxInnerCompanyService.getCorpId(1);
String agentSecret = qywxInnerCompany.getAgentSecret();

String accessTokenUrl = String.format(qywxInnerConfig.getAccessTokenUrl(), corpId, agentSecret);
Map response = RestUtils.get(accessTokenUrl);

//获取错误日志
if (response.containsKey("errcode") && (Integer) response.get("errcode") != 0) {
logger.error(response.toString());
} else {
result = (String) response.get("access_token");
}

return result;

}

前端构造第三方登录界面

user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
// 单点登录
SingleLogin({ commit }, userInfo) {
const userId = userInfo.userId;
return new Promise((resolve, reject) => {
singleLogin(userId).then(res => {
setToken(res.token)
commit('SET_TOKEN', res.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
auth.js
1
2
3
4
5
6
7
8
9
10
export function singleLogin(userId) {
const data = {
userId
}
return request({
url: '/h5/singleLogin/' + userId,
method: 'get',
params: data
})
}
H5Controller.java
1
2
3
4
5
6
7
8
9
@GetMapping("/h5/singleLogin/{userId}")
public AjaxResult socialLogin(@PathVariable("userId") String userId, AuthCallback callback, HttpServletRequest request) {
SysUser user = userService.selectUserByUserName(userId);
// 查数据库获取人员
LoginUser loginUser = new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
String token = tokenService.createToken(loginUser);

return success().put(Constants.TOKEN, token);
}
SingleLogin.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<template>
<div></div>
</template>

<script>
import {Loading} from 'element-ui'
import {oauthUser} from "@/api/system/auth";

let loadingInstance;
export default {
data() {
return {
redirect: undefined,
};
},
created() {
loadingInstance = Loading.service({
lock: true,
text: "正在验证第三方应用账户数据,请稍候",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)",
})
// 第三方登录回调参数
oauthUser(this.$route.query).then((res) => {
this.$store.dispatch("SingleLogin", {
userId: res.data.userid
}).then(() => {
loadingInstance.close();
console.log('res.data.userid:', res.data.userid);
this.$router.push({
path: this.redirect || "/",
params: {
data: 'true'
}
}).catch(() => {
});
}).catch(() => {
loadingInstance.close();
});
});

},
methods: {},
};
</script>

<style rel="stylesheet/scss" lang="scss">
</style>

效果

企业微信单点登录