diff --git a/docs/update.md b/docs/update.md index e81efe0c93ad697bdeada7661e90d8c84968c06f..c823dc4d46fbbd4b069e6d8016b551bfb8f299a9 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,4 +1,9 @@ ## v1.9.6 +### 2019/08/05 + +- 集成华为登录 +- 修改`AuthChecker#checkCode`方法,对于不同平台使用不同参数接受code的情况统一做处理 + ### 2019/08/03 合并github上[xkcoding](https://github.com/xkcoding) 的[pr#32](https://github.com/zhangyd-c/JustAuth/pull/32),抽取 cache 接口,方便用户自行集成 cache diff --git a/src/main/java/me/zhyd/oauth/config/AuthSource.java b/src/main/java/me/zhyd/oauth/config/AuthSource.java index 00e0ec2114bd18f895e3bd2827ba46a6ef0f726d..dd7a4368fc4453621a63c40ffad62427d822e978 100644 --- a/src/main/java/me/zhyd/oauth/config/AuthSource.java +++ b/src/main/java/me/zhyd/oauth/config/AuthSource.java @@ -518,6 +518,32 @@ public enum AuthSource { public String userInfo() { return "https://api.stackexchange.com/2.2/me"; } + }, + + /** + * 华为 + * @since 1.9.6 + */ + HUAWEI { + @Override + public String authorize() { + return "https://oauth-login.cloud.huawei.com/oauth2/v2/authorize"; + } + + @Override + public String accessToken() { + return "https://oauth-login.cloud.huawei.com/oauth2/v2/token"; + } + + @Override + public String userInfo() { + return "https://api.vmall.com/rest.php"; + } + + @Override + public String refresh() { + return "https://oauth-login.cloud.huawei.com/oauth2/v2/token"; + } }; /** diff --git a/src/main/java/me/zhyd/oauth/model/AuthCallback.java b/src/main/java/me/zhyd/oauth/model/AuthCallback.java index 810ebea036d3ad5c16f69e1c271c697f00484679..0ea02707d625c41177274def0e2be40e1358b9b0 100644 --- a/src/main/java/me/zhyd/oauth/model/AuthCallback.java +++ b/src/main/java/me/zhyd/oauth/model/AuthCallback.java @@ -27,4 +27,11 @@ public class AuthCallback { * 访问AuthorizeUrl后回调时带的参数state,用于和请求AuthorizeUrl前的state比较,防止CSRF攻击 */ private String state; + + /** + * 华为授权登录接受code的参数名 + * + * @since 1.9.6 + */ + private String authorization_code; } diff --git a/src/main/java/me/zhyd/oauth/request/AuthDefaultRequest.java b/src/main/java/me/zhyd/oauth/request/AuthDefaultRequest.java index fc26e649d5d7481d94e1bd0a91bb2f50a6df6d76..27f1d0915e875e1caf574050acdd4ecf64819874 100644 --- a/src/main/java/me/zhyd/oauth/request/AuthDefaultRequest.java +++ b/src/main/java/me/zhyd/oauth/request/AuthDefaultRequest.java @@ -74,7 +74,7 @@ public abstract class AuthDefaultRequest implements AuthRequest { @Override public AuthResponse login(AuthCallback authCallback) { try { - AuthChecker.checkCode(source == AuthSource.ALIPAY ? authCallback.getAuth_code() : authCallback.getCode()); + AuthChecker.checkCode(source, authCallback); this.checkState(authCallback.getState()); AuthToken authToken = this.getAccessToken(authCallback); diff --git a/src/main/java/me/zhyd/oauth/request/AuthHuaweiRequest.java b/src/main/java/me/zhyd/oauth/request/AuthHuaweiRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..28f21b82aa8a127c0dc6851aa91ac0f00eefeb5c --- /dev/null +++ b/src/main/java/me/zhyd/oauth/request/AuthHuaweiRequest.java @@ -0,0 +1,194 @@ +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.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.UrlBuilder; + +import static me.zhyd.oauth.enums.AuthResponseStatus.SUCCESS; + +/** + * 华为授权登录 + * + * @author yadong.zhang (yadong.zhang0415(a)gmail.com) + * @version 1.0 + * @since 1.9.6 + */ +public class AuthHuaweiRequest extends AuthDefaultRequest { + + public AuthHuaweiRequest(AuthConfig config) { + super(config, AuthSource.HUAWEI); + } + + public AuthHuaweiRequest(AuthConfig config, AuthStateCache authStateCache) { + super(config, AuthSource.HUAWEI, authStateCache); + } + + /** + * 获取access token + * + * @param authCallback 授权成功后的回调参数 + * @return token + * @see AuthDefaultRequest#authorize() + * @see AuthDefaultRequest#authorize(String) + */ + @Override + protected AuthToken getAccessToken(AuthCallback authCallback) { + HttpRequest request = HttpRequest.post(source.accessToken()) + .form("grant_type", "authorization_code") + .form("code", authCallback.getAuthorization_code()) + .form("client_id", config.getClientId()) + .form("client_secret", config.getClientSecret()) + .form("redirect_uri", config.getRedirectUri()); + return getAuthToken(request); + } + + /** + * 使用token换取用户信息 + * + * @param authToken token信息 + * @return 用户信息 + * @see AuthDefaultRequest#getAccessToken(AuthCallback) + */ + @Override + protected AuthUser getUserInfo(AuthToken authToken) { + HttpResponse response = HttpRequest.post(source.userInfo()) + .form("nsp_ts", System.currentTimeMillis()) + .form("access_token", authToken.getAccessToken()) + .form("nsp_fmt", "JS") + .form("nsp_svc", "OpenUP.User.getInfo") + .execute(); + JSONObject object = JSONObject.parseObject(response.body()); + + this.checkResponse(object); + + AuthUserGender gender = getRealGender(object); + + return AuthUser.builder() + .uuid(object.getString("userID")) + .username(object.getString("userName")) + .nickname(object.getString("userName")) + .gender(gender) + .avatar(object.getString("headPictureURL")) + .build(); + } + + /** + * 刷新access token (续期) + * + * @param authToken 登录成功后返回的Token信息 + * @return AuthResponse + */ + @Override + public AuthResponse refresh(AuthToken authToken) { + HttpRequest request = HttpRequest.post(source.refresh()) + .form("client_id", config.getClientId()) + .form("client_secret", config.getClientSecret()) + .form("refresh_token", authToken.getRefreshToken()) + .form("grant_type", "refresh_token"); + return AuthResponse.builder() + .code(SUCCESS.getCode()) + .data(getAuthToken(request)) + .build(); + } + + private AuthToken getAuthToken(HttpRequest request) { + HttpResponse response = request.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")) + .build(); + } + + /** + * 返回带{@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("response_type", "code") + .queryParam("client_id", config.getClientId()) + .queryParam("redirect_uri", config.getRedirectUri()) + .queryParam("access_type", "offline") + .queryParam("scope", "https%3A%2F%2Fwww.huawei.com%2Fauth%2Faccount%2Fbase.profile") + .queryParam("state", getRealState(state)) + .build(); + } + + /** + * 返回获取accessToken的url + * + * @param code 授权码 + * @return 返回获取accessToken的url + */ + @Override + protected String accessTokenUrl(String code) { + return UrlBuilder.fromBaseUrl(source.accessToken()) + .queryParam("grant_type", "authorization_code") + .queryParam("code", code) + .queryParam("client_id", config.getClientId()) + .queryParam("client_secret", config.getClientSecret()) + .queryParam("redirect_uri", config.getRedirectUri()) + .build(); + } + + /** + * 返回获取userInfo的url + * + * @param authToken token + * @return 返回获取userInfo的url + */ + @Override + protected String userInfoUrl(AuthToken authToken) { + return UrlBuilder.fromBaseUrl(source.userInfo()) + .queryParam("nsp_ts", System.currentTimeMillis()) + .queryParam("access_token", authToken.getAccessToken()) + .queryParam("nsp_fmt", "JS") + .queryParam("nsp_svc", "OpenUP.User.getInfo") + .build(); + } + + /** + * 获取用户的实际性别。华为系统中,用户的性别:1表示女,0表示男 + * + * @param object obj + * @return AuthUserGender + */ + private AuthUserGender getRealGender(JSONObject object) { + int genderCodeInt = object.getIntValue("gender"); + String genderCode = genderCodeInt == 1 ? "0" : (genderCodeInt == 0) ? "1" : genderCodeInt + ""; + return AuthUserGender.getRealGender(genderCode); + } + + /** + * 校验响应结果 + * + * @param object 接口返回的结果 + */ + private void checkResponse(JSONObject object) { + if (object.containsKey("error")) { + if (!object.containsKey("sub_error") && !object.containsKey("error_description")) { + throw new AuthException(object.getString("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 770f960f0865de240cd5a5299cc42a3930472ff1..8eaee0ef7eaf2a3a9532b69a413d37f1296d2302 100644 --- a/src/main/java/me/zhyd/oauth/utils/AuthChecker.java +++ b/src/main/java/me/zhyd/oauth/utils/AuthChecker.java @@ -4,6 +4,7 @@ import me.zhyd.oauth.config.AuthConfig; import me.zhyd.oauth.config.AuthSource; import me.zhyd.oauth.enums.AuthResponseStatus; import me.zhyd.oauth.exception.AuthException; +import me.zhyd.oauth.model.AuthCallback; /** * 授权配置类的校验器 @@ -56,11 +57,20 @@ public class AuthChecker { /** * 校验回调传回的code + *

+ * {@code v1.9.6}版本中改为传入{@code source}和{@code callback},对于不同平台使用不同参数接受code的情况统一做处理 * - * @param code 回调时传回的code + * @param source 当前授权平台 + * @param callback 从第三方授权回调回来时传入的参数集合 * @since 1.8.0 */ - public static void checkCode(String code) { + public static void checkCode(AuthSource source, AuthCallback callback) { + String code = callback.getCode(); + if (source == AuthSource.ALIPAY) { + code = callback.getAuth_code(); + } else if (source == AuthSource.HUAWEI) { + code = callback.getAuthorization_code(); + } if (StringUtils.isEmpty(code)) { throw new AuthException(AuthResponseStatus.ILLEGAL_CODE); } diff --git a/src/test/java/me/zhyd/oauth/AuthRequestTest.java b/src/test/java/me/zhyd/oauth/AuthRequestTest.java index 74ffa3966edf946a3e5e4c18b138bf88ad97d11a..e5341d3edd189c9e21ef8ad65b6779e11f5b0bce 100644 --- a/src/test/java/me/zhyd/oauth/AuthRequestTest.java +++ b/src/test/java/me/zhyd/oauth/AuthRequestTest.java @@ -277,4 +277,18 @@ public class AuthRequestTest { // 注:JustAuth默认保存state的时效为3分钟,3分钟内未使用则会自动清除过期的state AuthResponse login = authRequest.login(new AuthCallback()); } + + @Test + public void huaweiTest() { + AuthRequest authRequest = new AuthHuaweiRequest(AuthConfig.builder() + .clientId("clientId") + .clientSecret("clientSecret") + .redirectUri("redirectUri") + .build()); + // 返回授权页面,可自行跳转 + authRequest.authorize("state"); + // 授权登录后会返回code(auth_code(仅限支付宝))、state,1.8.0版本后,可以用AuthCallback类作为回调接口的入参 + // 注:JustAuth默认保存state的时效为3分钟,3分钟内未使用则会自动清除过期的state + AuthResponse login = authRequest.login(new AuthCallback()); + } } diff --git a/src/test/java/me/zhyd/oauth/sdk/ThirdPartSdkTest.java b/src/test/java/me/zhyd/oauth/sdk/ThirdPartSdkTest.java new file mode 100644 index 0000000000000000000000000000000000000000..05e55e5a859709f3dce89c29014c14a92fc2080d --- /dev/null +++ b/src/test/java/me/zhyd/oauth/sdk/ThirdPartSdkTest.java @@ -0,0 +1,41 @@ +package me.zhyd.oauth.sdk; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import com.alibaba.fastjson.JSONObject; +import org.junit.Test; + +/** + * @author yadong.zhang (yadong.zhang0415(a)gmail.com) + * @version 1.0 + * @since 1.9.6 + */ +public class ThirdPartSdkTest { + + @Test + public void huawei() { + String code = "CF1IE8WDUI7HR0cTOcl59SHBmIo0EGugnY99HTnLjH0BiCu5+maSDDejA7V2FJntFGfdTXY/jD68WZAVW2cMZoXrHW0LHVQ+uYqb498PkdI453sejJcaSIS6bBCZJBNzrYKGk4PYWc5OS/yuPorSSNRlXXhjN9selraIOF+TBMb7wzXDho7FVz/Es2rInRfttnr3AEaIvkg="; + HttpResponse response = HttpRequest.post("https://oauth-login.cloud.huawei.com/oauth2/v2/token") + .form("grant_type", "authorization_code") + .form("code", code) + .form("client_id", "100994535") + .form("client_secret", "22aea400bef603fef26d15a79c806eb477b35de0a529758f2a3b1bda32bfb80d") + .form("redirect_uri", "http://localhost:8443/huawei/login") + .execute(); + System.out.println(response.body()); + + // {"access_token":"CF1IGrYGi\/s0JddMwEQ1i0xBwWI7CepxuAm9HP9wvjuPOYyXItkSh6PVLfeD7AcDD3BS0APzgyyrS\/GK9FF9Hk91WrAROGfTfTVlh0DdEw9k4NW77EjQmA==","expires_in":3600,"refresh_token":"CF41WqZNkgaJhDtW1Kv5rypr8PklmwVsKlAbFLXmxte0mrTdvJd9k8vTrIlw5NoMnNn7nZ2b3fpgsl4zabm10QQEHY2H+s5qwx1dxXR\/arV6JQ9OYMDk+A==","scope":"https:\/\/www.huawei.com\/auth\/account\/mobile.number https:\/\/www.huawei.com\/auth\/account\/base.profile","token_type":"Bearer"} + + // + HttpResponse response2 = HttpRequest.post("https://api.vmall.com/rest.php") + .form("nsp_ts", System.currentTimeMillis()) + .form("access_token", JSONObject.parseObject(response.body()).getString("access_token")) + .form("nsp_fmt", "JS") +// .form("nsp_cb", "") + .form("nsp_svc", "OpenUP.User.getInfo") + .execute(); + System.out.println(response2.body()); + // 华为性别 0是男,女是1 + // {"gender":1,"headPictureURL":"https://upfile-drcn.platform.hicloud.com/FileServer/image/b.0260086000226601572.20190415065228.iBKdTsqaNkdPXSz4N7pIRWAgeu45ec3k.1000.9A5467309F9284B267ECA33B59D3D7DA4A71BC732D3BB24EC6B880A73DEE9BAB.jpg","languageCode":"zh-CN","userID":"260086000226601572","userName":"151****2326","userState":1,"userValidStatus":1} + } +}