一、网页授权登录:
1.使用内网穿透工具为“本地ip+端口”映射一个公网域名,比如:xxx.xxx.xxx.com 表示我本地的 127.0.0.1:8080。
2.申请一个测试用的企业微信,新建一个应用,比如叫:test_app_0001.
3.新建一个springboot工程:
企业微信的几个工具类:
QyOauthApi.java:
package com.test.qywechat.api;
import com.test.qywechat.httpclient.LocalHttpClient;
import com.test.qywechat.model.QyAuthUser;
import com.test.qywechat.model.QyAuthUserInfo;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
public class QyOauthApi extends QyWechatBaseApi{
private static Logger logger = LoggerFactory.getLogger(QyOauthApi.class);
/**
* 生成网页授权 URL (第三方平台开发)
* @param appid appid
* @param redirect_uri 自动URLEncoder
* @param snsapi_userinfo snsapi_userinfo
* @param state 可以为空
* @param component_appid 第三方平台开发,可以为空。
* 服务方的appid,在申请创建公众号服务成功后,可在公众号服务详情页找到
* @return url
*/
public static String connectOauth2Authorize(String appid,String redirect_uri,boolean snsapi_userinfo,String state,String component_appid){
try {
StringBuilder sb = new StringBuilder();
sb.append(OPEN_URI + "/connect/oauth2/authorize?")
.append("appid=").append(appid)
.append("&redirect_uri=").append(URLEncoder.encode(redirect_uri, "utf-8"))
.append("&response_type=code")
.append("&scope=").append(snsapi_userinfo?"snsapi_userinfo":"snsapi_base")
.append("&state=").append(state==null?"":state);
if(component_appid!=null){
sb.append("&component_appid=").append(component_appid);
}
sb.append("#wechat_redirect");
return sb.toString();
} catch (UnsupportedEncodingException e) {
logger.error("", e);
}
return null;
}
/**
* 获取企业微信当前登录账号
* @param accessToken access_token
* @param code 通过成员授权获取到的code,最大为512字节。每次成员授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。
* @return 登录账号
*/
public static QyAuthUser getAuthUserInfo(String accessToken, String code){
HttpUriRequest httpUriRequest = RequestBuilder.get()
.setUri(BASE_URI + "/cgi-bin/user/getuserinfo")
.addParameter(PARAM_ACCESS_TOKEN, accessToken)
.addParameter("code", code)
.build();
return LocalHttpClient.executeJsonResult(httpUriRequest,QyAuthUser.class);
}
/**
* 根据userId获取通讯录成员用户详情信息
* (注:应用须拥有指定成员的查看权限)
* @param accessToken access_token
* @param userId 登录账号
* @return 登录账号详情信息
*/
public static QyAuthUserInfo queryAuthUserInfo(String accessToken, String userId){
HttpUriRequest httpUriRequest = RequestBuilder.get()
.setUri(BASE_URI + "/cgi-bin/user/get")
.addParameter(PARAM_ACCESS_TOKEN, accessToken)
.addParameter("userid", userId)
.build();
return LocalHttpClient.executeJsonResult(httpUriRequest, QyAuthUserInfo.class);
}
}
QyTokenApi.java:
package com.test.qywechat.api;
import com.test.qywechat.httpclient.LocalHttpClient;
import com.test.qywechat.model.QyToken;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
public class QyTokenApi extends QyWechatBaseApi{
public static QyToken token(String corpId, String secret){
HttpUriRequest httpUriRequest = RequestBuilder.get()
.setUri(BASE_URI + "/cgi-bin/gettoken")
.addParameter("corpid", corpId)
.addParameter("corpsecret", secret)
.build();
return LocalHttpClient.executeJsonResult(httpUriRequest,QyToken.class);
}
}
QyWechatBaseApi.java:
package com.test.qywechat.api;
public class QyWechatBaseApi {
protected static final String BASE_URI = "https://qyapi.weixin.qq.com";
protected static final String OPEN_URI = "https://open.weixin.qq.com";
protected static final String PARAM_ACCESS_TOKEN = "access_token";
}
QyAuthUser.java:
package com.test.qywechat.model;
import lombok.Data;
@Data
public class QyAuthUser extends QyBaseResult {
private String userId;
private String deviceId;
@Override
public String toString() {
return "QyAuthUser [userId=" + userId + ", deviceId=" + deviceId + ", errcode=" + errcode + ", errmsg=" + errmsg + "]";
}
}
QyAuthUserInfo.java:
package com.test.qywechat.model;
import lombok.Data;
@Data
public class QyAuthUserInfo extends QyBaseResult {
private String userid;
private String name;
private String mobile;
private Integer[] department;
private String position;
//0表示未定义,1表示男性,2表示女性
private String gender;
private String email;
private String status;
private String main_department;
}
QyBaseResult.java:
package com.test.qywechat.model;
import lombok.Data;
@Data
public class QyBaseResult {
private static final String SUCCESS_CODE = "0";
public String errcode;
public String errmsg;
public boolean isSuccess() {
return errcode == null || errcode.isEmpty() || errcode.equals(SUCCESS_CODE);
}
}
QyToken.java:
package com.test.qywechat.model;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class QyToken extends QyBaseResult {
private String access_token;
private int expires_in;
}
HttpClientFactory.java:
package com.test.qywechat.httpclient;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.HttpContext;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.UnknownHostException;
import java.security.*;
/**
* httpclient 4.3.x
*
*/
public class HttpClientFactory{
private static final String[] supportedProtocols = new String[]{"TLSv1"};
public static CloseableHttpClient createHttpClient() {
return createHttpClient(100,10,5000,2);
}
/**
*
* @param maxTotal
* @param maxPerRoute
* @param timeout
* @param retryExecutionCount
* @return
*/
public static CloseableHttpClient createHttpClient(int maxTotal, int maxPerRoute, int timeout, int retryExecutionCount) {
try {
SSLContext sslContext = SSLContexts.custom().useSSL().build();
SSLConnectionSocketFactory sf = new SSLConnectionSocketFactory(sslContext, SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
poolingHttpClientConnectionManager.setMaxTotal(maxTotal);
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(maxPerRoute);
SocketConfig socketConfig = SocketConfig.custom().setSoTimeout(timeout).build();
poolingHttpClientConnectionManager.setDefaultSocketConfig(socketConfig);
return HttpClientBuilder.create()
.setConnectionManager(poolingHttpClientConnectionManager)
.setSSLSocketFactory(sf)
.setRetryHandler(new HttpRequestRetryHandlerImpl(retryExecutionCount))
.build();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
/**
* Key store 类型HttpClient
* @param keystore
* @param keyPassword
* @return
*/
public static CloseableHttpClient createKeyMaterialHttpClient(KeyStore keystore, String keyPassword, int timeout, int retryExecutionCount) {
return createKeyMaterialHttpClient(keystore, keyPassword, supportedProtocols,timeout,retryExecutionCount);
}
/**
* Key store 类型HttpClient
* @param keystore
* @param keyPassword
* @param supportedProtocols
* @return
*/
public static CloseableHttpClient createKeyMaterialHttpClient(KeyStore keystore, String keyPassword, String[] supportedProtocols, int timeout, int retryExecutionCount) {
try {
SSLContext sslContext = SSLContexts.custom().useSSL().loadKeyMaterial(keystore, keyPassword.toCharArray()).build();
SSLConnectionSocketFactory sf = new SSLConnectionSocketFactory(sslContext,supportedProtocols,
null, SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
SocketConfig socketConfig = SocketConfig.custom().setSoTimeout(timeout).build();
return HttpClientBuilder.create()
.setDefaultSocketConfig(socketConfig)
.setSSLSocketFactory(sf)
.setRetryHandler(new HttpRequestRetryHandlerImpl(retryExecutionCount))
.build();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnrecoverableKeyException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
}
return null;
}
/**
* HttpClient 超时重试
*/
private static class HttpRequestRetryHandlerImpl implements HttpRequestRetryHandler {
private int retryExecutionCount;
public HttpRequestRetryHandlerImpl(int retryExecutionCount){
this.retryExecutionCount = retryExecutionCount;
}
@Override
public boolean retryRequest(
IOException exception,
int executionCount,
HttpContext context) {
if (executionCount > retryExecutionCount) {
return false;
}
if (exception instanceof InterruptedIOException) {
return false;
}
if (exception instanceof UnknownHostException) {
return false;
}
if (exception instanceof ConnectTimeoutException) {
return true;
}
if (exception instanceof SSLException) {
return false;
}
HttpClientContext clientContext = HttpClientContext.adapt(context);
HttpRequest request = clientContext.getRequest();
boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
if (idempotent) {
// Retry if the req is considered idempotent
return true;
}
return false;
}
}
}
JsonResponseHandler.java:
package com.test.qywechat.httpclient;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Maps;
import org.apache.http.HttpEntity;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.util.EntityUtils;
import java.util.Map;
public class JsonResponseHandler{
private static Map<String, ResponseHandler<?>> map = Maps.newHashMap();
@SuppressWarnings("unchecked")
public static <T> ResponseHandler<T> createResponseHandler(final Class<T> clazz){
if(map.containsKey(clazz.getName())){
return (ResponseHandler<T>)map.get(clazz.getName());
}else{
ResponseHandler<T> responseHandler = response -> {
int status = response.getStatusLine().getStatusCode();
if (status >= 200 && status < 300) {
HttpEntity entity = response.getEntity();
String str = EntityUtils.toString(entity);
return JSONObject.parseObject(str, clazz);
} else {
throw new ClientProtocolException("Unexpected res status: " + status);
}
};
map.put(clazz.getName(), responseHandler);
return responseHandler;
}
}
}
LocalHttpClient.java:
package com.test.qywechat.httpclient;
import org.apache.http.HttpEntity;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.HashMap;
import java.util.Map;
public class LocalHttpClient {
private static final Logger logger = LoggerFactory.getLogger(LocalHttpClient.class);
private static int timeout = 5000;
private static int retryExecutionCount = 2;
protected static CloseableHttpClient httpClient = HttpClientFactory
.createHttpClient(100,10,timeout,retryExecutionCount);
private static Map<String, CloseableHttpClient> httpClient_mchKeyStore = new HashMap<String, CloseableHttpClient>();
/**
* @since 2.7.0
* @param timeout
*/
public static void setTimeout(int timeout) {
LocalHttpClient.timeout = timeout;
}
/**
* @since 2.7.0
* @param retryExecutionCount
*/
public static void setRetryExecutionCount(int retryExecutionCount) {
LocalHttpClient.retryExecutionCount = retryExecutionCount;
}
public static void init(int maxTotal,int maxPerRoute){
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
httpClient = HttpClientFactory.createHttpClient(maxTotal,maxPerRoute,timeout,retryExecutionCount);
}
/**
* 初始化 MCH HttpClient KeyStore
* @param mch_id
* @param keyStoreFilePath
*/
public static void initMchKeyStore(String mch_id,String keyStoreFilePath){
try {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
FileInputStream instream = new FileInputStream(new File(keyStoreFilePath));
keyStore.load(instream,mch_id.toCharArray());
instream.close();
CloseableHttpClient httpClient = HttpClientFactory
.createKeyMaterialHttpClient(keyStore, mch_id,timeout,retryExecutionCount);
httpClient_mchKeyStore.put(mch_id, httpClient);
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static CloseableHttpResponse execute(HttpUriRequest request){
loggerCatch(request);
try {
return httpClient.execute(request, HttpClientContext.create());
} catch (Exception e) {
e.printStackTrace();
logger.error(e.getMessage());
}
return null;
}
public static <T> T execute(HttpUriRequest request, ResponseHandler<T> responseHandler){
loggerCatch(request);
try {
return httpClient.execute(request, responseHandler, HttpClientContext.create());
} catch (Exception e) {
e.printStackTrace();
logger.error(e.getMessage());
}
return null;
}
/**
* 数据返回自动JSON对象解析
* @param request
* @param clazz
* @return
*/
public static <T> T executeJsonResult(HttpUriRequest request, Class<T> clazz){
return execute(request,JsonResponseHandler.createResponseHandler(clazz));
}
/**
* 日志记录
* @param request
*/
private static void loggerCatch(HttpUriRequest request){
if((logger.isInfoEnabled()||logger.isDebugEnabled())){
if(request instanceof HttpEntityEnclosingRequestBase){
HttpEntityEnclosingRequestBase request_base = (HttpEntityEnclosingRequestBase)request;
HttpEntity entity = request_base.getEntity();
String content = null;
//MULTIPART_FORM_DATA 请求类型判断
if(entity.getContentType().toString().indexOf(ContentType.MULTIPART_FORM_DATA.getMimeType()) == -1){
try {
content = EntityUtils.toString(entity);
} catch (Exception e) {
e.printStackTrace();
logger.error(e.getMessage());
}
}
logger.info("URI:{} {} ContentLength:{} Content:{}",
request.getURI().toString(),
entity.getContentType(),
entity.getContentLength(),
content == null?"multipart_form_data":content);
}else{
logger.info("URI:{}",request.getURI().toString());
}
}
}
}
TestController.java:
package com.test.controller;
import com.alibaba.fastjson.JSONObject;
import com.test.qywechat.model.QyAuthUser;
import com.test.qywechat.api.QyOauthApi;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/test")
@Api(value = "测试Controller", description = "测试Controller")
public class TestController {
private static final String corpId = "xxxxx";
private static final String secret_test_app_0001 = "xxx-xxxx";
/**
* 也即时用来根据 code 查询用户信息的
* @param action
* @param code
* @param state
* @return
*/
@ApiOperation(value = "企业微信code回调")
@GetMapping( "/receive/qywx/code" )
public String receiveQywxCode( @RequestParam( "action" ) String action,
@RequestParam( "code" ) String code,
@RequestParam( "state" ) String state){
System.out.println( "action = " + action );
System.out.println( "code = " + code );
System.out.println( "state = " + state );
// ps:调用 QyTokenApi.token( code,secret_test_app_0001 ).getAccess_token() 获取的,实际需要缓存起来,防止频繁调用
String accessToken = "xxx-xxx-xxx-xxx-xxx-xxx";
QyAuthUser userInfo = QyOauthApi.getAuthUserInfo(accessToken, code);
// userId 就是企业微信控制台--》通讯录--》成员详情 中的账号字段
String userId = userInfo.getUserId();
String deviceId = userInfo.getDeviceId();
JSONObject result = new JSONObject();
result.put( "姓名",userId );
result.put( "设备编码",deviceId );
return result.toJSONString();
}
@ApiOperation(value = "构造网页授权链接")
@GetMapping( "/oauth2_link" )
public String oauth2_link( ){
String redirect_uri = "http://xxx.xxx.xxx.com/test/receive/qywx/code?action=get";
String click_url = QyOauthApi.connectOauth2Authorize(corpId, redirect_uri, false, null,null);
System.out.println( "click_url is " + click_url );
String html = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\n" +
" \"http://www.w3.org/TR/html4/loose.dtd\">\n" +
"<html>\n" +
"<head>\n" +
" <meta charset=\"utf-8\">\n" +
" <meta http-equiv=\"refresh\" content=\"0; URL=" + click_url + "\">\n" +
" <title>个人信息</title>\n" +
"</head>\n" +
"<body>\n" +
" loading..." +
"</body>\n" +
"</html>";
return html;
}
}
企业微信管理端为应用“test_app_0001”新建一个菜单,菜单内容设置为“跳转到网页”,网址设置为 http://xxx.xxx.xxx.com/test/oauth2_link,其中xxx.xxx.xxx.com也就是为127.0.0.1:8080映射的公网域名,供微信回调使用, /test/oauth2_link为TestController中的接口:
为应用"test_app_0001"设置网页授权可信域名:
在手机端企业微信打开应用“test_app_0001”,点击菜单“个人信息”,就查询出了当前的用户的信息了:
二、在自己的web页面实现企业微信扫码登录:
1.在企业微信管理端对应应用中设置企业微信授权登录授权回调域:
2.TestController 中新增两个接口:
@ApiOperation(value = "企业微信授权登录二维码")
@GetMapping( "/show_qr_code" )
public String showQrCode( ) throws UnsupportedEncodingException {
System.out.println( "request /test/sso_qr_connect_link" );
String redirect_uri = "http://" + domain_ngrok_mock + "/test/home?action=get";
String sso_qr_connect_link = "https://open.work.weixin.qq.com/wwopen/sso/qrConnect?" +
"appid=" + corpId + "&" +
"agentid=" + agentId_test_app_0001 + "&" +
"redirect_uri=" + URLEncoder.encode(redirect_uri, "utf-8") + "&" +
"state=";
System.out.println( "sso_qr_connect_link is " + sso_qr_connect_link );
String html = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\n" +
" \"http://www.w3.org/TR/html4/loose.dtd\">\n" +
"<html>\n" +
"<head>\n" +
" <meta charset=\"utf-8\">\n" +
// todo 作用是该页面被打开后立即重定向到 click_url
" <meta http-equiv=\"refresh\" content=\"0; URL=" + sso_qr_connect_link + "\">\n" +
" <title>个人信息</title>\n" +
"</head>\n" +
"<body>\n" +
" loading..." +
"</body>\n" +
"</html>";
return html;
}
@ApiOperation(value = "测试主页")
@GetMapping( "/home" )
public String home( @RequestParam( "code" ) String code,
@RequestParam( value = "state",required = false ) String state ){
System.out.println( "request /test/home" );
System.out.println( "code = " + code );
System.out.println( "state = " + state );
// ps:调用 QyTokenApi.token( code,secret_test_app_0001 ).getAccess_token() 获取的,实际需要缓存起来,防止频繁调用
String accessToken = "xxx-xxx-xxx-xxx";
QyAuthUser userInfo = QyOauthApi.getAuthUserInfo(accessToken, code);
String userId = userInfo.getUserId();
QyAuthUserInfo userDetailInfo = QyOauthApi.queryAuthUserInfo(accessToken, userId);
return "welcome you! " + JSONObject.toJSONString( userDetailInfo );
}
效果: