From d9952968d10b361bb10217ede4fdf4929de011e8 Mon Sep 17 00:00:00 2001 From: harrylee Date: Tue, 10 Sep 2019 23:18:24 +0800 Subject: [PATCH 1/2] =?UTF-8?q?:memo:=20=E9=9B=86=E6=88=90=E4=BA=AC?= =?UTF-8?q?=E4=B8=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zhyd/oauth/config/AuthDefaultSource.java | 25 +++++ .../me/zhyd/oauth/request/AuthJdRequest.java | 100 ++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 src/main/java/me/zhyd/oauth/request/AuthJdRequest.java diff --git a/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java b/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java index 9b239e1..f40384f 100644 --- a/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java +++ b/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java @@ -672,6 +672,31 @@ public enum AuthDefaultSource implements AuthSource { public String refresh() { return "https://open-api.shop.ele.me/token"; } + }, + /** + * 京东 + * @since + */ + JD { + @Override + public String authorize() { + return "https://open-oauth.jd.com/oauth2/to_login"; + } + + @Override + public String accessToken() { + return "https://open-oauth.jd.com/oauth2/access_token"; + } + + @Override + public String userInfo() { + return null; + } + + @Override + public String refresh() { + return "https://open-oauth.jd.com/oauth2/refresh_token"; + } } } diff --git a/src/main/java/me/zhyd/oauth/request/AuthJdRequest.java b/src/main/java/me/zhyd/oauth/request/AuthJdRequest.java new file mode 100644 index 0000000..f8bacec --- /dev/null +++ b/src/main/java/me/zhyd/oauth/request/AuthJdRequest.java @@ -0,0 +1,100 @@ +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.AuthDefaultSource; +import me.zhyd.oauth.enums.AuthResponseStatus; +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.UrlBuilder; + +/** + * 京东登录 + * + * @author harry.lee (harryleexyz@qq.com) + * @since + */ +public class AuthJdRequest extends AuthDefaultRequest { + + public AuthJdRequest(AuthConfig config) { + super(config, AuthDefaultSource.JD); + } + + public AuthJdRequest(AuthConfig config, AuthStateCache authStateCache) { + super(config, AuthDefaultSource.JD, authStateCache); + } + + @Override + protected AuthToken getAccessToken(AuthCallback authCallback) { + HttpResponse response = HttpRequest.post(source.accessToken()) + .form("app_key", config.getClientId()) + .form("app_secret", config.getClientSecret()) + .form("grant_type", "authorization_code") + .form("code", authCallback.getCode()) + .execute(); + JSONObject object = JSONObject.parseObject(response.body()); + + this.checkResponse(object); + + return AuthToken.builder() + .accessToken(object.getString("access_token")) + .expireIn(object.getIntValue("expires_in")) + .refreshToken(object.getString("refresh_token")) + .scope(object.getString("scope")) + .openId(object.getString("open_id")) + .build(); + } + + @Override + protected AuthUser getUserInfo(AuthToken authToken) { + return null; + } + + @Override + public AuthResponse refresh(AuthToken oldToken) { + HttpResponse response = HttpRequest.post(source.refresh()) + .form("app_key", config.getClientId()) + .form("app_secret", config.getClientSecret()) + .form("grant_type", "refresh_token") + .form("refresh_token", oldToken.getRefreshToken()) + .execute(); + JSONObject object = JSONObject.parseObject(response.body()); + + this.checkResponse(object); + + return AuthResponse.builder() + .code(AuthResponseStatus.SUCCESS.getCode()) + .data(AuthToken.builder() + .accessToken(object.getString("access_token")) + .expireIn(object.getIntValue("expires_in")) + .refreshToken(object.getString("refresh_token")) + .scope(object.getString("scope")) + .openId(object.getString("open_id")) + .build()) + .build(); + } + + private void checkResponse(JSONObject object) { + if (object.containsKey("msg")) { + throw new AuthException(object.getString("msg")); + } + } + + @Override + public String authorize(String state) { + return UrlBuilder.fromBaseUrl(source.authorize()) + .queryParam("app_key", config.getClientId()) + .queryParam("response_type", "code") + .queryParam("redirect_uri", config.getRedirectUri()) + .queryParam("scope", "snsapi_base") + .queryParam("state", getRealState(state)) + .build(); + } + +} From 469558bddc1feebe298187eea8a3bdd138c10971 Mon Sep 17 00:00:00 2001 From: harrylee Date: Wed, 11 Sep 2019 15:06:50 +0800 Subject: [PATCH 2/2] =?UTF-8?q?:sparkles:=20=E9=9B=86=E6=88=90=E4=BA=AC?= =?UTF-8?q?=E4=B8=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zhyd/oauth/config/AuthDefaultSource.java | 2 +- .../me/zhyd/oauth/request/AuthJdRequest.java | 91 ++++++++++++++++++- .../me/zhyd/oauth/utils/GlobalAuthUtil.java | 20 ++++ .../java/me/zhyd/oauth/utils/UrlBuilder.java | 12 ++- 4 files changed, 122 insertions(+), 3 deletions(-) diff --git a/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java b/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java index f40384f..d30e627 100644 --- a/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java +++ b/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java @@ -690,7 +690,7 @@ public enum AuthDefaultSource implements AuthSource { @Override public String userInfo() { - return null; + return "https://api.jd.com/routerjson"; } @Override diff --git a/src/main/java/me/zhyd/oauth/request/AuthJdRequest.java b/src/main/java/me/zhyd/oauth/request/AuthJdRequest.java index f8bacec..04db60e 100644 --- a/src/main/java/me/zhyd/oauth/request/AuthJdRequest.java +++ b/src/main/java/me/zhyd/oauth/request/AuthJdRequest.java @@ -7,15 +7,24 @@ import me.zhyd.oauth.cache.AuthStateCache; import me.zhyd.oauth.config.AuthConfig; import me.zhyd.oauth.config.AuthDefaultSource; 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.AuthResponse; import me.zhyd.oauth.model.AuthToken; import me.zhyd.oauth.model.AuthUser; +import me.zhyd.oauth.utils.GlobalAuthUtil; +import me.zhyd.oauth.utils.StringUtils; import me.zhyd.oauth.utils.UrlBuilder; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Map; +import java.util.TreeMap; + /** * 京东登录 + * link: http://open.jd.com/home/home#/doc/common?listId=717 * * @author harry.lee (harryleexyz@qq.com) * @since @@ -51,9 +60,86 @@ public class AuthJdRequest extends AuthDefaultRequest { .build(); } + /** + * link: http://jos.jd.com/api/showTools.htm?id=3051&groupId=106 + * postUrl: https://api.jd.com/routerjson?v=2.0&method=jingdong.user.getUserInfoByOpenId + * &app_key=x&access_token=x&360buy_param_json={"openId":"x"} + * ×tamp=2019-09-11 11:12:26&sign=DB5278CD12443BEA22C5E5EA05A30D2B + * @param authToken token信息 + * @return + */ @Override protected AuthUser getUserInfo(AuthToken authToken) { - return null; + UrlBuilder urlBuilder = UrlBuilder.fromBaseUrl(source.userInfo()) + .queryParam("access_token", authToken.getAccessToken()) + .queryParam("app_key", config.getClientId()) + .queryParam("method", "jingdong.user.getUserInfoByOpenId") + .queryParam("360buy_param_json", "{\"openId\":\"" + authToken.getOpenId() + "\"}") + .queryParam("timestamp", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))) + .queryParam("v", "2.0"); + urlBuilder.queryParam("sign", sign(urlBuilder.getReadParams())); + HttpResponse response = HttpRequest.post(urlBuilder.build(true)).execute(); + JSONObject object = JSONObject.parseObject(response.body()); + + this.checkResponse(object); + + JSONObject data = this.getUserDataJsonObject(object); + + return AuthUser.builder() + .uuid(authToken.getOpenId()) + .username(data.getString("nickname")) + .nickname(data.getString("nickname")) + .avatar(data.getString("imageUrl")) + .gender(AuthUserGender.getRealGender(data.getString("gendar"))) + .token(authToken) + .source(source.toString()) + .build(); + } + + /** + * 个人用户无法申请应用 + * 暂时只能参考官网给出的返回结果解析 + * link: http://open.jd.com/home/home#/doc/api?apiCateId=106&apiId=3051&apiName=jingdong.user.getUserInfoByOpenId + * + * @param object 请求返回结果 + * @return data JSONObject + */ + private JSONObject getUserDataJsonObject(JSONObject object) { + return object.getJSONObject("jingdong_user_getUserInfoByOpenId_response") + .getJSONObject("getuserinfobyappidandopenid_result") + .getJSONObject("data"); + } + + /** + * 宙斯签名规则过程如下: + * 将所有请求参数按照字母先后顺序排列,例如将access_token,app_key,method,timestamp,v 排序为access_token,app_key,method,timestamp,v + * 1.把所有参数名和参数值进行拼接,例如:access_tokenxxxapp_keyxxxmethodxxxxxxtimestampxxxxxxvx + * 2.把appSecret夹在字符串的两端,例如:appSecret+XXXX+appSecret + * 3.使用MD5进行加密,再转化成大写 + * link: http://open.jd.com/home/home#/doc/common?listId=890 + * link: https://github.com/pingjiang/jd-open-api-sdk-src/blob/master/src/main/java/com/jd/open/api/sdk/DefaultJdClient.java + * + * @param params + * @return + */ + private String sign(Map params) { + // 放入 TreeMap 排序 + Map treeMap = new TreeMap<>(params); + String appSecret = config.getClientSecret(); + StringBuilder signBuilder = new StringBuilder(appSecret); + for (Map.Entry entry : treeMap.entrySet()) { + String name = entry.getKey(); + String value = (String) entry.getValue(); + if (StringUtils.isNotEmpty(name) && StringUtils.isNotEmpty(value)) { + signBuilder.append(name).append(value); + } + } + signBuilder.append(appSecret); + try { + return GlobalAuthUtil.jdMd5(signBuilder.toString()); + } catch (Exception e) { + throw new AuthException("build sign to jdMd5 error"); + } } @Override @@ -84,6 +170,9 @@ public class AuthJdRequest extends AuthDefaultRequest { if (object.containsKey("msg")) { throw new AuthException(object.getString("msg")); } + if (object.containsKey("error_response")) { + throw new AuthException(object.getJSONObject("error_response").getString("zh_desc")); + } } @Override diff --git a/src/main/java/me/zhyd/oauth/utils/GlobalAuthUtil.java b/src/main/java/me/zhyd/oauth/utils/GlobalAuthUtil.java index 5c248f0..ea0f6b2 100644 --- a/src/main/java/me/zhyd/oauth/utils/GlobalAuthUtil.java +++ b/src/main/java/me/zhyd/oauth/utils/GlobalAuthUtil.java @@ -238,4 +238,24 @@ public class GlobalAuthUtil { return null == buffer ? "" : buffer.toString(); } + /** + * 京东md5加密 + * link: https://github.com/pingjiang/jd-open-api-sdk-src/blob/master/src/main/java/com/jd/open/api/sdk/internal/util/CodecUtil.java + * @param source + * @return + * @throws Exception + */ + public static String jdMd5(String source) throws Exception { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] bytes = md.digest(source.getBytes(StandardCharsets.UTF_8)); + StringBuilder sign = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + String hex = Integer.toHexString(bytes[i] & 0xff); + if (hex.length() == 1) { + sign.append("0"); + } + sign.append(hex.toUpperCase()); + } + return sign.toString(); + } } diff --git a/src/main/java/me/zhyd/oauth/utils/UrlBuilder.java b/src/main/java/me/zhyd/oauth/utils/UrlBuilder.java index c16ea0c..940c1af 100644 --- a/src/main/java/me/zhyd/oauth/utils/UrlBuilder.java +++ b/src/main/java/me/zhyd/oauth/utils/UrlBuilder.java @@ -2,7 +2,6 @@ package me.zhyd.oauth.utils; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.StrUtil; import lombok.Setter; import java.util.LinkedHashMap; @@ -26,6 +25,17 @@ public class UrlBuilder { } + /** + * 只读的 Map, clone 内部实现也是 putAll + * HashMap#putAll 可实现对 基本类型 和 String 类型的深度复制 + * + * @return Map + */ + @SuppressWarnings("unchecked") + public Map getReadParams() { + return (Map) ((LinkedHashMap) params).clone(); + } + /** * @param baseUrl 基础路径 * @return the new {@code UrlBuilder}