oauth第三方登录scribejava库封装qq及微信api

2,205 阅读6分钟

ScribeJava 是一个简单的 Java 实现的 OAuth/OAuth2 库,如果要支持qq及微信第三方登录需要自己写封装协议的代码。然而介绍如何自己封装api的文章非常少。因此打算写一些基础东西,第一次写长博客,如有不足欢迎指正。我会在在文中分享关于scribejava中qq与微信的封装类,懒人的话可以略过封装代码的说明,复制到项目中去(根据scribejava-apis版本6.0封装,其他版本可能会由不兼容情况)。

1.流程简介

各种第三方对接oauth协议的步骤基本相同,详细介绍及流程可参考知乎 OAuth 2 详解,写得简单易懂。

文章中提到的OAuth 定义了四种角色

  • 资源拥有者 (Resource Owner)
  • 客户端 (Client)
  • 资源服务器 (Resource Server)
  • 授权服务器 (Authorization Server)

我在开发中按常用的Grant Type为授权码(Authorization Code)的方式作为协议进行开发。另外一种隐式(Grant Type: Implicit)方式不进行讨论。

再盗一下作者的授权码的流程图。

授权流程

上面提到的客户端 (Client),可以理解为我们所要来第三方登录的项目后台。Client通过处理与其他3个角色的关系完成最终的授权。

在封装scribejava的api时,面对来自不同第三方登录提供者(qq/微信),我们所要考虑的实际就是这5步流程中1,2,4步所要发送的url如何拼接处理的问题。

2.scribeJava引入与说明

maven项目pom文件中引入最新版scribejava。

<!--第三方oauth2登录-->
<dependency>
    <groupId>com.github.scribejava</groupId>
    <artifactId>scribejava-apis</artifactId>
    <version>6.0.0</version>
</dependency>

scribejava-apis包含一些作者对第三方oauth授权提供方的封装,可惜没有对qq与微信进行封装。如果想用纯净一点的scribejava,那pom文件可以设置如下:

<dependency>
    <groupId>com.github.scribejava</groupId>
    <artifactId>scribejava-core</artifactId>
    <version>6.0.0</version>
</dependency>

3.封装的代码

废话不多说,直接先放我qq封装的代码

public class QQApi20 extends DefaultApi20{

    protected QQApi20(){
    }

    public static QQApi20 instance(){return QQApi20.InstanceHolder.INSTANCE;}


    @Override
    public String getAccessTokenEndpoint() {
        return "https://graph.qq.com/oauth2.0/token";
    }

    @Override
    protected String getAuthorizationBaseUrl() {
        return "https://graph.qq.com/oauth2.0/authorize";
    }

    @Override
    public TokenExtractor<OAuth2AccessToken> getAccessTokenExtractor() {
        return OAuth2AccessTokenExtractor.instance();
    }

    /**
     * 添加appId跟appKey采用在http的请求body中添加
     * @return
     */
    @Override
    public ClientAuthentication getClientAuthentication() {
        return RequestBodyAuthenticationScheme.instance();
    }

    /**
     * 授权的token在http请求的body中传递
     * @return
     */
    @Override
    public BearerSignature getBearerSignature() {
        return BearerSignatureURIQueryParameter.instance();
    }


    private static class InstanceHolder {

        private static final QQApi20 INSTANCE = new QQApi20();
        private InstanceHolder() {
        }

    }
}

再就是wechat的封装代码:

public class WechatApi20 extends DefaultApi20 {

    protected WechatApi20(){}

    public static WechatApi20 instance(){return WechatApi20.InstanceHolder.INSTANCE;}

    @Override
    public String getAccessTokenEndpoint() {
        return "https://api.weixin.qq.com/sns/oauth2/access_token";
    }

    @Override
    public String getAuthorizationUrl(String responseType, String apiKey, String callback, String scope, String state,
                                      Map<String, String> additionalParams) {
        final ParameterList parameters = new ParameterList(additionalParams);
        parameters.add(OAuthConstants.RESPONSE_TYPE, "code");
        parameters.add("appid", apiKey);

        if (callback != null) {
            parameters.add(OAuthConstants.REDIRECT_URI, callback);
        }

        if (scope != null) {
            parameters.add(OAuthConstants.SCOPE, scope);
        }

        if (state != null) {
            parameters.add(OAuthConstants.STATE, state);
        }

        return parameters.appendTo("https://open.weixin.qq.com/connect/qrconnect");
    }

    @Override
    protected String getAuthorizationBaseUrl() {
        throw new UnsupportedOperationException("use getAuthorizationUrl instead");
    }

    @Override
    public TokenExtractor<OAuth2AccessToken> getAccessTokenExtractor() {
        return OAuth2AccessTokenJsonExtractor.instance();
    }

    /**
     * 添加appId跟appKey采用在http的请求body中添加
     * @return
     */
    @Override
    public ClientAuthentication getClientAuthentication() {
        return RequestBodyAuthenticationScheme.instance();
    }

    /**
     * 授权的token在http请求的body中传递
     * @return
     */
    @Override
    public BearerSignature getBearerSignature() {
        return BearerSignatureURIQueryParameter.instance();
    }

    @Override
    public WechatOAuthService createService(String apiKey, String apiSecret, String callback, String scope,
                                            OutputStream debugStream, String state, String responseType, String userAgent,
                                            HttpClientConfig httpClientConfig, HttpClient httpClient){
        return new WechatOAuthService(this, apiKey, apiSecret, callback, scope, state, responseType, userAgent,
                httpClientConfig, httpClient);
    }

    private static class InstanceHolder {

        private static final WechatApi20 INSTANCE = new WechatApi20();
        private InstanceHolder() {
        }

    }
}

介绍一下我是如何进行封装的。准备前先进入qq开放平台,了解qq互联接口的形式 (qq互联链接)。

这里自己封装时如果继承DefaultApi20类,我们的封装类会按oauth2常用的协议规则进行拼接操作。

  • 授权url封装处理

我们平时点击qq登录会跳转到的qq授权登录页面

我们构造跳转这个url到第三方服务上需要路径首先要重写方法getAuthorizationBaseUrl;其次一个完整的授权url请求会带一些参数,有可能要重写getAuthorizationUrl方法。

qq完整链接类似下面:

 https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=[YOUR_APPID]&redirect_uri=[YOUR_REDIRECT_URI]&scope=[THE_SCOPE]

微信的完整链接类似下面:

https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

参数上qq的client_id与微信appid其实是同一个东西,只是人家用不同的单词表示了😠。父类DefaultApi20类中的处理过程正好跟qq的oauth2协议基本相同,所以对于qq只重写getAuthorizationBaseUrl这个方法即可。 而微信的oauth2协议跟DefaultApi20有一些不同,这就要重写一下getAuthorizationUrl方法了。

重写完这两个方法后,还要重写getClientAuthentication(),如果不写,其实当我们调用getAuthorizationUrl()会发现获取的url缺少参数client_id,是在header头中发现传入了client_id与client_secret的base64编码信息。这样不是我们想要的接入方式。所以重写getClientAuthentication(),让clien_id在请求url的参数中带出来。

    @Override
    public ClientAuthentication getClientAuthentication() {
        return RequestBodyAuthenticationScheme.instance();
    }
  • 第三方应用请求服务端,获取 access token

用户通过授权页面获取到code值,这个值在后台controller捕获处理到后就要再请求第三方服务获取accessToken,这一步父类DefaultApi20默认交给OAuth20Service处理了,获取到的accessToken会由重写的getAccessToken方法返回。而微信在这一步又在搞事,用默认的OAuth20Service无法获取到accessToken值,因为微信的appid跟secret是必填项,所以我又新建了一个WechatOAuthService类把出现的差别处理一下。

public class WechatOAuthService extends OAuth20Service{

    public WechatOAuthService(DefaultApi20 api, String apiKey, String apiSecret, String callback, String scope,
                              String state, String responseType, String userAgent, HttpClientConfig httpClientConfig,
                              HttpClient httpClient) {
        super(api, apiKey, apiSecret, callback, scope, state, responseType, userAgent, httpClientConfig, httpClient);
    }

    @Override
    protected OAuthRequest createAccessTokenRequest(String oauthVerifier) {
        final DefaultApi20 api = getApi();
        final OAuthRequest request = new OAuthRequest(api.getAccessTokenVerb(), api.getAccessTokenEndpoint());
        request.addBodyParameter("appid", getApiKey());
        request.addBodyParameter("secret", getApiSecret());
        request.addBodyParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE);
        request.addBodyParameter(OAuthConstants.CODE, oauthVerifier);
        return request;
    }

}
  • 解析返回accessToken

    注意微信跟qq返回的accessToken形式也是不一样的,qq返回的形式如access_token=YOUR_ACCESS_TOKEN&expires_in=3600,而微信的则是形式如 { "access_token":"ACCESS_TOKEN", "expires_in":7200, "refresh_token":"REFRESH_TOKEN","openid":"OPENID", "scope":"SCOPE" } 的json,所以还要重写getAccessTokenExtractor()方法。

qq的accessToken解析直接解字符串:

    @Override
    public TokenExtractor<OAuth2AccessToken> getAccessTokenExtractor() {
        return OAuth2AccessTokenExtractor.instance();
    }

微信的accessToken解析的是json:

    @Override
    public TokenExtractor<OAuth2AccessToken> getAccessTokenExtractor() {
        return OAuth2AccessTokenJsonExtractor.instance();
    }

这样我们的封装api差不多完成了,刷新续期accessToken这种就不考虑了,反正我的项目中没用到这块。

运行调用的例子参考github提供的链接(github.com/scribejava/…)自行调整。

4.利用accessToken访问用户信息

方法返回service.getAccessToken(code)一个OAuth2AccessToken对象,现在我们可以操作这个对象来获取用户个人信息进行操作了,调用类似下面的代码:

        //PROTECTED_RESOURCE_URL个人信息api
        final OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL);
        service.signRequest(accessToken, request);
        final Response response = service.execute(request);
        System.out.println(response.getBody());

如果有细心看我封装代码的朋友,会发现我又重写了getBearerSignature。简单说明一作用:

  • getBearerSignature():在获取用户信息时,自然要附带上面获取的accessToken信息和其他判断标识信息,那么附带的形式包括哪些呢?查看DefaultApi20的源码getBearerSignature(),暂时知道的有 通过head带上标识信息和通过url参数带上标识信息。而qq与微信都是后者,所以我们重写了getBearerSignature()方法。
    @Override
    public BearerSignature getBearerSignature() {
        return BearerSignatureURIQueryParameter.instance();
    }