diff --git a/src/main/java/me/zhyd/oauth/config/AuthConfig.java b/src/main/java/me/zhyd/oauth/config/AuthConfig.java index 18ef25a61bd0cc7679f4b82713c5c6a5d3d74d71..a153c7876ac4931d1914ad1818c9865bc4267462 100644 --- a/src/main/java/me/zhyd/oauth/config/AuthConfig.java +++ b/src/main/java/me/zhyd/oauth/config/AuthConfig.java @@ -110,4 +110,18 @@ public class AuthConfig { * @since 1.15.7 */ private List scopes; + + /** + * 设备ID, 设备唯一标识ID + * + * @since 1.15.8 + */ + private String deviceId; + + /** + * 喜马拉雅:客户端包名,如果client_os_type为1或2时必填。对Android客户端是包名,对IOS客户端是Bundle ID + * + * @since 1.15.8 + */ + private String packId; } diff --git a/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java b/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java index 741a7e5ecabd4b70eb280841a640f4e40e667864..2a134073b111303b3f107cd556df79a39e446616 100644 --- a/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java +++ b/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java @@ -786,6 +786,30 @@ public enum AuthDefaultSource implements AuthSource { public String refresh() { return "https://oauth.aliyun.com/v1/token"; } - } + }, + /** + * 喜马拉雅 + */ + XMLY { + @Override + public String authorize() { + return "https://api.ximalaya.com/oauth2/js/authorize"; + } + + @Override + public String accessToken() { + return "https://api.ximalaya.com/oauth2/v2/access_token"; + } + + @Override + public String userInfo() { + return "https://api.ximalaya.com/profile/user_info"; + } + + @Override + public String refresh() { + return "https://oauth.aliyun.com/v1/token"; + } + } } diff --git a/src/main/java/me/zhyd/oauth/request/AuthXmlyRequest.java b/src/main/java/me/zhyd/oauth/request/AuthXmlyRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..64c778fe9519c971b58de3da66532298002dadc6 --- /dev/null +++ b/src/main/java/me/zhyd/oauth/request/AuthXmlyRequest.java @@ -0,0 +1,122 @@ +package me.zhyd.oauth.request; + +import com.alibaba.fastjson.JSONObject; +import com.xkcoding.http.HttpUtil; +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.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.GlobalAuthUtils; +import me.zhyd.oauth.utils.UrlBuilder; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +/** + * 喜马拉雅登录 + * + * @author zwzch (zwzch4j@gmail.com) + * @since 1.15.8 + */ +public class AuthXmlyRequest extends AuthDefaultRequest { + + public AuthXmlyRequest(AuthConfig config) { + super(config, AuthDefaultSource.XMLY); + } + + public AuthXmlyRequest(AuthConfig config, AuthStateCache authStateCache) { + super(config, AuthDefaultSource.XMLY, authStateCache); + } + + /** + * 获取access token + * + * @param authCallback 授权成功后的回调参数 + * @return token + * @see AuthDefaultRequest#authorize(String) + */ + @Override + protected AuthToken getAccessToken(AuthCallback authCallback) { + Map map = new HashMap<>(6); + map.put("code", authCallback.getCode()); + map.put("client_id", config.getClientId()); + map.put("client_secret", config.getClientSecret()); + map.put("device_id", config.getDeviceId()); + map.put("grant_type", "authorization_code"); + map.put("redirect_uri", config.getRedirectUri()); + String response = HttpUtil.post(source.accessToken(), map, true); + JSONObject accessTokenObject = JSONObject.parseObject(response); + this.checkResponse(accessTokenObject); + + return AuthToken.builder() + .accessToken(accessTokenObject.getString("access_token")) + .refreshToken(accessTokenObject.getString("refresh_token")) + .expireIn(accessTokenObject.getIntValue("expires_in")) + .uid(accessTokenObject.getString("uid")) + .build(); + } + + /** + * 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state} + * + * @param state state 验证授权流程的参数,可以防止csrf + * @return 返回授权地址 + * @since 1.15.8 + */ + @Override + public String authorize(String state) { + return UrlBuilder.fromBaseUrl(source.authorize()) + .queryParam("response_type", "code") + .queryParam("client_id", config.getClientId()) + .queryParam("redirect_uri", config.getRedirectUri()) + .queryParam("state", getRealState(state)) + .queryParam("client_os_type", "3") + .queryParam("device_id", config.getDeviceId()) + .build(); + } + + /** + * 使用token换取用户信息 + * + * @param authToken token信息 + * @return 用户信息 + * @see AuthDefaultRequest#getAccessToken(AuthCallback) + */ + @Override + public AuthUser getUserInfo(AuthToken authToken) { + Map map = new TreeMap<>(); + map.put("app_key", config.getClientId()); + map.put("client_os_type", "2"); + map.put("device_id", config.getDeviceId()); + map.put("pack_id", config.getPackId()); + map.put("access_token", authToken.getAccessToken()); + map.put("sig", GlobalAuthUtils.generateXmlySignature(map, config.getClientSecret())); + String rawUserInfo = HttpUtil.get(source.userInfo(), map, false); + System.out.println(rawUserInfo); + JSONObject object = JSONObject.parseObject(rawUserInfo); + checkResponse(object); + return AuthUser.builder() + .uuid(object.getString("id")) + .nickname(object.getString("nickname")) + .avatar(object.getString("avatar_url")) + .rawUserInfo(object) + .source(source.toString()) + .token(authToken) + .gender(AuthUserGender.UNKNOWN) + .build(); + } + + /** + * 校验响应结果 + * + * @param object 接口返回的结果 + */ + private void checkResponse(JSONObject object){ + if (object.containsKey("errcode")) { + throw new AuthException(object.getIntValue("error_no"), object.getString("error_desc")); + } + } +} diff --git a/src/main/java/me/zhyd/oauth/utils/GlobalAuthUtils.java b/src/main/java/me/zhyd/oauth/utils/GlobalAuthUtils.java index 2e9e10c6d54aae2b84185a6625f1bc35d30f0e73..f6dbf80b91cd74cc534681ab72a72b3bc04ca161 100644 --- a/src/main/java/me/zhyd/oauth/utils/GlobalAuthUtils.java +++ b/src/main/java/me/zhyd/oauth/utils/GlobalAuthUtils.java @@ -220,6 +220,33 @@ public class GlobalAuthUtils { return new String(Base64Utils.encode(signature, false)); } + /** + * 喜马拉雅签名算法 + * https://open.ximalaya.com/doc/detailApi?categoryId=6&articleId=69 + * + * @param params 加密参数 + * @param clientSecret 平台应用的授权key + * @return Signature + */ + public static String generateXmlySignature(Map params, String clientSecret) { + TreeMap map = new TreeMap<>(params); + String baseStr = Base64Utils.encode(parseMapToString(map, false)); + byte[] sign = sign(clientSecret.getBytes(DEFAULT_ENCODING), baseStr.getBytes(DEFAULT_ENCODING), HMAC_SHA1); + MessageDigest md5 = null; + StringBuilder builder = null; + try { + builder = new StringBuilder(); + md5 = MessageDigest.getInstance("MD5"); + md5.update(sign); + byte[] byteData = md5.digest(); + for (byte byteDatum : byteData) { + builder.append(Integer.toString((byteDatum & 0xff) + 0x100, 16).substring(1)); + } + } catch (Exception ignored) { + } + return null == builder ? "" : builder.toString(); + } + /** * 生成饿了么请求的Signature *