三方登录组件简介
三方登录目前 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>
-
新浪微博
<dependency> <groupId>org.hzero.starter</groupId> <artifactId>hzero-starter-social-sina</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 : 三方微信登录
- hzero-starter-social-sina : 三方微博登录
三方登录流程
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,前端根据渠道查询对应渠道的三方应用
- APP ID:申请的三方应用的授权 APP ID
- APP Key:申请的三方应用的授权 APP Key
- 应用图片:三方应用的图标
三方登录接口
下面以 QQ 三方登录为例介绍三方授权相关的一些接口。
1. 获取三方登录方式
调用 /oauth/login/init-params?client_id={clientId}
获取三方登录方式
openLoginWays: 三方登录方式
isNeedCaptcha: 是否需要输入图形验证码
2. PC端跳转三方授权平台
PC 端需跳转到三方平台让三方用户授权登录,APP 端则直接使用 SDK 拉起本地应用授权。
访问 http://domain/oauth/open/qq
,后台自动跳转到 QQ 授权页面
3. 用户授权回调
用户授权后,三方平台将回调 http://domain/oauth/open/qq/callback?code=XXXXX&state=xxx
,并带上授权码返回。移动端则会将授权码返回本地应用。之后的获取 access_token、认证用户是否已绑定,都是在后端自动进行,无需特别处理。
用户如果未绑定,默认会跳转到绑定账号页面,用户输入用户名、密码即可完成账号绑定。
如果不需要跳转到绑定账号页面,可更改如下配置。设置为 false 时,如果三方用户未绑定系统用户,会跳转到登录页面,并提示未绑定系统用户。
hzero:
oauth:
social:
# 如果三方用户未绑定系统用户,是否跳转到绑定账号页面
attempt-bind: true
4. PC端用户绑定三方账号
用户登录后,可在个人中心绑定三方账号。
绑定账号访问 http://domain/oauth/open/qq?access_token=xxxxx&bind_redirect_uri=redirectUrl
access_token: 用户登录后的 access_token
bind_redirect_uri: 绑定成功或失败的重定向地址,绑定失败将在重定向地址后通过 `social_error_message` 参数返回。
移动端三方认证
由于移动端三方授权都是在移动设备上进行,oauth 服务提供 绑定三方账号、通过三方账号获取 access_token 的接口。
1.三方认证接口
正常情况下,移动端首先引导用户授权得到三方应用的 open_id
(或 union_id
),然后调用 /oauth/token/open [POST]
接口认证并获取系统的 access_token。
-
认证时,首先根据
open_access_token
校验三方用户是否已授权,如果用户未授权(调三方用户接口不能查到三方用户信息),返回异常码hoth.social.userNotAuthorized
。 -
如果用户已授权,接着根据
open_id
查询系统绑定的用户,如果未查询到,再根据union_id
查询绑定的用户,如果还是未查到,返回异常码hoth.social.providerNotBindUser
。 -
如果用户未绑定系统用户,移动端需引导用户到绑定账号页面登录系统账号,获取系统的
access_token
,接着调用绑定系统账号的接口绑定三方用户。 -
如果用户已绑定系统用户,则会认证登录用户,并返回系统 access_token,三方用户认证成功。
2. 绑定系统账号
三方用户未绑定系统账号时,移动端引导用户到绑定账号页面,首先认证系统用户获取 access_token
,接着调用 /oauth/open-bind [POST]
绑定系统用户。
-
绑定用户时,首先根据
open_access_token
校验三方用户是否已授权,如果用户未授权(调三方用户接口不能查到三方用户信息),返回异常码hoth.social.userNotAuthorized
。 -
如果用户已授权,根据
access_token
查询已登录用户信息,将登录用户与三方用户进行绑定。
接口返回码
返回编码 | 说明 |
---|---|
hoth.social.providerUserNotFound | 未查询到您的三方用户信息 |
hoth.social.openIdNotFound | 无法获取到您的三方账号 |
hoth.social.userAlreadyBind | 您已绑定三方账户 |
hoth.social.openIdAlreadyBindOtherUser | 您的三方账户已绑定其他用户,您可以先解绑再绑定当前用户 |
hoth.social.providerNotBindUser | 您的三方账号未绑定系统用户,可先到个人中心绑定 |
hoth.social.userNotFound | 系统用户不存在 |
hoth.social.userNotAuthorized | 三方用户未授权 |
开发三方登录
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;
private String unionId;
// 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 地址、用户地址等等。
实现 getUser 方法,调用三方平台获取 open_id 的接口,根据 access_token 查询 open_id。有些三方登录在返回 access_token 时会将 open_id 直接返回,这时可以不用查询 open_id;有些则需要一次接口调用。接着调用三方平台用户信息查询接口,根据 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;
/**
* openId
*/
private String unionId;
private static final ObjectMapper mapper = new ObjectMapper();
public DefaultQQApi(String accessToken, Provider provider) {
super(accessToken);
this.appId = provider.getAppId();
this.userInfoUrl = provider.getUserInfoUrl() + "?oauth_consumer_key={appId}&openid={openId}";
this.openIdUrl = provider.getOpenIdUrl();
if (isGetUnionId()) {
this.openIdUrl += "?unionid=1";
}
}
@Override
public synchronized QQUser getUser() {
if (!isAuthorized()) {
throw new CommonSocialException(SocialErrorCode.SOCIAL_USER_NOT_AUTHORIZED);
}
if (StringUtils.isBlank(openId)) {
QQMe me = getMe();
this.openId = me.getOpenId();
this.unionId = me.getUnionId();
}
String result = getRestTemplate().getForObject(userInfoUrl, String.class, this.appId, this.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 || StringUtils.isBlank(user.getNickname())) {
LOGGER.info("not found provider user, result user={}", user);
throw new ProviderUserNotFoundException(SocialErrorCode.PROVIDER_USER_NOT_FOUND);
}
user.setOpenId(openId);
user.setUnionId(unionId);
return user;
}
/**
* 获取用户 OpenId
*/
private QQMe getMe() {
// 返回结构:callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID","unionid":"YOUR_UNIONID"} );
String openIdResult = getRestTemplate().getForObject(openIdUrl, String.class);
if (StringUtils.isBlank(openIdResult) || openIdResult.contains("error")) {
LOGGER.warn("request social user's openId return error, result={}", openIdResult);
throw new CommonSocialException(SocialErrorCode.OPEN_ID_NOT_FOUND);
}
// 解析 openId
String[] arr = StringUtils.substringBetween(openIdResult, "{", "}").replace("\"", "").split(",");
String openid = null;
String unionid = null;
for (String s : arr) {
if (s.contains("openid")) {
openid = s.split(":")[1];
} else if (s.contains("unionid")) {
unionid = s.split(":")[1];
}
}
return new QQMe(openid, unionid);
}
private boolean isGetUnionId() {
SocialProperties socialProperties = ApplicationContextHelper.getContext().getBean(SocialProperties.class);
return socialProperties.getQq().isGetUnionId();
}
private class QQMe {
private String openId;
private String unionId;
public QQMe(String openId, String unionId) {
this.openId = openId;
this.unionId = unionId;
}
public String getOpenId() {
return openId;
}
public String getUnionId() {
return unionId;
}
}
}
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
接口
并实现两个方法:
getProviderId
返回应用编码,对应三方应用管理配置的应用编码;buildConnectionFactory
构造连接工厂,参数 provider 会自动根据 providerId 查询并传入,相关授权地址需自行到三方开放平台获取。
@Configuration
public class QQSocialBuilder implements SocialConnectionFactoryBuilder {
@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
,接着会跳转到三方应用平台,后续的流程可参考三方登录流程图。