java对接企业微信授权

一、需求

从企业微信那边登录,获取到当前企业微信登录的用户ID。

二、思路

先去看官方文档 https://work.weixin.qq.com/api/doc/90000/90135/90664 , 在这里总结一下。

  1. 获取到企业微信的企业ID、应用ID、应用凭证
  2. 访问获取code链接
  3. 获取access_token
  4. 获取用户信息

三、实现

3.1 获取企业微信的企业ID、应用ID、应用凭证

企业ID如下

image-20210531102932567

应用ID、和凭证 需要创建一个应用,如果以前有可以直接用以前的。

image-20210531103015845

image-20210531103105645

3.2 访问获取code链接

https://open.work.weixin.qq.com/api/doc/90000/90135/91022 官方文档

code 地址

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

其中我们要修改的有两个值,appid 和 redirect_uri

appid :应用ID

redirect_uri : 授权后重定向的回调链接地址,需要使用urlencode对链接进行处理。

这里我们使用java来进行重定向,如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 跳转至企业微信登录界面
* @param response /
*/
@GetMapping("login/qywx")
public void loginByQywx(HttpServletResponse response) throws IOException {
log.info("使用企业微信登录");
// 跳转至获取code页面
String callbackUrl = myGlobalConfigProperties.getHostUri() + "/auth/login/qywx/callback";
// 加密回调地址
callbackUrl = URLEncoder.encode(callbackUrl,"UTF-8");
// 拼装微信授权发起地址
String targetUrl = StrUtil.format(qywxProperties.getCodeUrl(),callbackUrl);
response.sendRedirect(targetUrl);
}

3.3 获取access_token

其实按照文档的步骤这里应该是我们在回调的url处理器中接受到code ,然后使用code 获取到用户信息,只不过获取用户信息的接口在调用时需要一个access_token的参数。所以我们需要先获取access_token

https://open.work.weixin.qq.com/api/doc/90000/90135/91039

这里在后台使用http请求获取就行了,需要重点提的一点是这个接口不能频繁的调用,不然企业微信会在后台封禁这个应用的获取access_token的权限。官方的描述如下。

开发者需要缓存access_token,用于后续接口的调用(注意:不能频繁调用gettoken接口,否则会受到频率拦截)。当access_token失效或过期时,需要重新获取。

access_token的有效期通过返回的expires_in来传达,正常情况下为7200秒(2小时),有效期内重复获取返回相同结果,过期后获取会返回新的access_token。
由于企业微信每个应用的access_token是彼此独立的,所以进行缓存时需要区分应用来进行存储。
access_token至少保留512字节的存储空间。
企业微信可能会出于运营需要,提前使access_token失效,开发者应实现access_token失效时重新获取的逻辑。

这里我们将获取到的access_token 存储至Redis 。

3.4 获取用户信息

https://open.work.weixin.qq.com/api/doc/90000/90135/91023

这里通过http请求获取就行。注意的是需要获取的用户的详细信息,还需要去调用另外一个接口(通讯录接口:读取成员。)

四、代码

企业微信配置文件,将企业ID,应用ID , 凭证 和调用接口地址都存放在配置文件中

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
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
* 全局配置文件
* @author xia17
* @date 2020/4/13 11:46
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "qywx")
public class QywxProperties {

/** 获取accessToken的地址 */
private String accessTokenUrl;

/** 企业微信ID */
private String corpId;

/** 企业微信应用ID */
private String agentId;

/** 企业微信应用secret */
private String secret;

/** 获取用户信息的地址 */
private String userinfoUrl;

/** 获取code的地址 */
private String codeUrl;

}

示例的配置信息

1
2
3
4
5
6
7
8

qywx:
corp-id: 企业ID
agent-id: 应用ID
secret: 凭证
access-token-url: https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${qywx.corp-id}&corpsecret=${qywx.secret}
userinfo-url: https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token={}&code={}
code-url: https://open.weixin.qq.com/connect/oauth2/authorize?appid=${qywx.corp-id}&redirect_uri={}&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect

接口的返回值实体,这里抽出来一个 baseResult 。

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

/**
* 企业微信接口返回结果
* 因为大多数请求都有这两个字段,所以把他抽出来了
* @author xia17
* @date 2021/5/28 14:22
*/
@Getter
@Setter
public class QywxBaseResult {


/** 出错返回码,为0表示成功,非0表示调用失败 */
private Integer errcode;

/** 返回码提示语 */
private String errmsg;

public void valid(){
if (!Integer.valueOf(0).equals(errcode)){
throw new MyException("从企业微信获取access_token失败!errcode不为0,errmsg=" + errmsg);
}
}


}
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
/**
* 企业微信的access_token请求返回的结果实体定义
* @author xia17
* @date 2021/5/28 11:39
*/
@Getter
@Setter
public class QywxAccessTokenInfo extends QywxBaseResult {


/** 获取到的凭证,最长为512字节 */
@JsonProperty("access_token")
private String accessToken;

/** 凭证的有效时间(秒) */
@JsonProperty("expires_in")
private Integer expiresIn;

@Override
public void valid(){
super.valid();
if (expiresIn == null || expiresIn <= 0){
throw new MyException("从企业微信获取access_token失败!expiresIn = " + expiresIn);
}
if (accessToken == null){
throw new MyException("从企业微信获取access_token失败!access_token is null");
}
}

}
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
/**
* 访问用户身份
* @author xia17
* @date 2021/5/28 14:25
*/
@Getter
@Setter
public class QywxUserinfo extends QywxBaseResult {


@JsonProperty("UserId")
private String userId;

@JsonProperty("DeviceId")
private String deviceId;

@JsonProperty("OpenId")
private String openId;

@JsonProperty("external_userid")
private String externalUserId;

@Override
public void valid(){
super.valid();
if (userId == null || "".equals(userId)){
throw new MyException("userid is null!");
}
}


}

核心类QywxApi, 这里将调用http请求企业微信的代码都封装到了这里

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
import cn.hutool.core.util.StrUtil;
import com.qx.yxy.ins.assessment.common.redis.RedisUtils;
import com.qx.yxy.ins.assessment.core.exceptions.MyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
* 企业微信Api
* @author xia17
* @date 2021/5/28 11:26
*/
@Component
public class QywxApi {

private final RestTemplate restTemplate;
private final QywxProperties qywxProperties;
private final RedisUtils redisUtils;

private static final String ACCESS_TOKEN_REDIS_KEY = "fdykh:qywx_access_token";


public QywxApi(QywxProperties qywxProperties , RedisUtils redisUtils) {
this.qywxProperties = qywxProperties;
this.redisUtils = redisUtils;
// 这里手动的去创建一个 restTemplate ,而不使用容器中的 restTemplate
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
//单位为ms
factory.setReadTimeout(300 * 1000);
//单位为ms
factory.setConnectTimeout(300 * 1000);
this.restTemplate = new RestTemplate(factory);
}


/**
* 通过code获取用户信息
* @param code /
* @return /
*/
public QywxUserinfo getUserinfo(String code){
// 定义请求地址
String url = StrUtil.format(qywxProperties.getUserinfoUrl(),getAccessToken(),code);
ResponseEntity<QywxUserinfo> responseEntity = restTemplate.getForEntity(url, QywxUserinfo.class);
if (responseEntity.getStatusCode() != HttpStatus.OK){
throw new MyException("从企业微信获取userinfo失败!http状态码不为200");
}
QywxUserinfo body = responseEntity.getBody();
if (body == null){
throw new MyException("QywxUserinfo is null!");
}
body.valid();
return body;
}

/**
* 获取企业微信的 AccessToken
* @return /
*/
public String getAccessToken(){
// 尝试从Redis中获取
Optional<String> tokenOptional = this.getAccessTokenForRedis();
if (tokenOptional.isPresent()){
return tokenOptional.get();
}
// 加锁 为了防止多次拿http get token 这里使用了 双重检查锁
synchronized (this){
// 尝试从Redis中获取
tokenOptional = this.getAccessTokenForRedis();
return tokenOptional.orElseGet(this::getAccessTokenForHttp);
}
}

/**
* 从redis中获取到access_token
* @return /
*/
private Optional<String> getAccessTokenForRedis(){
Object o = redisUtils.get(ACCESS_TOKEN_REDIS_KEY);
return o == null ? Optional.empty() : Optional.of((String) o);
}

/**
* 发送http请求获取access_token
* @return /
*/
private String getAccessTokenForHttp(){
// 请求地址
String url = qywxProperties.getAccessTokenUrl();
ResponseEntity<QywxAccessTokenInfo> responseEntity = restTemplate.getForEntity(url, QywxAccessTokenInfo.class);
if (responseEntity.getStatusCode() != HttpStatus.OK){
throw new MyException("从企业微信获取access_token失败!http状态码不为200");
}
QywxAccessTokenInfo tokenInfo = responseEntity.getBody();
if (tokenInfo == null){
throw new MyException("从企业微信获取access_token失败!tokenInfo为null");
}
tokenInfo.valid();
// 存入redis -3s 提前失效
redisUtils.set(ACCESS_TOKEN_REDIS_KEY,tokenInfo.getAccessToken(),tokenInfo.getExpiresIn() - 3, TimeUnit.SECONDS);
return tokenInfo.getAccessToken();
}




}

企业微信登录的开始controller以及回调controller的代码如下。

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
/**
* 跳转至企业微信登录界面
* @param response /
*/
@GetMapping("login/qywx")
public void loginByQywx(HttpServletResponse response) throws IOException {
log.info("使用企业微信登录");
// 回调地址
String callbackUrl = myGlobalConfigProperties.getHostUri() + "/auth/login/qywx/callback";
// 加密回调地址
callbackUrl = URLEncoder.encode(callbackUrl,"UTF-8");
// 拼装微信授权发起地址
String targetUrl = StrUtil.format(qywxProperties.getCodeUrl(),callbackUrl);
response.sendRedirect(targetUrl);
}

/**
* 企业微信登录回调地址
* @param code /
*/
@GetMapping("login/qywx/callback")
public void loginByQywxCallback(@NotBlank(message = "params code is null") String code , HttpServletResponse response) throws IOException {
// 从企业微信那边获取到学工号
QywxUserinfo userinfo = qywxApi.getUserinfo(code);
// 检查是否有权限
if (checkUrlUserId(userinfo.getUserId(),response)){
return;
}
String token = tokenProvider.createToken(userinfo.getUserId(), AuthTypeEnum.QYWX);
// 带着code 重定向至前端接受code地址
response.sendRedirect(myGlobalConfigProperties.phoneHtmlCodeAcceptUri() + "?code=" + tokenCode.saveTokenAndGenerateCode(token));
}