diff --git a/README.md b/README.md index 7359b47da..3690cb49d 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ ScribeJava support out-of-box several HTTP clients: * XING (https://www.xing.com/) * Yahoo (https://www.yahoo.com/) * Misfit (http://misfit.com/) +* WeChat (http://www.wechat.com/) * check the [examples folder](https://github.com/scribejava/scribejava/tree/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples) ### Small and modular diff --git a/scribejava-apis/src/main/java/com/github/scribejava/apis/WechatApi.java b/scribejava-apis/src/main/java/com/github/scribejava/apis/WechatApi.java new file mode 100644 index 000000000..551d59f77 --- /dev/null +++ b/scribejava-apis/src/main/java/com/github/scribejava/apis/WechatApi.java @@ -0,0 +1,101 @@ +package com.github.scribejava.apis; + +import com.github.scribejava.apis.service.WechatService; +import com.github.scribejava.apis.wechat.WechatOAuth2AccessTokenJsonExtractor; +import com.github.scribejava.apis.wechat.WechatOAuth2Constants; +import com.github.scribejava.core.builder.api.DefaultApi20; +import com.github.scribejava.core.builder.api.OAuth2SignatureType; +import com.github.scribejava.core.model.OAuthConfig; +import com.github.scribejava.core.model.ParameterList; +import com.github.scribejava.core.model.Verb; +import com.github.scribejava.core.oauth.OAuth20Service; + +import java.util.Arrays; +import java.util.Map; + +public class WechatApi extends DefaultApi20 { + + protected WechatApi() { + } + + private static class InstanceHolder { + private static final WechatApi INSTANCE = new WechatApi(); + } + + public static WechatApi instance() { + return InstanceHolder.INSTANCE; + } + + @Override + public String getAccessTokenEndpoint() { + return WechatOAuth2Constants.ACCESS_TOKEN_ENDPOINT_URL; + } + + @Override + public String getRefreshTokenEndpoint() { + return WechatOAuth2Constants.REFRESH_TOKEN_ENDPOINT_URL; + } + + @Override + public WechatOAuth2AccessTokenJsonExtractor getAccessTokenExtractor() { + return WechatOAuth2AccessTokenJsonExtractor.instance(); + } + + @Override + public Verb getAccessTokenVerb() { + return Verb.GET; + } + + /** + * @see WeChat + * The authorization provided by WeChat does not fully reference the oauth2.0. + * For example, the parameter name is not the same, and the parameter order requirements. + * so I have to override this method to meet the requirements of it. + */ + @Override + public String getAuthorizationUrl(OAuthConfig config, Map additionalParams) { + final ParameterList parameters = new ParameterList(additionalParams); + + parameters.add(WechatOAuth2Constants.CLIENT_ID, config.getApiKey()); + + final String callback = config.getCallback(); + if (callback == null) { + throw new IllegalArgumentException("Missing required parameter 'redirect_uri'."); + } + parameters.add(WechatOAuth2Constants.REDIRECT_URI, callback); + + final String responseType = config.getResponseType(); + if (!WechatOAuth2Constants.CODE_RESPONSE_TYPE.equals(responseType)) { + throw new IllegalArgumentException("Parameter response_type must be 'code'."); + } + parameters.add(WechatOAuth2Constants.RESPONSE_TYPE, config.getResponseType()); + + final String scope = config.getScope(); + if (scope == null || Arrays.binarySearch(WechatOAuth2Constants.SCOPE_VALUES, scope) < 0) { + throw new IllegalArgumentException("Parameter scope can only be 'snsapi_base' or 'snsapi_userinfo'."); + } + parameters.add(WechatOAuth2Constants.SCOPE, scope); + + final String state = config.getState(); + if (state != null) { + parameters.add(WechatOAuth2Constants.STATE, state); + } + + return parameters.appendTo(getAuthorizationBaseUrl()).concat("#wechat_redirect"); + } + + @Override + public OAuth20Service createService(OAuthConfig config) { + return new WechatService(this, config); + } + + @Override + public OAuth2SignatureType getSignatureType() { + return OAuth2SignatureType.BEARER_URI_QUERY_PARAMETER; + } + + @Override + protected String getAuthorizationBaseUrl() { + return WechatOAuth2Constants.AUTHORIZE_URL; + } +} diff --git a/scribejava-apis/src/main/java/com/github/scribejava/apis/service/WechatService.java b/scribejava-apis/src/main/java/com/github/scribejava/apis/service/WechatService.java new file mode 100644 index 000000000..ac9389180 --- /dev/null +++ b/scribejava-apis/src/main/java/com/github/scribejava/apis/service/WechatService.java @@ -0,0 +1,66 @@ +package com.github.scribejava.apis.service; + +import com.github.scribejava.apis.wechat.WechatOAuth2Constants; +import com.github.scribejava.core.builder.api.DefaultApi20; +import com.github.scribejava.core.model.OAuthConfig; +import com.github.scribejava.core.model.OAuthConstants; +import com.github.scribejava.core.model.OAuthRequest; +import com.github.scribejava.core.oauth.OAuth20Service; + +import java.util.Map; + + +public class WechatService extends OAuth20Service { + + public WechatService(DefaultApi20 api, OAuthConfig config) { + super(api, config); + } + + /** + * The reason to override this method is the same as + * {@link com.github.scribejava.apis.WechatApi#getAuthorizationUrl(OAuthConfig, Map)} + */ + @Override + protected OAuthRequest createAccessTokenRequest(String code) { + final OAuthRequest request = new OAuthRequest(getApi().getAccessTokenVerb(), getApi().getAccessTokenEndpoint()); + final OAuthConfig config = getConfig(); + + request.addParameter(WechatOAuth2Constants.CLIENT_ID, config.getApiKey()); + request.addParameter(WechatOAuth2Constants.CLIENT_SECRET, config.getApiSecret()); + + request.addParameter(WechatOAuth2Constants.CODE, code); + request.addParameter(WechatOAuth2Constants.GRANT_TYPE, WechatOAuth2Constants.AUTHORIZATION_CODE); + + return request; + } + + /** + * The reason to override this method is the same as + * {@link com.github.scribejava.apis.WechatApi#getAuthorizationUrl(OAuthConfig, Map)} + */ + @Override + protected OAuthRequest createRefreshTokenRequest(String refreshToken) { + if (refreshToken == null || refreshToken.isEmpty()) { + throw new IllegalArgumentException("The refreshToken cannot be null or empty"); + } + final OAuthRequest request = new OAuthRequest(getApi().getAccessTokenVerb(), + getApi().getRefreshTokenEndpoint()); + + request.addParameter(WechatOAuth2Constants.CLIENT_ID, getConfig().getApiKey()); + + request.addParameter(OAuthConstants.GRANT_TYPE, WechatOAuth2Constants.REFRESH_TOKEN); + request.addParameter(WechatOAuth2Constants.REFRESH_TOKEN, refreshToken); + + return request; + } + + @Override + protected OAuthRequest createAccessTokenPasswordGrantRequest(String username, String password) { + throw new UnsupportedOperationException("WeChat doesn't support Resource Owner Password Credentials Grant"); + } + + @Override + protected OAuthRequest createAccessTokenClientCredentialsGrantRequest() { + throw new UnsupportedOperationException("WeChat doesn't support Client Credentials Grant"); + } +} diff --git a/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessToken.java b/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessToken.java new file mode 100644 index 000000000..750c4759c --- /dev/null +++ b/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessToken.java @@ -0,0 +1,43 @@ +package com.github.scribejava.apis.wechat; + +import com.github.scribejava.core.model.OAuth2AccessToken; + +public class WechatOAuth2AccessToken extends OAuth2AccessToken { + + private final String openid; + + public WechatOAuth2AccessToken(String accessToken, Integer expiresIn, String refreshToken, String scope, + String openid, String response) { + super(accessToken, null, expiresIn, refreshToken, scope, response); + this.openid = openid; + } + + public String getOpenid() { + return openid; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof WechatOAuth2AccessToken)) { + return false; + } + if (!super.equals(o)) { + return false; + } + + final WechatOAuth2AccessToken that = (WechatOAuth2AccessToken) o; + + return openid.equals(that.openid); + + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + openid.hashCode(); + return result; + } +} diff --git a/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessTokenErrorResponse.java b/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessTokenErrorResponse.java new file mode 100644 index 000000000..244709f30 --- /dev/null +++ b/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessTokenErrorResponse.java @@ -0,0 +1,64 @@ +package com.github.scribejava.apis.wechat; + +import com.github.scribejava.core.exceptions.OAuthException; + +public class WechatOAuth2AccessTokenErrorResponse extends OAuthException { + + private String errcode; + + private String errmsg; + + private String rawResponse; + + public WechatOAuth2AccessTokenErrorResponse(String message) { + super(message); + } + + public WechatOAuth2AccessTokenErrorResponse(String message, String errcode, String errmsg, String rawResponse) { + super(message); + this.errcode = errcode; + this.errmsg = errmsg; + this.rawResponse = rawResponse; + } + + public String getErrcode() { + return errcode; + } + + public String getErrmsg() { + return errmsg; + } + + public String getRawResponse() { + return rawResponse; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final WechatOAuth2AccessTokenErrorResponse that = (WechatOAuth2AccessTokenErrorResponse) o; + + if (errcode != null ? !errcode.equals(that.errcode) : that.errcode != null) { + return false; + } + if (errmsg != null ? !errmsg.equals(that.errmsg) : that.errmsg != null) { + return false; + } + return rawResponse != null ? rawResponse.equals(that.rawResponse) : that.rawResponse == null; + + } + + @Override + public int hashCode() { + int result = errcode != null ? errcode.hashCode() : 0; + result = 31 * result + (errmsg != null ? errmsg.hashCode() : 0); + result = 31 * result + (rawResponse != null ? rawResponse.hashCode() : 0); + return result; + } +} diff --git a/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessTokenJsonExtractor.java b/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessTokenJsonExtractor.java new file mode 100644 index 000000000..05e9419b4 --- /dev/null +++ b/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2AccessTokenJsonExtractor.java @@ -0,0 +1,63 @@ +package com.github.scribejava.apis.wechat; + +import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.Response; +import com.github.scribejava.core.utils.Preconditions; + +import java.io.IOException; +import java.util.regex.Pattern; + +public class WechatOAuth2AccessTokenJsonExtractor extends OAuth2AccessTokenJsonExtractor { + + private static final Pattern ERRCODE_REGEX_PATTERN = Pattern.compile("\"errcode\"\\s*:\\s*([0-9]+)"); + private static final Pattern ERRMSG_REGEX_PATTERN = Pattern.compile("\"errmsg\"\\s*:\\s*\"([\\S\\s]*?)\""); + private static final Pattern OPENID_REGEX_PATTERN = Pattern.compile("\"openid\"\\s*:\\s*\"(\\S*?)\""); + + protected WechatOAuth2AccessTokenJsonExtractor() { + } + + private static class InstanceHolder { + private static final WechatOAuth2AccessTokenJsonExtractor INSTANCE = new WechatOAuth2AccessTokenJsonExtractor(); + } + + public static WechatOAuth2AccessTokenJsonExtractor instance() { + return InstanceHolder.INSTANCE; + } + + @Override + public OAuth2AccessToken extract(Response response) throws IOException { + checkError(response.getBody()); + return super.extract(response); + } + + /** + * Try to extract the error information in response. If 'errcode' or 'errmsg' is found, + * {@link WechatOAuth2AccessTokenErrorResponse} will be thrown, otherwise execute super method. + */ + private void checkError(final String response) { + if (!Preconditions.hasText(response)) { + return; + } + final String errcode = extractParameter(response, ERRCODE_REGEX_PATTERN, false); + final String errmsg = extractParameter(response, ERRMSG_REGEX_PATTERN, false); + + if (errcode != null || errmsg != null) { + throw new WechatOAuth2AccessTokenErrorResponse("Obtaining WeChat OAuth2 access_token failed.", + errcode, errmsg, response); + } + } + + @Override + public void generateError(String response) { + throw new WechatOAuth2AccessTokenErrorResponse("An unknown failure occurred" + + " while obtaining the WeChat OAuth2 access_token."); + } + + @Override + protected WechatOAuth2AccessToken createToken(String accessToken, String tokenType, Integer expiresIn, + String refreshToken, String scope, String response) { + final String openid = extractParameter(response, OPENID_REGEX_PATTERN, true); + return new WechatOAuth2AccessToken(accessToken, expiresIn, refreshToken, scope, openid, response); + } +} diff --git a/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2Constants.java b/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2Constants.java new file mode 100644 index 000000000..b843d47c1 --- /dev/null +++ b/scribejava-apis/src/main/java/com/github/scribejava/apis/wechat/WechatOAuth2Constants.java @@ -0,0 +1,20 @@ +package com.github.scribejava.apis.wechat; + +import com.github.scribejava.core.model.OAuthConstants; + +public interface WechatOAuth2Constants extends OAuthConstants { + + String ACCESS_TOKEN_ENDPOINT_URL = "https://api.weixin.qq.com/sns/oauth2/access_token"; + + String REFRESH_TOKEN_ENDPOINT_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token"; + + String AUTHORIZE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize"; + + String CODE_RESPONSE_TYPE = "code"; + + String[] SCOPE_VALUES = new String[]{"snsapi_base", "snsapi_userinfo "}; + + String CLIENT_ID = "appid"; + + String CLIENT_SECRET = "secret"; +} diff --git a/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/WechatExample.java b/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/WechatExample.java new file mode 100644 index 000000000..c6b5303fe --- /dev/null +++ b/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/WechatExample.java @@ -0,0 +1,86 @@ +package com.github.scribejava.apis.examples; + +import com.github.scribejava.apis.WechatApi; +import com.github.scribejava.apis.wechat.WechatOAuth2AccessToken; +import com.github.scribejava.core.builder.ServiceBuilder; +import com.github.scribejava.core.model.OAuthRequest; +import com.github.scribejava.core.model.Response; +import com.github.scribejava.core.model.Verb; +import com.github.scribejava.core.oauth.OAuth20Service; + +import java.io.IOException; +import java.util.Random; +import java.util.Scanner; +import java.util.concurrent.ExecutionException; + +public class WechatExample { + + private static final String NETWORK_NAME = "WeChat"; + private static final String PROTECTED_RESOURCE_URL = "https://api.weixin.qq.com/sns/userinfo"; + + private WechatExample() { + } + + public static void main(String... args) throws IOException, InterruptedException, ExecutionException { + // Replace these with your client id and secret + final String clientId = "your client id"; + final String clientSecret = "your client secret"; + final String secretState = "secret" + new Random().nextInt(999_999); + final OAuth20Service service = new ServiceBuilder(clientId) + .apiSecret(clientSecret) + .scope("snsapi_base") + .state(secretState) + .callback("http://www.example.com/oauth_callback") + .build(WechatApi.instance()); + + final Scanner in = new Scanner(System.in, "UTF-8"); + + System.out.println("=== " + NETWORK_NAME + "'s OAuth2 Workflow ==="); + System.out.println(); + + // Obtain the Authorization URL + System.out.println("Fetching the Authorization URL..."); + final String authorizationUrl = service.getAuthorizationUrl(); + System.out.println("Got the Authorization URL!"); + System.out.println("Now go and authorize ScribeJava here:"); + System.out.println(authorizationUrl); + System.out.println("And paste the authorization code here"); + System.out.print(">>"); + final String code = in.nextLine(); + System.out.println(); + + System.out.println("And paste the state from server here. We have set 'secretState'='" + secretState + "'."); + System.out.print(">>"); + final String value = in.nextLine(); + if (secretState.equals(value)) { + System.out.println("State value does match!"); + } else { + System.out.println("Ooops, state value does not match!"); + System.out.println("Expected = " + secretState); + System.out.println("Got = " + value); + System.out.println(); + } + + // Trade the Request Token and Verfier for the Access Token + System.out.println("Trading the Request Token for an Access Token..."); + final WechatOAuth2AccessToken accessToken = (WechatOAuth2AccessToken) service.getAccessToken(code); + System.out.println("Got the Access Token!"); + System.out.println("(The raw response looks like this: " + accessToken.getRawResponse() + "')"); + System.out.println(); + + // Now let's go and ask for a protected resource! + System.out.println("Now we're going to access a protected resource..."); + final OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL); + request.addParameter("openid", accessToken.getOpenid()); + service.signRequest(accessToken, request); + final Response response = service.execute(request); + System.out.println("Got it! Lets see what we found..."); + System.out.println(); + System.out.println(response.getCode()); + System.out.println(response.getBody()); + + System.out.println(); + System.out.println("Thats it man! Go and build something awesome with ScribeJava! :)"); + + } +}