一.前言
开发企业微信自建应用的时候难免会有获取企微个人信息的业务需求,这篇博客将详细说明企微自建应用获取userId的具体流程.
二.基本概念介绍
2.1 corpid
每个企业都拥有唯一的
corpid
,获取此信息可在管理后台“我的企业”-“企业信息”下查看“企业ID”(需要有管理员权限)
2.2 userid
每个成员都有唯一的
userid
,即所谓“账号”。在管理后台->“通讯录”->点进某个成员的详情页,可以看到
2.3 部门id
每个部门都有唯一的id,在管理后台->“通讯录”->“组织架构”->点击某个部门右边的小圆点可以看到
2.4 agentid
每个应用都有唯一的agentid。在管理后台->“应用管理”->“应用”,点进某个应用,即可看到agentid
2.5 secret
secret是企业应用里面用于保障数据安全的“钥匙”,每一个应用都有一个独立的访问密钥,为了保证数据的安全,secret务必不能泄漏
2.6 access_token
access_token
是企业后台去企业微信的后台获取信息时的重要票据,由corpid和secret产生。所有接口在通信时都需要携带此信息用于验证接口的访问权限
接口调用流程:
三.获取access_token
获取
access_token
是调用企业微信API接口的第一步,相当于创建了一个登录凭证
,其它的业务API接口,都需要依赖于access_token来鉴权
调用者身份。因此开发者,在使用业务接口前,要明确access_token的颁发来源,使用正确的access_token。
- 请求方式:
GET(HTTPS)
- 请求地址: https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET
此处标注大写的单词 ID 和 SECRET,为需要替换的变量,根据实际获取值更新。其它接口也采用相同的标注,不再说明
3.1 请求参数说明:
参数 | 必须 | 说明 |
---|---|---|
corpid | 是 | 企业ID |
corpsecret | 是 | 应用的凭证密钥,注意应用需要是启用状态(secret) |
权限说明:
每个应用有独立的
secret
,获取到的access_token只能本应用使用,所以每个应用的access_token应该分开来获取
3.2 返回结果:
{
"errcode": 0,
"errmsg": "ok",
"access_token": "accesstoken000001",
"expires_in": 7200
}
响应参数说明:
参数 | 说明 |
---|---|
errcode | 出错返回码,为0表示成功,非0表示调用失败 |
errmsg | 返回码提示语 |
access_token | 获取到的凭证,最长为512字节 |
expires_in | 凭证的有效时间(秒) |
3.3 注意事项:
-
开发者需要缓存access_token,用于后续接口的调用(注意:不能频繁调用gettoken接口,否则会受到频率拦截)。当access_token失效或过期时,需要重新获取。
-
access_token的有效期通过返回的expires_in来传达,正常情况下为7200秒(2小时),有效期内重复获取返回相同结果,过期后获取会返回新的access_token。
-
由于企业微信每个应用的access_token是彼此独立的,所以进行缓存时需要区分应用来进行存储。
-
access_token至少保留512字节的存储空间。
-
企业微信可能会出于运营需要,提前使access_token失效,开发者应实现access_token失效时重新获取的逻辑。
四.获取访问用户身份
该接口用于根据
code
获取成员信息,适用于自建应用与代开发应用
- 请求方式:
GET(HTTPS)
- 请求地址:https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=ACCESS_TOKEN&code=CODE
4.1 请求参数说明:
参数 | 必须 | 说明 |
---|---|---|
access_token | 是 | 调用接口凭证 |
code | 是 | 通过成员授权获取到的code,最大为512字节。每次成员授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期 |
权限说明:
跳转的域名须完全匹配access_token对应应用的可信域名,否则会返回50001错误
4.2 返回结果:
{
"errcode": 0,
"errmsg": "ok",
"userid":"USERID",
"user_ticket": "USER_TICKET"
}
响应参数说明:
参数 | 说明 |
---|---|
errcode | 返回码 |
errmsg | 对返回码的文本描述内容 |
userid | 成员UserID。若需要获得用户详情信息,可调用通讯录接口:读取成员。如果是互联企业/企业互联/上下游,则返回的UserId格式如:CorpId/userid |
user_ticket | t 成员票据,最大为512字节,有效期为1800s。scope为snsapi_privateinfo,且用户在应用可见范围之内时返回此参数。后续利用该参数可以获取用户信息或敏感信息,参见"获取访问用户敏感信息"。暂时不支持上下游或/企业互联场景 |
五.代码编写
本次业务需求我只要获取userId即可,所以在后端采用静态变量存储access_token,如不满足你的业务需求可采用redis进行存储
5.1 前端
created() {
this.getUserIdByCode()
}
methods: {
// 获取userID
getUserIdByCode() {
console.log('钩子函数执行了')
const code = this.GetQueryString('code')
if (code) {
// 通过code获取用户信息
this.getUserId(code)
}
},
// 获取code
GetQueryString(name) {
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)')
var r = window.location.search.substr(1).match(reg)
if (r != null) return unescape(r[2]);
return null
},
getUserId(code) {
uni.request({
url: 'http://localhost:13889/wechat/getUserId/' + code,
method: 'POST',
success: (res) => {
this.userId = res.data.data
console.log("员工工号:" + this.userId);
}
})
}
}
5.2 后端
5.2.1 企微常量参数
/**
* 描述:企微常量参数
* 创建人: 黎明
* 创建时间: 2023/10/19
* 版本: 1.0.0
*/
public interface WeChatConstant {
// 企业ID
public final static String CORP_ID = "企业ID";
// 应用的凭证密钥
public final static String SOD_SECRET = "应用的凭证密钥";
// 获取access_token
public final static String ACCESS_TOKEN_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET";
// 获取访问用户身份
public final static String CODE_URL = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=ACCESS_TOKEN&code=CODE";
}
5.2.2 工具类
/**
* 描述:http工具类
* 创建人: 黎明
* 创建时间: 2023/12/25
* 版本: 1.0.0
*/
@Slf4j
public class HttpUtils {
/**
* 获取企业微信的access_token
*
* @param corpid 企业ID
* @param corpsecret 应用的凭证密钥
* @return AccessToken对象,包含access_token等信息
*/
public static AccessToken getAccessToken(String corpid, String corpsecret) {
AccessToken accessToken = null;
// 构造请求URL,将其中的ID和SECRET替换为实际参数
String requestUrl = WeChatConstant.ACCESS_TOKEN_URL.replace("ID", corpid).replace("SECRET", corpsecret);
// 发送HTTP GET请求,获取响应结果
String resResult = HttpUtil.get(requestUrl);
// 将响应结果转换为JSON对象
JSONObject obj = new JSONObject(resResult);
// 如果请求成功
if (null != obj) {
accessToken = new AccessToken();
if (obj.getInt("errcode").equals(0)) {
// 设置AccessToken对象的属性值
accessToken.setErrcode(obj.getInt("errcode"));
accessToken.setErrmsg(obj.getStr("errmsg"));
accessToken.setAccessToken(obj.getStr("access_token"));
accessToken.setExpiresIn(obj.getInt("expires_in"));
accessToken.setTokenTime(DateUtil.date());
} else {
// 如果错误码不为0,表示请求失败,将accessToken设置为null
accessToken = null;
// 记录错误日志
log.error("获取token失败 errcode:{} errmsg:{}", obj.getInt("errcode"), obj.getStr("errmsg"));
}
}
// 返回AccessToken对象
return accessToken;
}
}
/**
* 描述:日期工具类
* 创建人: 黎明
* 创建时间: 2023/12/25
* 版本: 1.0.0
*/
public class DateUtils {
/**
* 根据date转换成localDateTime
*
* @param date 日期
* @return {@link LocalDateTime}
*/
public static LocalDateTime dateConvertLocalDateTime(Date date) {
// 将Date对象转换为Instant对象,然后根据系统默认时区转换为LocalDateTime对象
LocalDateTime localDateTime = Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault()).toLocalDateTime();
return localDateTime;
}
/**
* 根据localDateTime转换成date
*
* @param localDateTime 本地日期时间
* @return Date对象
*/
public static Date localDateTimeConvertDate(LocalDateTime localDateTime) {
// 调用getZonedDateTimeByLocalDateTime方法将LocalDateTime对象转换为ZonedDateTime对象,然后将ZonedDateTime对象转换为Instant对象,最后将Instant对象转换为Date对象
return Date.from(getZonedDateTimeByLocalDateTime(localDateTime).toInstant());
}
/**
* 根据localDateTime转换成ZonedDateTime对象,用于把localDatTime转成Date
*
* @param localDateTime 本地日期时间
* @return ZonedDateTime对象
*/
public static ZonedDateTime getZonedDateTimeByLocalDateTime(LocalDateTime localDateTime) {
// 获取系统默认时区
ZoneId zoneId = ZoneId.systemDefault();
// 将LocalDateTime对象转换为ZonedDateTime对象
ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);
return zonedDateTime;
}
}
5.2.3 封装access_token返回结果
@Data
public class AccessToken implements Serializable {
/**
* 返回码
*/
private Integer errcode;
/**
* 返回消息
*/
private String errmsg;
/**
* token
*/
private String accessToken;
/**
* 凭证有效时间,单位:秒
*/
private Integer expiresIn;
/**
* 获取token的时间
*/
private Date tokenTime;
}
5.2.4 定义接口
* 描述:获取企微人员信息
* 创建人: 黎明
* 创建时间: 2023/12/25
* 版本: 1.0.0
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/wechat")
public class UserInfoController {
private final WeChatServiceImpl weChatService;
/**
* 获取用户id
*
* @return 员工工号
*/
@PostMapping ("/getUserId/{code}")
public Result<String> getUserId(@PathVariable String code){
String userId = weChatService.getUserIdByCode(code);
return Result.success(userId);
}
}
5.2.5 编写业务层代码
@Service
@Slf4j
public class WeChatServiceImpl {
// 定义access_token静态变量,用于存储获取到的access_token
private static String ACCESS_TOKEN = "";
// 定义获取access_token的时间
private static Date ACCESS_TOKEN_TIMR = DateUtil.date();
/**
* 根据微信授权码获取用户ID
*
* @param code 微信授权码
* @return 用户ID
*/
public String getUserIdByCode(String code) {
// 初始化成员变量userId为空字符串
String userId = "";
// 如果ACCESS_TOKEN为空或者过期,则重新获取access_token
if (StrUtil.hasEmpty(ACCESS_TOKEN)) {
ACCESS_TOKEN = getUserInfo();
}
boolean flag = checkAccessToken(ACCESS_TOKEN_TIMR);
// 如果access_token已过期,则重新获取access_token
if (!flag) {
ACCESS_TOKEN = getUserInfo();
}
// 构造请求URL,替换其中的ACCESS_TOKEN和CODE
String userUrl = WeChatConstant.CODE_URL.replace("ACCESS_TOKEN", ACCESS_TOKEN).replaceAll("CODE", code);
// 发送HTTP请求,获取响应结果
String res = HttpUtil.get(userUrl);
// 将响应结果转换为JSON对象
JSONObject response = new JSONObject(res);
// 如果响应中的错误编码为0,表示成功获取到用户信息,将用户ID赋值给userId
if (response.getStr("errcode").equals("0")) {
userId = response.getStr("userid");
} else {
// 如果错误编码不为0,记录日志,输出错误编码和错误信息
log.info("未获取到人员信息,错误编码:{},错误信息:{}", response.getStr("errcode"), response.getStr("errmsg"));
}
// 返回用户ID
return userId;
}
/**
* 获取access_token
*
* @return access_token字符串
*/
private static String getUserInfo() {
// 调用HttpUtils工具类的getAccessToken方法,传入企业ID和应用密钥,获取access_token
return HttpUtils.getAccessToken(WeChatConstant.CORP_ID, WeChatConstant.SOD_SECRET).getAccessToken();
}
/**
* 检查access_token是否过期
*
* @param date 时间
* @return 如果access_token未过期,返回true;否则返回false
*/
private static boolean checkAccessToken(Date date) {
// 将时间转换为LocalDateTime对象
LocalDateTime localDateTimeNow = DateUtils.dateConvertLocalDateTime(date);
// 将LocalDateTime对象转换为时间戳(毫秒)
Long milliSecond = getTimestampOfDateTime(localDateTimeNow);
// 将时间戳加上7200秒(2小时),得到新的过期时间
milliSecond = milliSecond + (7200L * 1000L);
// 将新的过期时间转换为LocalDateTime对象
LocalDateTime dateTime = getDateTimeOfTimestamp(milliSecond);
// 获取当前时间
LocalDateTime now = LocalDateTime.now();
// 如果当前时间在新的过期时间之前,说明access_token未过期,返回true;否则返回false
if (now.isBefore(dateTime)) {
return true;
} else {
return false;
}
}
/**
* 将LocalDateTime对象转换为时间戳(毫秒)
*
* @param localDateTime 要转换的LocalDateTime对象
* @return 对应的时间戳(毫秒)
*/
private static long getTimestampOfDateTime(LocalDateTime localDateTime) {
// 获取系统默认时区
ZoneId zone = ZoneId.systemDefault();
// 将LocalDateTime对象转换为Instant对象
Instant instant = localDateTime.atZone(zone).toInstant();
// 将Instant对象转换为时间戳(毫秒)并返回
return instant.toEpochMilli();
}
/**
* 将时间戳(毫秒)转换为LocalDateTime对象
*
* @param timestamp 要转换的时间戳(毫秒)
* @return 对应的LocalDateTime对象
*/
private static LocalDateTime getDateTimeOfTimestamp(long timestamp) {
// 将时间戳(毫秒)转换为Instant对象
Instant instant = Instant.ofEpochMilli(timestamp);
// 获取系统默认时区
ZoneId zone = ZoneId.systemDefault();
// 将Instant对象转换为LocalDateTime对象并返回
return LocalDateTime.ofInstant(instant, zone);
}
}
六.构造网页授权链接
https://open.weixin.qq.com/connect/oauth2/authorize?appid=CORPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_base&state=STATE&agentid=AGENTID#wechat_redirect