From 20731ea77e8ea81324ba541425415e020edfbf82 Mon Sep 17 00:00:00 2001 From: "yadong.zhang" Date: Sun, 1 Sep 2024 18:15:18 +0800 Subject: [PATCH] =?UTF-8?q?:hankey:=20=E6=B7=BB=E5=8A=A0=E6=96=B0=E7=89=88?= =?UTF-8?q?`=E5=8D=8E=E4=B8=BA`=E7=99=BB=E5=BD=95=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=EF=BC=8C=E5=8E=9F`AuthHuaweiRequest`=E4=BC=9A=E5=9C=A8?= =?UTF-8?q?=E5=90=8E=E9=9D=A2=E7=89=88=E6=9C=AC=E8=A2=AB=E5=BC=83=E7=94=A8?= =?UTF-8?q?=EF=BC=8C=E5=A6=82=E6=9C=89=E4=BD=BF=E7=94=A8=EF=BC=8C=E8=AF=B7?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E5=88=B0`AuthHuaweiV3Request`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOGS.md | 1 + .../zhyd/oauth/config/AuthDefaultSource.java | 35 ++++ .../oauth/enums/scope/AuthHuaweiScope.java | 3 + .../oauth/enums/scope/AuthHuaweiV3Scope.java | 51 +++++ .../zhyd/oauth/request/AuthHuaweiRequest.java | 7 +- .../oauth/request/AuthHuaweiV3Request.java | 196 ++++++++++++++++++ .../java/me/zhyd/oauth/utils/AuthChecker.java | 2 +- 7 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 src/main/java/me/zhyd/oauth/enums/scope/AuthHuaweiV3Scope.java create mode 100644 src/main/java/me/zhyd/oauth/request/AuthHuaweiV3Request.java diff --git a/CHANGELOGS.md b/CHANGELOGS.md index 6e399cb..40a76ba 100644 --- a/CHANGELOGS.md +++ b/CHANGELOGS.md @@ -6,6 +6,7 @@ - 添加`appleid`社交登录能力。 [Github#192](https://github.com/justauth/JustAuth/pull/192) - 添加`figma`社交登录能力。 [Gitee#41](https://gitee.com/yadong.zhang/JustAuth/pulls/41) - 添加新版`企业微信扫码`登录能力。 [Github Issue#165](https://github.com/justauth/JustAuth/issues/165) + - 添加新版`华为`登录能力,原`AuthHuaweiRequest`会在后面版本被弃用,如有使用,请切换到`AuthHuaweiV3Request` - 新增微信小程序授权登录 - 优化 - 更新 Google 端点地址。[Github #198](https://github.com/justauth/JustAuth/pull/198) diff --git a/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java b/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java index dd704c3..4b583a5 100644 --- a/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java +++ b/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java @@ -709,8 +709,11 @@ public enum AuthDefaultSource implements AuthSource { /** * 华为 * + * 当前方式未来可能被废弃,建议使用 {@link this#HUAWEI_V3} + * * @since 1.10.0 */ + @Deprecated HUAWEI { @Override public String authorize() { @@ -738,6 +741,38 @@ public enum AuthDefaultSource implements AuthSource { } }, + /** + * 华为最新版本的 API + * + * @since 1.16.7 + */ + HUAWEI_V3 { + @Override + public String authorize() { + return "https://oauth-login.cloud.huawei.com/oauth2/v3/authorize"; + } + + @Override + public String accessToken() { + return "https://oauth-login.cloud.huawei.com/oauth2/v3/token"; + } + + @Override + public String userInfo() { + return "https://account.cloud.huawei.com/rest.php"; + } + + @Override + public String refresh() { + return "https://oauth-login.cloud.huawei.com/oauth2/v3/token"; + } + + @Override + public Class getTargetClass() { + return AuthHuaweiV3Request.class; + } + }, + /** * 企业微信二维码登录 * diff --git a/src/main/java/me/zhyd/oauth/enums/scope/AuthHuaweiScope.java b/src/main/java/me/zhyd/oauth/enums/scope/AuthHuaweiScope.java index 4e450f9..05d0873 100644 --- a/src/main/java/me/zhyd/oauth/enums/scope/AuthHuaweiScope.java +++ b/src/main/java/me/zhyd/oauth/enums/scope/AuthHuaweiScope.java @@ -6,12 +6,15 @@ import lombok.Getter; /** * 华为平台 OAuth 授权范围 * + * 当前方式未来可能被废弃,建议使用 {@link AuthHuaweiV3Scope} + * * @author yadong.zhang (yadong.zhang0415(a)gmail.com) * @version 1.0.0 * @since 1.0.0 */ @Getter @AllArgsConstructor +@Deprecated public enum AuthHuaweiScope implements AuthScope { /** diff --git a/src/main/java/me/zhyd/oauth/enums/scope/AuthHuaweiV3Scope.java b/src/main/java/me/zhyd/oauth/enums/scope/AuthHuaweiV3Scope.java new file mode 100644 index 0000000..f2d04a4 --- /dev/null +++ b/src/main/java/me/zhyd/oauth/enums/scope/AuthHuaweiV3Scope.java @@ -0,0 +1,51 @@ +package me.zhyd.oauth.enums.scope; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 华为平台 V3 版本 OAuth 授权范围 + * + * @author yadong.zhang (yadong.zhang0415(a)gmail.com) + * @version 1.0.0 + * @since 1.16.7 + */ +@Getter +@AllArgsConstructor +public enum AuthHuaweiV3Scope implements AuthScope { + + /** + * {@code scope} 含义,以{@code description} 为准 + */ + OPENID("openid", "基础scope,v3必选", true), + /** + * {@code scope} 含义,以{@code description} 为准 + */ + BASE_PROFILE("https://www.huawei.com/auth/account/base.profile", "获取用户的基本信息", true), + MOBILE_NUMBER("https://www.huawei.com/auth/account/mobile.number", "获取用户的手机号", false), + ACCOUNTLIST("https://www.huawei.com/auth/account/accountlist", "获取用户的账单列表", false), + + /** + * 以下两个 scope 不需要经过华为评估和验证 + */ + SCOPE_DRIVE_FILE("https://www.huawei.com/auth/drive.file", "只允许访问由应用程序创建或打开的文件", false), + SCOPE_DRIVE_APPDATA("https://www.huawei.com/auth/drive.appdata", "只允许访问由应用程序创建或打开的文件", false), + /** + * 以下四个 scope 使用前需要向drivekit@huawei.com提交申请 + *

+ * 参考:https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides-V5/server-dev-0000001050039664-V5#ZH-CN_TOPIC_0000001050039664__section1618418855716 + */ + SCOPE_DRIVE("https://www.huawei.com/auth/drive", "只允许访问由应用程序创建或打开的文件", false), + SCOPE_DRIVE_READONLY("https://www.huawei.com/auth/drive.readonly", "只允许访问由应用程序创建或打开的文件", false), + SCOPE_DRIVE_METADATA("https://www.huawei.com/auth/drive.metadata", "只允许访问由应用程序创建或打开的文件", false), + SCOPE_DRIVE_METADATA_READONLY("https://www.huawei.com/auth/drive.metadata.readonly", "只允许访问由应用程序创建或打开的文件", false), + + + ; + ; + + private final String scope; + private final String description; + private final boolean isDefault; + +} diff --git a/src/main/java/me/zhyd/oauth/request/AuthHuaweiRequest.java b/src/main/java/me/zhyd/oauth/request/AuthHuaweiRequest.java index 22a12d6..2249c7a 100644 --- a/src/main/java/me/zhyd/oauth/request/AuthHuaweiRequest.java +++ b/src/main/java/me/zhyd/oauth/request/AuthHuaweiRequest.java @@ -23,10 +23,13 @@ import static me.zhyd.oauth.enums.AuthResponseStatus.SUCCESS; /** * 华为授权登录 * + * 当前方式未来可能被废弃,建议使用 {@link AuthHuaweiV3Request} + * * @author yadong.zhang (yadong.zhang0415(a)gmail.com) * @version 1.0 * @since 1.10.0 */ +@Deprecated public class AuthHuaweiRequest extends AuthDefaultRequest { public AuthHuaweiRequest(AuthConfig config) { @@ -71,7 +74,9 @@ public class AuthHuaweiRequest extends AuthDefaultRequest { form.put("nsp_ts", System.currentTimeMillis() + ""); form.put("access_token", authToken.getAccessToken()); form.put("nsp_fmt", "JS"); - form.put("nsp_svc", "OpenUP.User.getInfo"); + form.put("open_id", "OPENID"); + // form.put("nsp_svc", "OpenUP.User.getInfo"); + form.put("nsp_svc", "huawei.oauth2.user.getTokenInfo"); String response = new HttpUtils(config.getHttpConfig()).post(source.userInfo(), form, false).getBody(); JSONObject object = JSONObject.parseObject(response); diff --git a/src/main/java/me/zhyd/oauth/request/AuthHuaweiV3Request.java b/src/main/java/me/zhyd/oauth/request/AuthHuaweiV3Request.java new file mode 100644 index 0000000..6da0ad0 --- /dev/null +++ b/src/main/java/me/zhyd/oauth/request/AuthHuaweiV3Request.java @@ -0,0 +1,196 @@ +package me.zhyd.oauth.request; + +import com.alibaba.fastjson.JSONObject; +import com.xkcoding.http.constants.Constants; +import com.xkcoding.http.support.HttpHeader; +import me.zhyd.oauth.cache.AuthStateCache; +import me.zhyd.oauth.config.AuthConfig; +import me.zhyd.oauth.config.AuthDefaultSource; +import me.zhyd.oauth.enums.AuthUserGender; +import me.zhyd.oauth.enums.scope.AuthHuaweiV3Scope; +import me.zhyd.oauth.exception.AuthException; +import me.zhyd.oauth.model.AuthCallback; +import me.zhyd.oauth.model.AuthResponse; +import me.zhyd.oauth.model.AuthToken; +import me.zhyd.oauth.model.AuthUser; +import me.zhyd.oauth.utils.*; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static me.zhyd.oauth.enums.AuthResponseStatus.SUCCESS; + +/** + * 华为授权登录 + * + * @author yadong.zhang (yadong.zhang0415(a)gmail.com) + * @version 1.0 + * @since 1.16.7 + */ +public class AuthHuaweiV3Request extends AuthDefaultRequest { + + public AuthHuaweiV3Request(AuthConfig config) { + super(config, AuthDefaultSource.HUAWEI_V3); + } + + public AuthHuaweiV3Request(AuthConfig config, AuthStateCache authStateCache) { + super(config, AuthDefaultSource.HUAWEI_V3, authStateCache); + } + + /** + * 获取access token + * + * @param authCallback 授权成功后的回调参数 + * @return token + * @see AuthDefaultRequest#authorize() + * @see AuthDefaultRequest#authorize(String) + */ + @Override + public AuthToken getAccessToken(AuthCallback authCallback) { + Map form = new HashMap<>(8); + form.put("grant_type", "authorization_code"); + form.put("code", authCallback.getCode()); + form.put("client_id", config.getClientId()); + form.put("client_secret", config.getClientSecret()); + form.put("redirect_uri", config.getRedirectUri()); + + if (config.isPkce()) { + String cacheKey = this.source.getName().concat(":code_verifier:").concat(authCallback.getState()); + String codeVerifier = this.authStateCache.get(cacheKey); + form.put("code_verifier", codeVerifier); + } + + HttpHeader httpHeader = new HttpHeader(); + httpHeader.add(Constants.CONTENT_TYPE, "application/x-www-form-urlencoded"); + String response = new HttpUtils(config.getHttpConfig()).post(source.accessToken(), form, httpHeader, false).getBody(); + return getAuthToken(response); + } + + /** + * 使用token换取用户信息 + * + * @param authToken token信息 + * @return 用户信息 + * @see AuthDefaultRequest#getAccessToken(AuthCallback) + */ + @Override + public AuthUser getUserInfo(AuthToken authToken) { + String idToken = authToken.getIdToken(); + if (StringUtils.isEmpty(idToken)) { + Map form = new HashMap<>(7); + form.put("access_token", authToken.getAccessToken()); + form.put("getNickName", "1"); + form.put("nsp_svc", "GOpen.User.getInfo"); + + HttpHeader httpHeader = new HttpHeader(); + httpHeader.add(Constants.CONTENT_TYPE, "application/x-www-form-urlencoded"); + String response = new HttpUtils(config.getHttpConfig()).post(source.userInfo(), form, httpHeader, false).getBody(); + JSONObject object = JSONObject.parseObject(response); + + this.checkResponse(object); + + return AuthUser.builder() + .rawUserInfo(object) + .uuid(object.getString("unionID")) + .username(object.getString("displayName")) + .nickname(object.getString("displayName")) + .gender(AuthUserGender.UNKNOWN) + .avatar(object.getString("headPictureURL")) + .token(authToken) + .source(source.toString()) + .build(); + } + String payload = new String(Base64.getUrlDecoder().decode(idToken.split("\\.")[1]), StandardCharsets.UTF_8); + + JSONObject object = JSONObject.parseObject(payload); + return AuthUser.builder() + .rawUserInfo(object) + .uuid(object.getString("sub")) + .username(object.getString("name")) + .nickname(object.getString("nickname")) + .gender(AuthUserGender.UNKNOWN) + .avatar(object.getString("picture")) + .token(authToken) + .source(source.toString()) + .build(); + } + + /** + * 刷新access token (续期) + * + * @param authToken 登录成功后返回的Token信息 + * @return AuthResponse + */ + @Override + public AuthResponse refresh(AuthToken authToken) { + Map form = new HashMap<>(7); + form.put("client_id", config.getClientId()); + form.put("client_secret", config.getClientSecret()); + form.put("refresh_token", authToken.getRefreshToken()); + form.put("grant_type", "refresh_token"); + + HttpHeader httpHeader = new HttpHeader(); + httpHeader.add(Constants.CONTENT_TYPE, "application/x-www-form-urlencoded"); + String response = new HttpUtils(config.getHttpConfig()).post(source.refresh(), form, httpHeader, false).getBody(); + return AuthResponse.builder().code(SUCCESS.getCode()).data(getAuthToken(response)).build(); + } + + private AuthToken getAuthToken(String response) { + JSONObject object = JSONObject.parseObject(response); + + this.checkResponse(object); + + return AuthToken.builder() + .accessToken(object.getString("access_token")) + .expireIn(object.getIntValue("expires_in")) + .refreshToken(object.getString("refresh_token")) + .idToken(object.getString("id_token")) + .build(); + } + + /** + * 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state} + * + * @param state state 验证授权流程的参数,可以防止csrf + * @return 返回授权地址 + * @since 1.9.3 + */ + @Override + public String authorize(String state) { + String realState = getRealState(state); + UrlBuilder builder = UrlBuilder.fromBaseUrl(super.authorize(realState)) + .queryParam("access_type", "offline") + .queryParam("scope", this.getScopes(" ", true, AuthScopeUtils.getDefaultScopes(AuthHuaweiV3Scope.values()))); + + if (config.isPkce()) { + String cacheKey = this.source.getName().concat(":code_verifier:").concat(realState); + String codeVerifier = PkceUtil.generateCodeVerifier(); + String codeChallengeMethod = "S256"; + String codeChallenge = PkceUtil.generateCodeChallenge(codeChallengeMethod, codeVerifier); + builder.queryParam("code_challenge", codeChallenge) + .queryParam("code_challenge_method", codeChallengeMethod); + // 缓存 codeVerifier 十分钟 + this.authStateCache.cache(cacheKey, codeVerifier, TimeUnit.MINUTES.toMillis(10)); + } + return builder.build(); + } + + /** + * 校验响应结果 + * + * @param object 接口返回的结果 + */ + private void checkResponse(JSONObject object) { + if (object.containsKey("NSP_STATUS")) { + throw new AuthException(object.getString("error")); + } + if (object.containsKey("error")) { + throw new AuthException(object.getString("sub_error") + ":" + object.getString("error_description")); + } + } + + +} diff --git a/src/main/java/me/zhyd/oauth/utils/AuthChecker.java b/src/main/java/me/zhyd/oauth/utils/AuthChecker.java index 74c6095..5fe6792 100644 --- a/src/main/java/me/zhyd/oauth/utils/AuthChecker.java +++ b/src/main/java/me/zhyd/oauth/utils/AuthChecker.java @@ -80,7 +80,7 @@ public class AuthChecker { return; } String code = callback.getCode(); - if (source == AuthDefaultSource.HUAWEI) { + if (StringUtils.isEmpty(code) && source == AuthDefaultSource.HUAWEI) { code = callback.getAuthorization_code(); } if (StringUtils.isEmpty(code)) {