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();
}