diff --git a/README.md b/README.md index c4643e020ff57a823ac833712241762843196c04..0422f73436dbf34b6312c6b09d06d361e8fc2aa0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
-
+
@@ -15,7 +15,7 @@
-
+
@@ -89,7 +89,7 @@ JustAuth,如你所见,它仅仅是一个**第三方授权登录**的**工具
-
+
@@ -18,7 +18,7 @@
-
+
@@ -91,7 +91,7 @@ JustAuth,如你所见,它仅仅是一个**第三方授权登录**的**工具
- * 1.9.0版本新增参数 + * + * @since 1.9.0 */ private String stackOverflowKey; + + /** + * 企业微信,授权方的网页应用ID + * + * @since 1.10.0 + */ + private String agentId; } diff --git a/src/main/java/me/zhyd/oauth/config/AuthSource.java b/src/main/java/me/zhyd/oauth/config/AuthSource.java index dd7a4368fc4453621a63c40ffad62427d822e978..3cd14a16b9b0d69688bf2a23de7feaade163ea01 100644 --- a/src/main/java/me/zhyd/oauth/config/AuthSource.java +++ b/src/main/java/me/zhyd/oauth/config/AuthSource.java @@ -1,7 +1,7 @@ package me.zhyd.oauth.config; -import me.zhyd.oauth.exception.AuthException; import me.zhyd.oauth.enums.AuthResponseStatus; +import me.zhyd.oauth.exception.AuthException; /** * 各api需要的url, 用枚举类分平台类型管理 @@ -522,7 +522,8 @@ public enum AuthSource { /** * 华为 - * @since 1.9.6 + * + * @since 1.10.0 */ HUAWEI { @Override @@ -544,6 +545,28 @@ public enum AuthSource { public String refresh() { return "https://oauth-login.cloud.huawei.com/oauth2/v2/token"; } + }, + + /** + * 企业微信 + * + * @since 1.10.0 + */ + WECHAT_ENTERPRISE { + @Override + public String authorize() { + return "https://open.work.weixin.qq.com/wwopen/sso/qrConnect"; + } + + @Override + public String accessToken() { + return "https://qyapi.weixin.qq.com/cgi-bin/gettoken"; + } + + @Override + public String userInfo() { + return "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo"; + } }; /** diff --git a/src/main/java/me/zhyd/oauth/log/Log.java b/src/main/java/me/zhyd/oauth/log/Log.java index a6f54f9f107e0702505afafdf71d7f3efcc32f88..7a783373bf83354be34a5bc68a06269abd4a5b5f 100644 --- a/src/main/java/me/zhyd/oauth/log/Log.java +++ b/src/main/java/me/zhyd/oauth/log/Log.java @@ -17,7 +17,7 @@ import java.time.format.DateTimeFormatter; * @see Log#warn(String, Throwable) * @see Log#error(String) * @see Log#error(String, Throwable) - * @since 1.9.6 + * @since 1.10.0 */ public class Log { @@ -109,7 +109,7 @@ public class Log { * 日志级别 * * @author yadong.zhang (yadong.zhang0415(a)gmail.com) - * @since 1.9.6 + * @since 1.10.0 */ @Getter @AllArgsConstructor @@ -134,7 +134,7 @@ public class Log { * 日志配置 * * @author yadong.zhang (yadong.zhang0415(a)gmail.com) - * @since 1.9.6 + * @since 1.10.0 */ static class Config { diff --git a/src/main/java/me/zhyd/oauth/model/AuthCallback.java b/src/main/java/me/zhyd/oauth/model/AuthCallback.java index 0ea02707d625c41177274def0e2be40e1358b9b0..4e268f0016f0e496b35b9bd64cf66b72299af974 100644 --- a/src/main/java/me/zhyd/oauth/model/AuthCallback.java +++ b/src/main/java/me/zhyd/oauth/model/AuthCallback.java @@ -31,7 +31,7 @@ public class AuthCallback { /** * 华为授权登录接受code的参数名 * - * @since 1.9.6 + * @since 1.10.0 */ private String authorization_code; } diff --git a/src/main/java/me/zhyd/oauth/model/AuthToken.java b/src/main/java/me/zhyd/oauth/model/AuthToken.java index 805a196c3c93b2149c07c5c44265e3d998fedea7..5ae5fbd6ea2682a3f3029838cb31e887b6645703 100644 --- a/src/main/java/me/zhyd/oauth/model/AuthToken.java +++ b/src/main/java/me/zhyd/oauth/model/AuthToken.java @@ -1,7 +1,6 @@ package me.zhyd.oauth.model; import lombok.Builder; -import lombok.Data; import lombok.Getter; import lombok.Setter; @@ -36,4 +35,11 @@ public class AuthToken { private String macAlgorithm; private String macKey; + /** + * 企业微信附带属性 + * + * @since 1.10.0 + */ + private String code; + } diff --git a/src/main/java/me/zhyd/oauth/request/AuthHuaweiRequest.java b/src/main/java/me/zhyd/oauth/request/AuthHuaweiRequest.java index be33552ac37ac480d0a261064e315fb2f5687834..aea00abc179b1e26fc3c76db7003af348d872a7a 100644 --- a/src/main/java/me/zhyd/oauth/request/AuthHuaweiRequest.java +++ b/src/main/java/me/zhyd/oauth/request/AuthHuaweiRequest.java @@ -21,7 +21,7 @@ import static me.zhyd.oauth.enums.AuthResponseStatus.SUCCESS; * * @author yadong.zhang (yadong.zhang0415(a)gmail.com) * @version 1.0 - * @since 1.9.6 + * @since 1.10.0 */ public class AuthHuaweiRequest extends AuthDefaultRequest { diff --git a/src/main/java/me/zhyd/oauth/request/AuthWeChatEnterpriseRequest.java b/src/main/java/me/zhyd/oauth/request/AuthWeChatEnterpriseRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..3d9befdb43ba4481e08516a5c5170aa00d5f2b69 --- /dev/null +++ b/src/main/java/me/zhyd/oauth/request/AuthWeChatEnterpriseRequest.java @@ -0,0 +1,171 @@ +package me.zhyd.oauth.request; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import com.alibaba.fastjson.JSONObject; +import me.zhyd.oauth.cache.AuthStateCache; +import me.zhyd.oauth.config.AuthConfig; +import me.zhyd.oauth.config.AuthSource; +import me.zhyd.oauth.enums.AuthResponseStatus; +import me.zhyd.oauth.enums.AuthUserGender; +import me.zhyd.oauth.exception.AuthException; +import me.zhyd.oauth.model.AuthCallback; +import me.zhyd.oauth.model.AuthToken; +import me.zhyd.oauth.model.AuthUser; +import me.zhyd.oauth.utils.UrlBuilder; + +/** + *
+ * 企业微信登录 + *
+ * + * @author yangkai.shen (https://xkcoding.com) + * @since 1.10.0 + */ +public class AuthWeChatEnterpriseRequest extends AuthDefaultRequest { + public AuthWeChatEnterpriseRequest(AuthConfig config) { + super(config, AuthSource.WECHAT_ENTERPRISE); + } + + public AuthWeChatEnterpriseRequest(AuthConfig config, AuthStateCache authStateCache) { + super(config, AuthSource.WECHAT_ENTERPRISE, authStateCache); + } + + /** + * 微信的特殊性,此时返回的信息同时包含 openid 和 access_token + * + * @param authCallback 回调返回的参数 + * @return 所有信息 + */ + @Override + protected AuthToken getAccessToken(AuthCallback authCallback) { + HttpResponse response = doGetAuthorizationCode(accessTokenUrl(authCallback.getCode())); + + JSONObject object = this.checkResponse(response); + + return AuthToken.builder() + .accessToken(object.getString("access_token")) + .expireIn(object.getIntValue("expires_in")) + .code(authCallback.getCode()) + .build(); + } + + @Override + protected AuthUser getUserInfo(AuthToken authToken) { + HttpResponse response = doGetUserInfo(authToken); + JSONObject object = this.checkResponse(response); + + // 返回 OpenId 或其他,均代表非当前企业用户,不支持 + if (!object.containsKey("UserId")) { + throw new AuthException(AuthResponseStatus.UNIDENTIFIED_PLATFORM); + } + String userId = object.getString("UserId"); + HttpResponse userDetailResponse = getUserDetail(authToken.getAccessToken(), userId); + JSONObject userDetail = this.checkResponse(userDetailResponse); + + String gender = getRealGender(userDetail); + + return AuthUser.builder() + .username(userDetail.getString("name")) + .nickname(userDetail.getString("alias")) + .avatar(userDetail.getString("avatar")) + .location(userDetail.getString("address")) + .email(userDetail.getString("email")) + .uuid(userId) + .gender(AuthUserGender.getRealGender(gender)) + .token(authToken) + .source(source) + .build(); + } + + /** + * 校验请求结果 + * + * @param response 请求结果 + * @return 如果请求结果正常,则返回JSONObject + */ + private JSONObject checkResponse(HttpResponse response) { + JSONObject object = JSONObject.parseObject(response.body()); + + if (object.containsKey("errcode") && object.getIntValue("errcode") != 0) { + throw new AuthException(object.getIntValue("errcode"), object.getString("errmsg")); + } + + return object; + } + + /** + * 获取用户的实际性别,0表示未定义,1表示男性,2表示女性 + * + * @param userDetail 用户详情 + * @return 用户性别 + */ + private String getRealGender(JSONObject userDetail) { + int gender = userDetail.getIntValue("gender"); + if (AuthUserGender.MALE.getCode() == gender) { + return "1"; + } + return 2 == gender ? "0" : null; + } + + /** + * 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state} + * + * @param state state 验证授权流程的参数,可以防止csrf + * @return 返回授权地址 + * @since 1.9.3 + */ + @Override + public String authorize(String state) { + return UrlBuilder.fromBaseUrl(source.authorize()) + .queryParam("appid", config.getClientId()) + .queryParam("agentid", config.getAgentId()) + .queryParam("redirect_uri", config.getRedirectUri()) + .queryParam("state", getRealState(state)) + .build(); + } + + /** + * 返回获取accessToken的url + * + * @param code 授权码 + * @return 返回获取accessToken的url + */ + @Override + protected String accessTokenUrl(String code) { + return UrlBuilder.fromBaseUrl(source.accessToken()) + .queryParam("corpid", config.getClientId()) + .queryParam("corpsecret", config.getClientSecret()) + .build(); + } + + /** + * 返回获取userInfo的url + * + * @param authToken 用户授权后的token + * @return 返回获取userInfo的url + */ + @Override + protected String userInfoUrl(AuthToken authToken) { + return UrlBuilder.fromBaseUrl(source.userInfo()) + .queryParam("access_token", authToken.getAccessToken()) + .queryParam("code", authToken.getCode()) + .build(); + } + + /** + * 用户详情 + * + * @param accessToken accessToken + * @param userId 企业内用户id + * @return 用户详情 + */ + private HttpResponse getUserDetail(String accessToken, String userId) { + String userDetailUrl = UrlBuilder.fromBaseUrl("https://qyapi.weixin.qq.com/cgi-bin/user/get") + .queryParam("access_token", accessToken) + .queryParam("userid", userId) + .build(); + return HttpRequest.get(userDetailUrl).execute(); + } + +} diff --git a/src/main/java/me/zhyd/oauth/utils/AuthChecker.java b/src/main/java/me/zhyd/oauth/utils/AuthChecker.java index 8eaee0ef7eaf2a3a9532b69a413d37f1296d2302..8e56e2120d1cedd9c1ec19f8fedf74549099ba5f 100644 --- a/src/main/java/me/zhyd/oauth/utils/AuthChecker.java +++ b/src/main/java/me/zhyd/oauth/utils/AuthChecker.java @@ -30,6 +30,9 @@ public class AuthChecker { if (isSupported && AuthSource.STACK_OVERFLOW == source) { isSupported = StringUtils.isNotEmpty(config.getStackOverflowKey()); } + if (isSupported && AuthSource.WECHAT_ENTERPRISE == source){ + isSupported = StringUtils.isNotEmpty(config.getAgentId()); + } return isSupported; } @@ -58,7 +61,7 @@ public class AuthChecker { /** * 校验回调传回的code *- * {@code v1.9.6}版本中改为传入{@code source}和{@code callback},对于不同平台使用不同参数接受code的情况统一做处理 + * {@code v1.10.0}版本中改为传入{@code source}和{@code callback},对于不同平台使用不同参数接受code的情况统一做处理 * * @param source 当前授权平台 * @param callback 从第三方授权回调回来时传入的参数集合 diff --git a/src/test/java/me/zhyd/oauth/sdk/ThirdPartSdkTest.java b/src/test/java/me/zhyd/oauth/sdk/ThirdPartSdkTest.java index 9c89ed19942e38aceffe7c2ba59273942f6d28e9..6aa252c53c6a76641480e2a44e138c55a3697477 100644 --- a/src/test/java/me/zhyd/oauth/sdk/ThirdPartSdkTest.java +++ b/src/test/java/me/zhyd/oauth/sdk/ThirdPartSdkTest.java @@ -8,19 +8,19 @@ import org.junit.Test; /** * @author yadong.zhang (yadong.zhang0415(a)gmail.com) * @version 1.0 - * @since 1.9.6 + * @since 1.10.0 */ public class ThirdPartSdkTest { @Test public void huawei() { - String code = "CF1IvwdXw18r6LTfoRSgs+LrdP/DuO1VJJmAD0up2grQrSs3gcuyrt1O+jjWp7/TFiBy9IlPepNs/PUggcLe8cgjesqj1+DGXXojJsjEqsokFCCU0eJVt1F02zLDWH1+bq40HSlljXDaTvCBNrqWJJnIZhRetoV9pocrWPLZpYrx/h0iaC9T0GjMRVEXC//LnTAlTjg7"; + String code = "CF1IwmFc6uZABI9Y795BkhXfvHidIFFw04I4Zc4KML4n+vlXxwNUcQKS4xlopjFDpEk6LzQbjwdTNxvjZ9jqnd/1m5nswhx8X7e0/dL2kyGAMVZWFgVq9ClxNN18b+Z0xtfJjkm7bDnfC3W5h4COgTCoLSjiWKSHWp5hCunp6pQRo1FHovZXm13TLNlhF9mCVtJx3kTQ"; HttpResponse response = HttpRequest.post("https://oauth-login.cloud.huawei.com/oauth2/v2/token") .form("grant_type", "authorization_code") .form("code", code) - .form("client_id", "100xxxxx") - .form("client_secret", "22aea400bef603xxxxxbfb80d") - .form("redirect_uri", "http://localhost:8443/huawei/login") + .form("client_id", "100994535") + .form("client_secret", "22aea400bef603fef26d15a79c806eb477b35de0a529758f2a3b1bda32bfb80d") + .form("redirect_uri", "http://127.0.0.1:8443/oauth/callback/huawei") .execute(); System.out.println(response.body()); @@ -31,7 +31,7 @@ public class ThirdPartSdkTest { .form("nsp_ts", System.currentTimeMillis()) .form("access_token", JSONObject.parseObject(response.body()).getString("access_token")) .form("nsp_fmt", "JS") -// .form("nsp_cb", "") +// .form("nsp_cb", "_jqjsp") .form("nsp_svc", "OpenUP.User.getInfo") .execute(); System.out.println(response2.body());