【外部服务对接】对接Firebase支持谷歌、Facebook、苹果等第三方平台登录
背景
因主要做国外尼日的市场,相关的应用的全是国外用户使用,为了方便用户的注册和登录,接入国外的统一平台Firebase,集成使用很方便。
主要步骤
1.注册登录Firebase 平台
1.1 注册google账号进入firebase后台,创建项目
1.2 在项目下创建应用,根据实际情况可选,安卓、ios、web类型。
1.3 生成自己项目的私钥文件,下载保存到java应用中
2.引入Maven jar 包依赖
<dependency>
<groupId>com.google.firebase</groupId>
<artifactId>firebase-admin</artifactId>
<version>9.1.1</version>
</dependency>
3.第三方注册登录授权表
CREATE TABLE `user_login_authorize` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_id` varchar(32) NOT NULL COMMENT '用户id',
`email` varchar(255) DEFAULT NULL COMMENT '邮箱',
`identity_type` varchar(20) NOT NULL COMMENT '登录类型: google/facebook',
`identifier` varchar(255) NOT NULL COMMENT '手机号/邮箱/第三方唯一标识',
`credential` longtext COMMENT '第三方的token',
`create_datetime` datetime DEFAULT NULL COMMENT '创建时间',
`update_datetime` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_identifier` (`identifier`) USING BTREE COMMENT '唯一键'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户授权信息表';
4.Firebase注册登录的核心业务实现
4.1 FireBaseService
package com.ogc.standard.firebase;
import com.alibaba.fastjson.JSONObject;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.common.collect.Lists;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseAuthException;
import com.google.firebase.auth.FirebaseToken;
import com.google.firebase.auth.UserRecord;
import com.google.firebase.messaging.*;
import com.ogc.standard.enums.EEnv;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static com.ogc.standard.common.PropertiesUtil.Config.ENV;
@Slf4j
@Service
public class FireBaseService {
/**
* 如果设备处于离线状态,消息应在 FCM 存储中保留多长时间(以秒为单位)。
* 支持的最长存活时间为 4 周,如果不设置则默认值为 4 周。如果要立即发送消息,请将其设置为 0。
* 在 JSON 格式中,Duration 类型被编码为字符串而不是对象,其中字符串以后缀“s”(表示秒)结尾,前面是秒数,纳秒表示为小数秒。
* 例如3秒0纳秒,JSON格式编码为“3s”,3秒1纳秒JSON格式编码为“3.000000001s”。 ttl 将向下舍入到最接近的秒数。
* 以秒为单位的持续时间,最多九个小数位,以 ' s ' 结尾。示例: "3.5s" 。
*/
@Value("${firebase.messaging.android.ttl:}")
private Long androidTtl;
/**
* 消息优先级。可以取“正常”和“高”值
*/
@Value("${firebase.messaging.android.priority:}")
private String androidPriority;
/**
* 默认为10,立刻发送推送消息
*/
@Value("${firebase.messaging.apns.priority:}")
private String apnsPriority;
/**
* 发送次数
*/
@Value("${firebase.messaging.apns.aps.badge:}")
private Integer apnsApsBadge;
private static FirebaseApp firebaseApplication;
static {
//firebase初始化
ClassPathResource classPathResource = new ClassPathResource("tbay-5c74c-firebase-adminsdk-1kj38-64818b9446.json");
InputStream inputStream = null;
FirebaseOptions options = null;
try {
inputStream = classPathResource.getInputStream();
options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(inputStream))
.build();
} catch (IOException e) {
log.error("firebase初始化初始化失败,原因:{}", e.getMessage(), e);
}
firebaseApplication = FirebaseApp.initializeApp(options);
}
public Boolean verifyTokenId(String idToken, String email) {
if (ENV.equals(EEnv.DEV.getCode())) {
System.setProperty("https.proxyHost", "10.0.84.188");
System.setProperty("https.proxyPort", "7890");
System.setProperty("com.google.api.client.should_use_proxy", "true");
}
FirebaseToken decodedToken = null;
try {
decodedToken = FirebaseAuth.getInstance().verifyIdToken(idToken);
} catch (FirebaseAuthException e) {
log.error("firebase id token 解析失败 {}", e.getMessage(), e);
}
return decodedToken != null && decodedToken.getEmail().equalsIgnoreCase(email);
}
public Boolean verifyTokenIdAndUid(String idToken, String uid) {
if (ENV.equals(EEnv.DEV.getCode())) {
return true;
}
FirebaseToken decodedToken = null;
try {
decodedToken = FirebaseAuth.getInstance().verifyIdToken(idToken);
} catch (FirebaseAuthException e) {
log.error("firebase id token 解析失败 {}", e.getMessage(), e);
}
return decodedToken != null && decodedToken.getUid().equals(uid);
}
/**
* 获取用户基本信息
*
* @param uid
* @return
*/
public static UserRecord getUserById(String uid) {
UserRecord userRecord = null;
try {
userRecord = FirebaseAuth.getInstance().getUser(uid);
} catch (FirebaseAuthException e) {
log.error(e.getMessage(), e);
}
return userRecord;
}
/**
* 给指定的用户发送推送消息
*
* @param req 请求参数
*/
public void sendMessage(FirebaseMessagingReq req) {
//开发和测试环境需要自己配置代理访问外网
if (!runtimeEnv.isProd()){
System.setProperty("https.proxyHost", "10.0.84.188");
System.setProperty("https.proxyPort", "7890");
}
Asserts.isTrue(StrUtil.isEmpty(req.getTitle()), "param title should not be null");
Asserts.isTrue(StrUtil.isEmpty(req.getBody()), "param body should not be null");
Asserts.isTrue(Objects.isNull(req.getUserIds()), "param userIds should not be null");
// 查询用户的firebase的token,用户信息表需要保存idToken
List<UmsMemberResponse> umsMemberResponseList = umsMemberInfoService.findByUserIds(req.getUserIds());
// 如果token为空,不发送
if (CollUtil.isEmpty(umsMemberResponseList)) {
return;
}
List<Message> androidMessageList = Lists.newArrayList();
List<Message> apnsMessageList = Lists.newArrayList();
umsMemberResponseList.forEach(item -> {
String token = item.getFirebaseToken();
if (StrUtil.isNotEmpty(token)) {
req.setToken(token);
Map<String, String> data = new HashMap<>();
if (Objects.nonNull(req.getBizType())) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("bizType", req.getBizType());
jsonObject.put("bizData", req.getBizData());
jsonObject.put("imageUrl", req.getImageUrl());
data.put("data", jsonObject.toJSONString());
}
if ("ios".equals(item.getFirebaseTokenSource())) {
// 组装ios推送消息
Message apnsMessage = apnsMessage(req, data);
apnsMessageList.add(apnsMessage);
} else {
// 组装android推送消息
Message androidMessage = androidMessage(req, data);
androidMessageList.add(androidMessage);
}
}
});
// 发送推送消息
try {
if (CollUtil.isNotEmpty(androidMessageList)) {
int sendCount = send(androidMessageList);
log.info("Successfully sent android messages, count: {}", sendCount);
}
if (CollUtil.isNotEmpty(apnsMessageList)) {
int sendCount = send(apnsMessageList);
log.info("Successfully sent ios messages, count: {}", sendCount);
}
} catch (Exception e) {
log.error("send firebase messages failed:{}", e.getMessage(), e);
}
}
public int send(List<Message> messageList) throws FirebaseMessagingException {
int index = 0;
int sendCount = 0;
while (index < messageList.size()) {
int endIndex = index + 499;
//分批插入消息
BatchResponse response = FirebaseMessaging.getInstance(firebaseApplication).sendAll(messageList.subList(index, Math.min(endIndex, messageList.size())));
sendCount += response.getSuccessCount();
index += 500;
}
return sendCount;
}
public Message androidMessage(FirebaseMessagingReq req, Map<String, String> data) {
return Message.builder()
.putAllData(data)
.setAndroidConfig(AndroidConfig.builder()
.setTtl(androidTtl) // 1 hour in milliseconds
.setPriority("NORMAL".equals(androidPriority) ? AndroidConfig.Priority.NORMAL : AndroidConfig.Priority.HIGH)
.setNotification(AndroidNotification.builder()
.setTitle(req.getTitle())
.setBody(req.getBody())
.setImage(req.getImageUrl())
.build())
.build())
.setToken(req.getToken())
.build();
}
public Message apnsMessage(FirebaseMessagingReq req, Map<String, String> data) {
String value = "1";
ApnsFcmOptions.Builder builder = ApnsFcmOptions.builder().setImage(req.getImageUrl());
return Message.builder()
.putAllData(data)
.setApnsConfig(
ApnsConfig.builder()
.putHeader("apns-priority", apnsPriority)
.putHeader("mutable-content", value)
.setFcmOptions(builder.build())
.setAps(Aps.builder()
.setAlert(ApsAlert.builder()
.setTitle(req.getTitle())
.setBody(req.getBody())
.build())
.setBadge(apnsApsBadge)
.build())
.build())
.setToken("Bearer " + req.getToken())
.build();
}
/**
* firebase 消息体
*/
@Data
class FirebaseMessagingReq {
/**
* 标题
*/
private String title;
/**
* 消息实体
*/
private String body;
/**
* 用户id
*/
private List<Long> userIds;
/**
* 用户token
*/
private String token;
/**
* 业务类型 1:订单状态变更 2:售后状态变更 3:提现状态变更 4:转账记录状态变更
*/
private Integer bizType;
/**
* 业务数据
*/
private JSONObject bizData;
/**
* 图片地址
*/
private String imageUrl;
}
}
4.2 UmsSsoServiceImpl
/**
* 第三方登录方法
* @param Request
* email//邮箱
* idToken //token
* uid//唯一键
* bindEmail//绑定邮箱
* identityType//登录类型 google/facebook
* captcha//验证码
* inviteCode//推荐人手机号
* nickname//昵称
* photo//头像
* @return 用户ID
*/
@Override
@Transactional(rollbackFor = Exception.class)
public String doThirdLogin(Request request) {
String thirdUid = request.getUid();
String idToken = request.getIdToken();
UserLoginAuthorize userLoginAuthorize = null;
// 验证token是否有效
Boolean verifyToken = fireBaseService.verifyTokenIdAndUid(idToken, request.getUid());
if(!verifyToken){
throw new BizException(SUS00016.getCode(),SUS00016.getInfo());
}
// 根据第三方ID查询用户登录授权信息
if(StringUtils.isNotBlank(request.getUid())){
userLoginAuthorize = iUserLoginAuthorizeAO.findByIdentifier(thirdUid);
}
// 根据邮箱查询用户登录授权信息
if(userLoginAuthorize == null && StringUtils.isNotBlank(request.getEmail())){
List<UserLoginAuthorize> authorizes = iUserLoginAuthorizeAO.findByEmail(request.getEmail(), null);
if(CollectionUtils.isNotEmpty(authorizes)){
List<UserLoginAuthorize> collect = authorizes.stream().filter(s -> s.getIdentityType().equals(request.getIdentityType())).collect(Collectors.toList());
if(CollectionUtils.isNotEmpty(collect)){
userLoginAuthorize = collect.get(0);
}
}else{
User user = userBO.getUserByEmail(request.getEmail(), request.getSubsidiaryCode());
if(user != null){
userLoginAuthorize = new UserLoginAuthorize();
userLoginAuthorize.setUserId(user.getUserId());
}
}
}
// 根据绑定邮箱查询用户登录授权信息
if(userLoginAuthorize == null && StringUtils.isNotBlank(request.getBindEmail())){
User user = userBO.getUserByEmail(request.getBindEmail(), request.getSubsidiaryCode());
if(user != null){
userLoginAuthorize = new UserLoginAuthorize();
userLoginAuthorize.setUserId(user.getUserId());
}
}
String userId = "";
if(userLoginAuthorize == null){
// 调用系统原进行注册接口
}else{
// 调用系统原登录接口
}
//新增或者更新 第三方登录授权表信息
iUserLoginAuthorizeAO.addOrUpdate(request,userId);
return userId;
}
注意
1.不同平台的同一个邮箱号注册登录的兼容情况
2.里面还有应用外消息推送的代码,可以针对用户手机推送业务消息