三方登录简介
三方登录目前 HZERO 支持 微信、QQ 三方登录,同时支持项目上开发特定的三方登录,只需按规范开发相应的实现,然后在 oauth 服务中引入依赖即可。
1. 组件依赖
如果想使用某个组件,需自行在 oauth 服务中引入相关依赖:
-
QQ
<dependency> <groupId>org.hzero.starter</groupId> <artifactId>hzero-starter-social-qq</artifactId> <version>${hzero.starter.version}</version> </dependency>
-
微信
<dependency> <groupId>org.hzero.starter</groupId> <artifactId>hzero-starter-social-wechat</artifactId> <version>${hzero.starter.version}</version> </dependency>
2. 三方登录组件
hzero-starter-social 三方登录组件基于 spring-social、spring-security、oauth2.0 扩展开发,hzero 三方组件如下:
- hzero-starter-social-core : 三方登录核心组件,抽象了三方认证流程,及相关API封装
- hzero-starter-social-qq : 三方QQ登录
- hzero-starter-social-wechat : 三方微信登录
三方登录流程
Spring Social 三方登录流程是基于 oauth2.0 标准的授权码模式来完成的,所以 hzero-starter-social 组件只能在三方应用平台的授权方式是授权码模式才可以使用。具体的流程可以参考如下流程图。
三方应用管理
1. 申请授权信息
在使用某种三方登录方式时,首先需要到对应三方开放平台上申请三方应用的授权信息。
- QQ 开放平台 :https://connect.qq.com/index.html
- 微信 开放平台:https://open.weixin.qq.com/cgi-bin/index
- 微博 开放平台:https://open.weibo.com/
在申请三方应用授权信息时,需要填入网站回调地址,回调地址在 oauth 服务中,且回调地址必须能让外网访问,否则三方平台无法回调。
回调地址格式为:http://{domain}/oauth/open/{appCode}/callback
其中 domain 为网站网关域名,appCode 为三方应用编码。
- QQ 回调地址 :
http://domain/oauth/open/qq/callback
- 微信 回调地址 :
http://domain/oauth/open/wechat/callback
- 微博 回调地址 :
http://domain/oauth/open/sina/callback
申请成功后,将得到三方应用平台的 APP ID
以及 APP Key
,例如 QQ 开放平台申请的应用:
2. 配置三方应用
首先需要在 三方应用管理
功能下配置系统的三方应用信息,维护好之后,才可以在个人中心三方账号及oauth登录页面看到三方应用的图标。
- 应用编码:取自值集:HIAM.OPEN_APP_CODE,应用编码的值就是回调地址中的 appCode
- 登录渠道:取自值集:HIAM.CHANNEL,前端根据渠道查询对应渠道的三方应用,且在调用 /open/** 接口时传入渠道参数(channel=xx)
- APP ID:申请的三方应用的授权 APP ID
- APP Key:申请的三方应用的授权 APP Key
- 应用图片:三方应用的图标
三方登录接口
下面以 QQ 三方登录为例介绍三方授权相关的一些接口。
1. 获取三方登录方式
调用 /oauth/login/init-params?channel={channel}&client_id={clientId}
获取三方登录方式
openLoginWays: 三方登录方式
isNeedCaptcha: 是否需要输入图形验证码
2. PC端跳转三方授权平台
PC 端需跳转到三方平台让三方用户授权登录,APP 端则直接使用 SDK 拉起本地应用授权。
访问 http://domain/oauth/open/qq?channel=pc
,后台自动跳转到 QQ 授权页面
3. 用户授权回调
用户授权后,三方平台将回调 http://domain/oauth/open/qq/callback?code=XXXXX&state=xxx
,并带上授权码返回。移动端则会将授权码返回本地应用。
之后的获取 access_token、认证用户是否已绑定,都是在后端自动进行,无需特别处理。(用户如果未绑定,默认返回错误信息到登录页面,将在下个迭代中支持跳转到绑定账号页面)
4. 移动端三方认证接口
移动端在本地获取到三方平台的 access_token 和 open_id 之后,调用后端接口 /oauth/token/open
认证用户及获取 oauth access_token。
认证成功将返回 access_token,认证失败将返回对应的失败信息。
接口返回码
返回编码 | 说明 |
---|---|
hoth.social.providerUserNotFound | 未查询到您的三方用户信息 |
hoth.social.openIdNotFound | 无法获取到您的三方账号 |
hoth.social.userAlreadyBind | 您已绑定三方账户 |
hoth.social.openIdAlreadyBindOtherUser | 您的三方账户已绑定其他用户,您可以先解绑再绑定当前用户 |
hoth.social.providerNotBindUser | 三方账号未绑定系统用户 |
hoth.social.userNotFound | 系统用户不存在 |
5. PC端用户绑定三方账号
用户登录后,可在个人中心绑定三方账号。绑定账号访问 http://domain/oauth/open/qq?channel=pc&access_token=xxxxx&bind_redirect_uri=redirectUrl
channel: 登录渠道
access_token: 用户登录后的 access_token
bind_redirect_uri: 绑定成功或失败的重定向地址,绑定失败将在重定向地址后通过 `social_error_message` 参数返回。
开发三方登录
HZERO 目前已支持 微信、QQ 三方登录方式,如果项目上需要开发其它的三方登录,可按照如下步骤开发。三方应用平台相关的接口、参数、返回内容等请到对应三方开放平台查找。
1. 创建三方组件
开发三方登录时,建议新建一个项目或模块,开发完成后在 oauth 服务中依赖该组件即可。parent 依赖 hzero-starter-social-parent
,引入 hzero-starter-social-core
组件。下面以QQ开发的流程为例讲解如何基于 hzero-starter-social-core 开发三方登录。
- QQ pom:
<parent> <groupId>org.hzero.starter</groupId> <artifactId>hzero-starter-social-parent</artifactId> <version>1.0.0.RELEASE</version> </parent> <artifactId>hzero-starter-social-qq</artifactId> <dependencies> <dependency> <groupId>org.hzero.starter</groupId> <artifactId>hzero-starter-social-core</artifactId> <version>${hzero.starter.version}</version> </dependency> </dependencies>
2. 三方API封装
① 三方用户信息类:实现 org.hzero.starter.social.core.common.api.SocialUser
接口,根据三方开放平台文档,封装三方用户信息
public class QQUser implements SocialUser {
private String ret;
private String msg;
private String openId;
private String nickname;
private String figureurl;
private String gender;
// getter/setter...
}
② 三方接口类:继承 org.hzero.starter.social.core.common.api.SocialApi
接口,该接口类有一个默认的 getUser 接口方法,用于向三方平台获取用户信息。
public interface QQApi extends SocialApi {
}
③ 三方接口默认实现:继承 org.hzero.starter.social.core.common.api.AbstractSocialApi
抽象类,并实现 QQApi
三方接口。
在构造方法中,必须包含 access_token
参数,Provider 则封装了三方平台的信息,包括 APP ID、APP Key、Token 地址、用户地址等等。
在构造方法中调用三方平台获取 open_id 的接口,根据 access_token 查询 open_id。有些三方登录在返回 access_token 时会将 open_id 直接返回,这时可以不用查询 open_id;有些则需要一次接口调用。
实现 getUser 方法,调用三方平台用户信息查询接口,根据 APP ID 及 openId 查询用户信息
public class DefaultQQApi extends AbstractSocialApi implements QQApi {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultQQApi.class);
private String userInfoUrl;
private String openIdUrl;
/**
* 客户端 appId
*/
private String appId;
/**
* openId
*/
private String openId;
private static final ObjectMapper mapper = new ObjectMapper();
public DefaultQQApi(String accessToken, Provider provider) {
super(accessToken);
// APP ID
this.appId = provider.getAppId();
// 获取用户信息的地址
this.userInfoUrl = provider.getUserInfoUrl() + "?oauth_consumer_key={appId}&openid={openId}";
// 获取 open_id 的地址
this.openIdUrl = provider.getOpenIdUrl() + "?access_token={accessToken}";
// 根据 access_token 获取 open_id
this.openId = getOpenId(accessToken);
}
@Override
public QQUser getUser() {
// 获取用户信息
String result = getRestTemplate().getForObject(userInfoUrl, String.class, appId, openId);
QQUser user = null;
try {
user = mapper.readValue(result, QQUser.class);
} catch (Exception e) {
LOGGER.error("parse qq UserInfo error. result : {}", result);
}
if (user == null) {
throw new ProviderUserNotFoundException(SocialErrorCode.PROVIDER_USER_NOT_FOUND);
}
user.setOpenId(openId);
return user;
}
/**
* 获取用户 OpenId
*/
private String getOpenId(String accessToken) {
// 返回结构:callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
String openIdResult = getRestTemplate().getForObject(openIdUrl, String.class, accessToken);
if (StringUtils.isBlank(openIdResult) || openIdResult.contains("code")) {
throw new CommonSocialException(SocialErrorCode.OPEN_ID_NOT_FOUND);
}
// 解析 openId
String[] arr = StringUtils.substringBetween(openIdResult, "{", "}").replace("\"", "").split(",");
String openid = null;
for (String s : arr) {
if (s.contains("openid")) {
openid = s.split(":")[1];
}
}
return openid;
}
}
3. API 适配器
开发三方应用与本地应用用户之间的适配器,继承 org.hzero.starter.social.core.common.connect.SocialApiAdapter
抽象类,覆盖 setConnectionValues
,在方法中,首先调用 api 获取用户信息,然后向 ConnectionValues 中设置用户昵称、open_id 等。
public class QQApiAdapter extends SocialApiAdapter {
/**
* QQApi 与 Connection 做适配
* @param api QQApi
* @param values Connection
*/
@Override
public void setConnectionValues(SocialApi api, ConnectionValues values) {
// 调用三方接口获取用户信息
QQUser user = (QQUser) api.getUser();
// 设置昵称
values.setDisplayName(user.getNickname());
values.setImageUrl(user.getFigureurl());
// 设置 open_id
values.setProviderUserId(user.getOpenId());
}
}
4. 三方服务提供商
服务提供商用于提供具体的 API,需继承 org.hzero.starter.social.core.common.connect.SocialServiceProvider
抽象类,在 getSocialApi
方法中,返回三方API的具体实现类。
public class QQServiceProvider extends SocialServiceProvider {
private Provider provider;
public QQServiceProvider(Provider provider, SocialTemplate template) {
super(provider, template);
this.provider = provider;
}
@Override
public QQApi getSocialApi(String accessToken) {
// 构造服务提供商API
return new DefaultQQApi(accessToken, provider);
}
}
5. OAuth token 模板类
OAuth token 模板类用于获取三方应用 access_token,刷新 token 等等,需继承 org.hzero.starter.social.core.common.connect.SocialTemplate
抽象类,根据实际的API情况获取授权信息。
public class QQTemplate extends SocialTemplate {
private static final Logger LOGGER = LoggerFactory.getLogger(QQTemplate.class);
public QQTemplate(Provider provider) {
super(provider);
// 设置带上 client_id、client_secret
setUseParametersForClientAuthentication(true);
}
/**
* 解析 QQ 返回的令牌
*/
@Override
protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
// 返回格式:access_token=FE04********CCE2&expires_in=7776000&refresh_token=88E4***********BE14
String result = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
if (StringUtils.isBlank(result)) {
throw new RestClientException("access token endpoint returned empty result");
}
LOGGER.debug("==> get qq access_token: " + result);
String[] arr = StringUtils.split(result, "&");
String accessToken = "", expireIn = "", refreshToken = "";
for (String s : arr) {
if (s.contains("access_token")) {
accessToken = s.split("=")[1];
} else if (s.contains("expires_in")) {
expireIn = s.split("=")[1];
} else if (s.contains("refresh_token")) {
refreshToken = s.split("=")[1];
}
}
return createAccessGrant(accessToken, null, refreshToken, Long.valueOf(expireIn), null);
}
/**
* QQ 响应 ContentType=text/html;因此需要加入 text/html; 的处理器
*/
@Override
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate = super.createRestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charsets.UTF_8));
return restTemplate;
}
}
6. 连接工厂
连接工厂用于创建 Connection 连接信息,需继承 org.hzero.starter.social.core.common.connect.SocialConnectionFactory
类。
public class QQConnectionFactory extends SocialConnectionFactory {
public QQConnectionFactory(Provider provider, SocialServiceProvider serviceProvider, SocialApiAdapter apiAdapter) {
super(provider, serviceProvider, apiAdapter);
}
}
7. 连接工厂构造器
连接工厂构造器用于创建连接工厂,需实现 org.hzero.starter.social.core.common.configurer.SocialConnectionFactoryBuilder
接口
并实现三个方法,getChannel
返回登录渠道,getProviderId
返回应用编码,在 buildConnectionFactory 中构造连接工厂。
参数 provider 会自动根据 channel 和 providerId 查询并传入,相关授权地址需自行到三方开放平台获取。
@Configuration
public class QQSocialBuilder implements SocialConnectionFactoryBuilder {
@Override
public String getChannel() {
return ChannelEnum.pc.name();
}
@Override
public String getProviderId() {
return ProviderEnum.qq.name();
}
@Override
public SocialConnectionFactory buildConnectionFactory(Provider provider) {
// 获取授权码地址
final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize";
// 获取令牌地址
final String URL_GET_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token";
// 获取 openId 的地址
final String URL_GET_OPEN_ID = "https://graph.qq.com/oauth2.0/me";
// 获取用户信息的地址
final String URL_GET_USER_INFO = "https://graph.qq.com/user/get_user_info";
provider.setAuthorizeUrl(URL_AUTHORIZE);
provider.setAccessTokenUrl(URL_GET_ACCESS_TOKEN);
provider.setOpenIdUrl(URL_GET_OPEN_ID);
provider.setUserInfoUrl(URL_GET_USER_INFO);
// 创建适配器
QQApiAdapter apiAdapter = new QQApiAdapter();
// 创建三方模板
QQTemplate template = new QQTemplate(provider);
// 创建服务提供商
QQServiceProvider serviceProvider = new QQServiceProvider(provider, template);
// 创建连接工厂
return new QQConnectionFactory(provider, serviceProvider, apiAdapter);
}
}
8. 添加配置
在 resources 资源目录下,新建 META-INF
目录,添加 spring.factories
文件,并将 QQSocialBuilder 添加到自动配置。内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.hzero.starter.social.qq.config.QQSocialBuilder
9. API 测试
开发完成后,就可以打包发布,然后在 oauth 服务中引入依赖即可使用。
正常情况下,个人中心或登录页面,我们可以看到在三方应用管理配置的三方登录方式。
点击QQ图标会访问 http://domain/oauth/open/qq?channel=pc
,接着会跳转到三方应用平台,后续的流程可参考三方登录流程图。