From 05374e7a1f69f74f74e02433a9a1862852a37a8f Mon Sep 17 00:00:00 2001 From: "yadong.zhang" Date: Fri, 6 Sep 2019 19:22:49 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E9=9B=86=E6=88=90=E2=80=9C?= =?UTF-8?q?=E9=A5=BF=E4=BA=86=E4=B9=88=E2=80=9D=E6=8E=88=E6=9D=83=E7=99=BB?= =?UTF-8?q?=E5=BD=95=EF=BC=88=E6=9C=AA=E6=B5=8B=E8=AF=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.en-US.md | 2 + README.md | 2 + docs/README.md | 3 +- docs/update.md | 1 + .../java/me/zhyd/oauth/config/AuthSource.java | 29 +++ .../zhyd/oauth/request/AuthElemeRequest.java | 193 ++++++++++++++++++ .../oauth/request/AuthMeituanRequest.java | 4 +- .../me/zhyd/oauth/utils/GlobalAuthUtil.java | 124 ++++++++++- 8 files changed, 352 insertions(+), 6 deletions(-) create mode 100644 src/main/java/me/zhyd/oauth/request/AuthElemeRequest.java diff --git a/README.en-US.md b/README.en-US.md index 6b6ceff..523798d 100644 --- a/README.en-US.md +++ b/README.en-US.md @@ -67,6 +67,7 @@ + @@ -152,6 +153,7 @@ authRequest.login(callback); | | [AuthKujialeRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthKujialeRequest.java) | 参考文档 | | | [AuthGitlabRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthGitlabRequest.java) | 参考文档 | | | [AuthMeituanRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthMeituanRequest.java) | 参考文档 | +| | [AuthElemeRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthElemeRequest.java) | 参考文档 | | | [AuthCsdnRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthCsdnRequest.java) | 无 | diff --git a/README.md b/README.md index f59f473..a22e10d 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ + @@ -162,6 +163,7 @@ authRequest.login(callback); | | [AuthKujialeRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthKujialeRequest.java) | 参考文档 | | | [AuthGitlabRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthGitlabRequest.java) | 参考文档 | | | [AuthMeituanRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthMeituanRequest.java) | 参考文档 | +| | [AuthElemeRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthElemeRequest.java) | 参考文档 | | | [AuthCsdnRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthCsdnRequest.java) | 无 | _请知悉:经咨询CSDN官方客服得知,CSDN的授权开放平台已经下线。如果以前申请过的应用,可以继续使用,但是不再支持申请新的应用。so, 本项目中的CSDN登录只能针对少部分用户使用了_ diff --git a/docs/README.md b/docs/README.md index cab35c2..a59bc6c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -87,10 +87,11 @@ JustAuth,如你所见,它仅仅是一个**第三方授权登录**的**工具 | | [AuthStackOverflowRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthStackOverflowRequest.java) | 参考文档 | | | [AuthHuaweiRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthHuaweiRequest.java) | 参考文档 | | | [AuthWeChatEnterpriseRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthWeChatEnterpriseRequest.java) | 参考文档 | -| | [AuthCsdnRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthCsdnRequest.java) | 无 | | | [AuthKujialeRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthKujialeRequest.java) | 参考文档 | | | [AuthGitlabRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthGitlabRequest.java) | 参考文档 | | | [AuthMeituanRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthMeituanRequest.java) | 参考文档 | +| | [AuthElemeRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthElemeRequest.java) | 参考文档 | +| | [AuthCsdnRequest](https://gitee.com/yadong.zhang/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthCsdnRequest.java) | 无 | ## 快速开始 diff --git a/docs/update.md b/docs/update.md index 6d30a08..b5e5473 100644 --- a/docs/update.md +++ b/docs/update.md @@ -2,6 +2,7 @@ ### 2019/09/06 - 集成“美团”授权登录 +- 集成“饿了么”授权登录 - 升级Fastjson依赖到1.2.60,预防[“Fastjson < 1.2.60 远程拒绝服务漏洞预警”](https://card.weibo.com/article/m/show/id/2309404413257925394542) ## v1.11.0 diff --git a/src/main/java/me/zhyd/oauth/config/AuthSource.java b/src/main/java/me/zhyd/oauth/config/AuthSource.java index ee0c189..f4ecf5e 100644 --- a/src/main/java/me/zhyd/oauth/config/AuthSource.java +++ b/src/main/java/me/zhyd/oauth/config/AuthSource.java @@ -643,6 +643,35 @@ public enum AuthSource { public String refresh() { return "https://openapi.waimai.meituan.com/oauth/refresh_token"; } + }, + + /** + * 饿了么 + *

+ * 注:集成的是正式环境,非沙箱环境 + * + * @since 1.12.0 + */ + ELEME { + @Override + public String authorize() { + return "https://open-api.shop.ele.me/authorize"; + } + + @Override + public String accessToken() { + return "https://open-api.shop.ele.me/token"; + } + + @Override + public String userInfo() { + return "https://open-api.shop.ele.me/api/v1/"; + } + + @Override + public String refresh() { + return "https://open-api.shop.ele.me/token"; + } }; /** diff --git a/src/main/java/me/zhyd/oauth/request/AuthElemeRequest.java b/src/main/java/me/zhyd/oauth/request/AuthElemeRequest.java new file mode 100644 index 0000000..9773e69 --- /dev/null +++ b/src/main/java/me/zhyd/oauth/request/AuthElemeRequest.java @@ -0,0 +1,193 @@ +package me.zhyd.oauth.request; + +import cn.hutool.core.codec.Base64; +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.AuthResponse; +import me.zhyd.oauth.model.AuthToken; +import me.zhyd.oauth.model.AuthUser; +import me.zhyd.oauth.utils.GlobalAuthUtil; +import me.zhyd.oauth.utils.UrlBuilder; +import me.zhyd.oauth.utils.UuidUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * 饿了么 + *

+ * 注:集成的是正式环境,非沙箱环境 + * + * @author yadong.zhang (yadong.zhang0415(a)gmail.com) + * @since 1.12.0 + */ +public class AuthElemeRequest extends AuthDefaultRequest { + + public AuthElemeRequest(AuthConfig config) { + super(config, AuthSource.ELEME); + } + + public AuthElemeRequest(AuthConfig config, AuthStateCache authStateCache) { + super(config, AuthSource.ELEME, authStateCache); + } + + @Override + protected AuthToken getAccessToken(AuthCallback authCallback) { + + HttpRequest request = HttpRequest.post(source.accessToken()) + .form("client_id", config.getClientId()) + .form("redirect_uri", config.getRedirectUri()) + .form("code", authCallback.getCode()) + .form("grant_type", "authorization_code"); + + // 设置header + this.setHeader(request); + + HttpResponse response = request.execute(); + JSONObject object = JSONObject.parseObject(response.body()); + + this.checkResponse(object); + + return AuthToken.builder() + .openId(this.getOpenId(authCallback.getCode())) + .accessToken(object.getString("access_token")) + .refreshToken(object.getString("refresh_token")) + .tokenType(object.getString("token_type")) + .expireIn(object.getIntValue("expires_in")) + .build(); + } + + @Override + protected AuthUser getUserInfo(AuthToken authToken) { + Map parameters = new HashMap<>(); + // 获取商户账号信息的API接口名称 + String action = "eleme.user.getUser"; + // 时间戳,单位秒。API服务端允许客户端请求最大时间误差为正负5分钟。 + final long timestamp = System.currentTimeMillis(); + // 公共参数 + Map metasHashMap = new HashMap(); + metasHashMap.put("app_key", config.getClientId()); + metasHashMap.put("timestamp", timestamp); + String signature = GlobalAuthUtil.generateElemeSignature(config.getClientId(), config.getClientSecret(), timestamp, action, authToken.getAccessToken(), parameters); + + HttpRequest request = HttpRequest.post(source.userInfo()) + .form("nop", "1.0.0") + .form("id", this.getRequestId()) + .form("metas", metasHashMap) + .form("action", action) + .form("token", authToken.getAccessToken()) + .form("params", parameters) + .form("signature", signature); + + // 设置header + this.setHeader(request, "application/json; charset=utf-8"); + + HttpResponse response = request.execute(); + + JSONObject object = JSONObject.parseObject(response.body()); + + // 校验请求 + if (object.containsKey("error")) { + throw new AuthException(object.getJSONObject("error").getString("message")); + } + + JSONObject result = object.getJSONObject("result"); + + return AuthUser.builder() + .uuid(result.getString("userId")) + .username(result.getString("userName")) + .nickname(result.getString("userName")) + .gender(AuthUserGender.UNKNOWN) + .token(authToken) + .source(source) + .build(); + } + + @Override + public AuthResponse refresh(AuthToken oldToken) { + HttpRequest request = HttpRequest.post(source.refresh()) + .form("refresh_token", oldToken.getRefreshToken()) + .form("grant_type", "refresh_token"); + + // 设置header + this.setHeader(request); + + HttpResponse response = request.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")) + .refreshToken(object.getString("refresh_token")) + .tokenType(object.getString("token_type")) + .expireIn(object.getIntValue("expires_in")) + .build()) + .build(); + } + + @Override + public String authorize(String state) { + return UrlBuilder.fromBaseUrl(super.authorize(state)) + .queryParam("scope", "all") + .build(); + } + + private String getOpenId(String code) { + HttpRequest request = HttpRequest.post("https://open-api.shop.ele.me/identity") + .form("grant_type", "authorization_code") + .form("code", code) + .form("redirect_uri", config.getRedirectUri()) + .form("client_id", config.getClientId()); + + // 设置header + this.setHeader(request); + + HttpResponse response = request.execute(); + JSONObject object = JSONObject.parseObject(response.body()); + + this.checkResponse(object); + return object.getString("openId"); + } + + private String getBasic(String appKey, String appSecret) { + StringBuilder sb = new StringBuilder(); + String encodeToString = Base64.encode((appKey + ":" + appSecret).getBytes()); + sb.append("Basic").append(" ").append(encodeToString); + return sb.toString(); + } + + private void setHeader(HttpRequest request) { + setHeader(request, "application/x-www-form-urlencoded;charset=UTF-8"); + } + + private void setHeader(HttpRequest request, String contentType) { + request.header("Accept", "text/xml,text/javascript,text/html") + .header("Content-Type", contentType) + .header("Accept-Encoding", "gzip") + .header("User-Agent", "eleme-openapi-java-sdk") + .header("x-eleme-requestid", getRequestId()) + .header("Authorization", this.getBasic(config.getClientId(), config.getClientSecret())); + } + + private String getRequestId() { + return UuidUtils.getUUID() + "|" + System.currentTimeMillis(); + } + + private void checkResponse(JSONObject object) { + if (object.containsKey("error")) { + throw new AuthException(object.getString("error_description")); + } + } + +} diff --git a/src/main/java/me/zhyd/oauth/request/AuthMeituanRequest.java b/src/main/java/me/zhyd/oauth/request/AuthMeituanRequest.java index 4ba079c..7685d2f 100644 --- a/src/main/java/me/zhyd/oauth/request/AuthMeituanRequest.java +++ b/src/main/java/me/zhyd/oauth/request/AuthMeituanRequest.java @@ -74,11 +74,11 @@ public class AuthMeituanRequest extends AuthDefaultRequest { @Override public AuthResponse refresh(AuthToken oldToken) { - HttpResponse response = HttpRequest.post(source.accessToken()) + HttpResponse response = HttpRequest.post(source.refresh()) .form("app_id", config.getClientId()) .form("secret", config.getClientSecret()) .form("refresh_token", oldToken.getRefreshToken()) - .form("grant_type", "authorization_code") + .form("grant_type", "refresh_token") .execute(); JSONObject object = JSONObject.parseObject(response.body()); diff --git a/src/main/java/me/zhyd/oauth/utils/GlobalAuthUtil.java b/src/main/java/me/zhyd/oauth/utils/GlobalAuthUtil.java index e1aad50..5c248f0 100644 --- a/src/main/java/me/zhyd/oauth/utils/GlobalAuthUtil.java +++ b/src/main/java/me/zhyd/oauth/utils/GlobalAuthUtil.java @@ -4,6 +4,7 @@ import cn.hutool.core.codec.Base64; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSON; import me.zhyd.oauth.exception.AuthException; import javax.crypto.Mac; @@ -14,6 +15,7 @@ import java.net.URLEncoder; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; @@ -27,11 +29,25 @@ public class GlobalAuthUtil { private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8; private static final String ALGORITHM = "HmacSHA256"; + /** + * 生成钉钉请求的Signature + * + * @param secretKey 平台应用的授权密钥 + * @param timestamp 时间戳 + * @return Signature + */ public static String generateDingTalkSignature(String secretKey, String timestamp) { byte[] signData = sign(secretKey.getBytes(DEFAULT_ENCODING), timestamp.getBytes(DEFAULT_ENCODING)); return urlEncode(new String(Base64.encode(signData, false))); } + /** + * 签名 + * + * @param key key + * @param data data + * @return byte[] + */ private static byte[] sign(byte[] key, byte[] data) { try { Mac mac = Mac.getInstance(ALGORITHM); @@ -44,11 +60,16 @@ public class GlobalAuthUtil { } } + /** + * 编码 + * + * @param value str + * @return encode str + */ public static String urlEncode(String value) { if (value == null) { return ""; } - try { String encoded = URLEncoder.encode(value, GlobalAuthUtil.DEFAULT_ENCODING.displayName()); return encoded.replace("+", "%20").replace("*", "%2A").replace("~", "%7E").replace("/", "%2F"); @@ -57,6 +78,13 @@ public class GlobalAuthUtil { } } + + /** + * 解码 + * + * @param value str + * @return decode str + */ public static String urlDecode(String value) { if (value == null) { return ""; @@ -68,6 +96,12 @@ public class GlobalAuthUtil { } } + /** + * string字符串转map,str格式为 {@code xxx=xxx&xxx=xxx} + * + * @param accessTokenStr 待转换的字符串 + * @return map + */ public static Map parseStringToMap(String accessTokenStr) { Map res = new HashMap<>(); if (accessTokenStr.contains("&")) { @@ -82,7 +116,13 @@ public class GlobalAuthUtil { return res; } - + /** + * map转字符串,转换后的字符串格式为 {@code xxx=xxx&xxx=xxx} + * + * @param params 待转换的map + * @param encode 是否转码 + * @return str + */ public static String parseMapToString(Map params, boolean encode) { List paramList = new ArrayList<>(); params.forEach((k, v) -> { @@ -95,13 +135,25 @@ public class GlobalAuthUtil { }); return CollUtil.join(paramList, "&"); } - + + /** + * 将url的参数列表转换成map + * + * @param url 待转换的url + * @return map + */ public static Map parseQueryToMap(String url) { Map paramMap = new HashMap<>(); HttpUtil.decodeParamMap(url, "UTF-8").forEach(paramMap::put); return paramMap; } + /** + * 是否为http协议 + * + * @param url 待验证的url + * @return true: http协议, false: 非http协议 + */ public static boolean isHttpProtocol(String url) { if (StringUtils.isEmpty(url)) { return false; @@ -109,6 +161,12 @@ public class GlobalAuthUtil { return url.startsWith("http://"); } + /** + * 是否为https协议 + * + * @param url 待验证的url + * @return true: https协议, false: 非https协议 + */ public static boolean isHttpsProtocol(String url) { if (StringUtils.isEmpty(url)) { return false; @@ -116,8 +174,68 @@ public class GlobalAuthUtil { return url.startsWith("https://"); } + /** + * 是否为本地主机(域名) + * + * @param url 待验证的url + * @return true: 本地主机(域名), false: 非本地主机(域名) + */ public static boolean isLocalHost(String url) { return StringUtils.isEmpty(url) || url.contains("127.0.0.1") || url.contains("localhost"); } + /** + * 生成饿了么请求的Signature + *

+ * 代码copy并修改自:https://coding.net/u/napos_openapi/p/eleme-openapi-java-sdk/git/blob/master/src/main/java/eleme/openapi/sdk/utils/SignatureUtil.java + * + * @param appKey 平台应用的授权key + * @param secret 平台应用的授权密钥 + * @param timestamp 时间戳,单位秒。API服务端允许客户端请求最大时间误差为正负5分钟。 + * @param action 饿了么请求的api方法 + * @param token 用户授权的token + * @param parameters 加密参数 + * @return Signature + */ + public static String generateElemeSignature(String appKey, String secret, long timestamp, String action, String token, Map parameters) { + final Map sorted = new TreeMap<>(); + for (Map.Entry entry : parameters.entrySet()) { + sorted.put(entry.getKey(), entry.getValue()); + } + sorted.put("app_key", appKey); + sorted.put("timestamp", timestamp); + StringBuffer string = new StringBuffer(); + for (Map.Entry entry : sorted.entrySet()) { + string.append(entry.getKey()).append("=").append(JSON.toJSONString(entry.getValue())); + } + String splice = String.format("%s%s%s%s", action, token, string, secret); + String calculatedSignature = md5(splice); + return calculatedSignature.toUpperCase(); + } + + /** + * MD5加密饿了么请求的Signature + *

+ * 代码copy并修改自:https://coding.net/u/napos_openapi/p/eleme-openapi-java-sdk/git/blob/master/src/main/java/eleme/openapi/sdk/utils/SignatureUtil.java + * + * @param str 饿了么请求的Signature + * @return md5 str + */ + private static String md5(String str) { + MessageDigest md = null; + StringBuilder buffer = null; + try { + md = MessageDigest.getInstance("MD5"); + md.update(str.getBytes(StandardCharsets.UTF_8)); + byte[] byteData = md.digest(); + buffer = new StringBuilder(); + for (byte byteDatum : byteData) { + buffer.append(Integer.toString((byteDatum & 0xff) + 0x100, 16).substring(1)); + } + } catch (Exception ignored) { + } + + return null == buffer ? "" : buffer.toString(); + } + } -- GitLab