saas:支持社交应用的多租户配置

This commit is contained in:
YunaiV
2023-10-18 23:48:14 +08:00
parent 5842a361e2
commit d256275099
17 changed files with 375 additions and 140 deletions

View File

@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.system.api.social;
import cn.iocoder.yudao.module.system.service.social.SocialClientService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
/**
* 社交应用的 API 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class SocialClientApiImpl implements SocialClientApi {
@Resource
private SocialClientService socialClientService;
@Override
public String getAuthorizeUrl(Integer type, Integer userType, String redirectUri) {
return socialClientService.getAuthorizeUrl(type, userType, redirectUri);
}
}

View File

@ -21,11 +21,6 @@ public class SocialUserApiImpl implements SocialUserApi {
@Resource
private SocialUserService socialUserService;
@Override
public String getAuthorizeUrl(Integer type, String redirectUri) {
return socialUserService.getAuthorizeUrl(type, redirectUri);
}
@Override
public String bindSocialUser(SocialUserBindReqDTO reqDTO) {
return socialUserService.bindSocialUser(reqDTO);
@ -34,7 +29,7 @@ public class SocialUserApiImpl implements SocialUserApi {
@Override
public void unbindSocialUser(SocialUserUnbindReqDTO reqDTO) {
socialUserService.unbindSocialUser(reqDTO.getUserId(), reqDTO.getUserType(),
reqDTO.getType(), reqDTO.getUnionId());
reqDTO.getSocialType(), reqDTO.getOpenid());
}
@Override

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.system.controller.admin.auth;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.framework.security.config.SecurityProperties;
@ -16,7 +17,7 @@ import cn.iocoder.yudao.module.system.service.auth.AdminAuthService;
import cn.iocoder.yudao.module.system.service.permission.MenuService;
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
import cn.iocoder.yudao.module.system.service.permission.RoleService;
import cn.iocoder.yudao.module.system.service.social.SocialUserService;
import cn.iocoder.yudao.module.system.service.social.SocialClientService;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@ -57,7 +58,7 @@ public class AuthController {
@Resource
private PermissionService permissionService;
@Resource
private SocialUserService socialUserService;
private SocialClientService socialClientService;
@Resource
private SecurityProperties securityProperties;
@ -147,7 +148,7 @@ public class AuthController {
})
public CommonResult<String> socialLogin(@RequestParam("type") Integer type,
@RequestParam("redirectUri") String redirectUri) {
return CommonResult.success(socialUserService.getAuthorizeUrl(type, redirectUri));
return success(socialClientService.getAuthorizeUrl(type, UserTypeEnum.ADMIN.getValue(), redirectUri));
}
@PostMapping("/social-login")

View File

@ -0,0 +1,68 @@
package cn.iocoder.yudao.module.system.dal.dataobject.social;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.xingyuv.jushauth.config.AuthConfig;
import lombok.*;
/**
* 社交客户端 DO
*
* 对应 {@link AuthConfig} 配置,满足不同租户,有自己的客户端配置,实现社交(三方)登录
*
* @author 芋道源码
*/
@TableName(value = "system_social_client", autoResultMap = true)
@KeySequence("system_social_client_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SocialClientDO extends TenantBaseDO {
/**
* 编号,自增
*/
@TableId
private Long id;
/**
* 应用名
*/
private String name;
/**
* 社交类型
*
* 枚举 {@link SocialTypeEnum}
*/
private Integer socialType;
/**
* 用户类型
*
* 目的:不同用户类型,对应不同的小程序,需要自己的配置
*
* 枚举 {@link UserTypeEnum}
*/
private Integer userType;
/**
* 状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 客户端 id
*/
private String clientId;
/**
* 客户端 Secret
*/
private String clientSecret;
}

View File

@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.system.dal.mysql.social;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SocialClientMapper extends BaseMapperX<SocialClientDO> {
default SocialClientDO selectBySocialTypeAndUserType(Integer socialType, Integer userType) {
return selectOne(SocialClientDO::getSocialType, socialType,
SocialClientDO::getUserType, userType);
}
}

View File

@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.system.service.social;
import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
import com.xingyuv.jushauth.model.AuthUser;
/**
* 社交应用 Service 接口
*
* @author 芋道源码
*/
public interface SocialClientService {
/**
* 获得社交平台的授权 URL
*
* @param socialType 社交平台的类型 {@link SocialTypeEnum}
* @param userType 用户类型
* @param redirectUri 重定向 URL
* @return 社交平台的授权 URL
*/
String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri);
/**
* 请求社交平台,获得授权的用户
*
* @param socialType 社交平台的类型
* @param userType 用户类型
* @param code 授权码
* @param state 授权 state
* @return 授权的用户
*/
AuthUser getAuthUser(Integer socialType, Integer userType, String code, String state);
}

View File

@ -0,0 +1,94 @@
package cn.iocoder.yudao.module.system.service.social;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ReflectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.social.core.YudaoAuthRequestFactory;
import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO;
import cn.iocoder.yudao.module.system.dal.mysql.social.SocialClientMapper;
import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
import com.xingyuv.jushauth.config.AuthConfig;
import com.xingyuv.jushauth.model.AuthCallback;
import com.xingyuv.jushauth.model.AuthResponse;
import com.xingyuv.jushauth.model.AuthUser;
import com.xingyuv.jushauth.request.AuthRequest;
import com.xingyuv.jushauth.utils.AuthStateUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SOCIAL_USER_AUTH_FAILURE;
/**
* 社交应用 Service 实现类
*
* @author 芋道源码
*/
@Service
@Slf4j
public class SocialClientServiceImpl implements SocialClientService {
@Resource // 由于自定义了 YudaoAuthRequestFactory 无法覆盖默认的 AuthRequestFactory所以只能注入它
private YudaoAuthRequestFactory yudaoAuthRequestFactory;
@Resource
private SocialClientMapper socialClientMapper;
@Override
public String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri) {
// 获得对应的 AuthRequest 实现
AuthRequest authRequest = buildAuthRequest(socialType, userType);
// 生成跳转地址
String authorizeUri = authRequest.authorize(AuthStateUtils.createState());
return HttpUtils.replaceUrlQuery(authorizeUri, "redirect_uri", redirectUri);
}
@Override
public AuthUser getAuthUser(Integer socialType, Integer userType, String code, String state) {
// 构建请求
AuthRequest authRequest = buildAuthRequest(socialType, userType);
AuthCallback authCallback = AuthCallback.builder().code(code).state(state).build();
// 执行请求
AuthResponse<?> authResponse = authRequest.login(authCallback);
log.info("[getAuthUser][请求社交平台 type({}) request({}) response({})]", socialType,
toJsonString(authCallback), toJsonString(authResponse));
if (!authResponse.ok()) {
throw exception(SOCIAL_USER_AUTH_FAILURE, authResponse.getMsg());
}
return (AuthUser) authResponse.getData();
}
/**
* 构建 AuthRequest 对象,支持多租户配置
*
* @param socialType 社交类型
* @param userType 用户类型
* @return AuthRequest 对象
*/
private AuthRequest buildAuthRequest(Integer socialType, Integer userType) {
// 1. 先查找默认的配置项,从 application-*.yaml 中读取
AuthRequest request = yudaoAuthRequestFactory.get(SocialTypeEnum.valueOfType(socialType).getSource());
Assert.notNull(request, String.format("社交平台(%d) 不存在", socialType));
// 2. 查询 DB 的配置项,如果存在则进行覆盖
SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType(socialType, userType);
if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
// 2.1 构造新的 AuthConfig 对象
AuthConfig authConfig = (AuthConfig) ReflectUtil.getFieldValue(request, "config");
AuthConfig newAuthConfig = ReflectUtil.newInstance(authConfig.getClass());
BeanUtil.copyProperties(authConfig, newAuthConfig);
// 2.2 修改对应的 clientId + clientSecret 密钥
newAuthConfig.setClientId(client.getClientId());
newAuthConfig.setClientSecret(client.getClientSecret());
// 2.3 设置会 request 里,进行后续使用
ReflectUtil.setFieldValue(request, "config", newAuthConfig);
}
return request;
}
}

View File

@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO;
import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
@ -17,27 +16,6 @@ import java.util.List;
*/
public interface SocialUserService {
/**
* 获得社交平台的授权 URL
*
* @param type 社交平台的类型 {@link SocialTypeEnum}
* @param redirectUri 重定向 URL
* @return 社交平台的授权 URL
*/
String getAuthorizeUrl(Integer type, String redirectUri);
/**
* 授权获得对应的社交用户
* 如果授权失败,则会抛出 {@link ServiceException} 异常
*
* @param type 社交平台的类型 {@link SocialTypeEnum}
* @param code 授权码
* @param state state
* @return 授权用户
*/
@NotNull
SocialUserDO authSocialUser(Integer type, String code, String state);
/**
* 获得指定用户的社交用户列表
*

View File

@ -2,8 +2,7 @@ package cn.iocoder.yudao.module.system.service.social;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.social.core.YudaoAuthRequestFactory;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO;
import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserBindDO;
@ -11,24 +10,22 @@ import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO;
import cn.iocoder.yudao.module.system.dal.mysql.social.SocialUserBindMapper;
import cn.iocoder.yudao.module.system.dal.mysql.social.SocialUserMapper;
import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
import com.xingyuv.jushauth.model.AuthCallback;
import com.xingyuv.jushauth.model.AuthResponse;
import com.xingyuv.jushauth.model.AuthUser;
import com.xingyuv.jushauth.request.AuthRequest;
import com.xingyuv.jushauth.utils.AuthStateUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import javax.validation.constraints.NotNull;
import java.util.Collections;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.AUTH_THIRD_LOGIN_NOT_BIND;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SOCIAL_USER_NOT_FOUND;
/**
* 社交用户 Service 实现类
@ -40,51 +37,13 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
@Slf4j
public class SocialUserServiceImpl implements SocialUserService {
@Resource// 由于自定义了 YudaoAuthRequestFactory 无法覆盖默认的 AuthRequestFactory所以只能注入它
private YudaoAuthRequestFactory yudaoAuthRequestFactory;
@Resource
private SocialUserBindMapper socialUserBindMapper;
@Resource
private SocialUserMapper socialUserMapper;
@Override
public String getAuthorizeUrl(Integer type, String redirectUri) {
// 获得对应的 AuthRequest 实现
AuthRequest authRequest = yudaoAuthRequestFactory.get(SocialTypeEnum.valueOfType(type).getSource());
// 生成跳转地址
String authorizeUri = authRequest.authorize(AuthStateUtils.createState());
return HttpUtils.replaceUrlQuery(authorizeUri, "redirect_uri", redirectUri);
}
@Override
public SocialUserDO authSocialUser(Integer type, String code, String state) {
// 优先从 DB 中获取,因为 code 有且可以使用一次。
// 在社交登录时,当未绑定 User 时,需要绑定登录,此时需要 code 使用两次
SocialUserDO socialUser = socialUserMapper.selectByTypeAndCodeAnState(type, code, state);
if (socialUser != null) {
return socialUser;
}
// 请求获取
AuthUser authUser = getAuthUser(type, code, state);
Assert.notNull(authUser, "三方用户不能为空");
// 保存到 DB 中
socialUser = socialUserMapper.selectByTypeAndOpenid(type, authUser.getUuid());
if (socialUser == null) {
socialUser = new SocialUserDO();
}
socialUser.setType(type).setCode(code).setState(state) // 需要保存 code + state 字段,保证后续可查询
.setOpenid(authUser.getUuid()).setToken(authUser.getToken().getAccessToken()).setRawTokenInfo((toJsonString(authUser.getToken())))
.setNickname(authUser.getNickname()).setAvatar(authUser.getAvatar()).setRawUserInfo(toJsonString(authUser.getRawUserInfo()));
if (socialUser.getId() == null) {
socialUserMapper.insert(socialUser);
} else {
socialUserMapper.updateById(socialUser);
}
return socialUser;
}
@Resource
private SocialClientService socialClientService;
@Override
public List<SocialUserDO> getSocialUserList(Long userId, Integer userType) {
@ -101,7 +60,8 @@ public class SocialUserServiceImpl implements SocialUserService {
@Transactional
public String bindSocialUser(SocialUserBindReqDTO reqDTO) {
// 获得社交用户
SocialUserDO socialUser = authSocialUser(reqDTO.getType(), reqDTO.getCode(), reqDTO.getState());
SocialUserDO socialUser = authSocialUser(reqDTO.getSocialType(), reqDTO.getUserType(),
reqDTO.getCode(), reqDTO.getState());
Assert.notNull(socialUser, "社交用户不能为空");
// 社交用户可能之前绑定过别的用户,需要进行解绑
@ -134,7 +94,7 @@ public class SocialUserServiceImpl implements SocialUserService {
@Override
public SocialUserRespDTO getSocialUser(Integer userType, Integer type, String code, String state) {
// 获得社交用户
SocialUserDO socialUser = authSocialUser(type, code, state);
SocialUserDO socialUser = authSocialUser(type, userType, code, state);
Assert.notNull(socialUser, "社交用户不能为空");
// 如果未绑定的社交用户,则无法自动登录,进行报错
@ -146,24 +106,44 @@ public class SocialUserServiceImpl implements SocialUserService {
return new SocialUserRespDTO(socialUser.getOpenid(), socialUserBind.getUserId());
}
// TODO 芋艿:调整下单测
/**
* 请求社交平台,获得授权的用户
* 授权获得对应的社交用户
* 如果授权失败,则会抛出 {@link ServiceException} 异常
*
* @param type 社交平台的类型
* @param type 社交平台的类型 {@link SocialTypeEnum}
* @param userType 用户类型
* @param code 授权码
* @param state 授权 state
* @return 授权用户
* @param state state
* @return 授权用户
*/
private AuthUser getAuthUser(Integer type, String code, String state) {
AuthRequest authRequest = yudaoAuthRequestFactory.get(SocialTypeEnum.valueOfType(type).getSource());
AuthCallback authCallback = AuthCallback.builder().code(code).state(state).build();
AuthResponse<?> authResponse = authRequest.login(authCallback);
log.info("[getAuthUser][请求社交平台 type({}) request({}) response({})]", type,
toJsonString(authCallback), toJsonString(authResponse));
if (!authResponse.ok()) {
throw exception(SOCIAL_USER_AUTH_FAILURE, authResponse.getMsg());
@NotNull
public SocialUserDO authSocialUser(Integer type, Integer userType, String code, String state) {
// 优先从 DB 中获取,因为 code 有且可以使用一次。
// 在社交登录时,当未绑定 User 时,需要绑定登录,此时需要 code 使用两次
SocialUserDO socialUser = socialUserMapper.selectByTypeAndCodeAnState(type, code, state);
if (socialUser != null) {
return socialUser;
}
return (AuthUser) authResponse.getData();
// 请求获取
AuthUser authUser = socialClientService.getAuthUser(type, userType, code, state);
Assert.notNull(authUser, "三方用户不能为空");
// 保存到 DB 中
socialUser = socialUserMapper.selectByTypeAndOpenid(type, authUser.getUuid());
if (socialUser == null) {
socialUser = new SocialUserDO();
}
socialUser.setType(type).setCode(code).setState(state) // 需要保存 code + state 字段,保证后续可查询
.setOpenid(authUser.getUuid()).setToken(authUser.getToken().getAccessToken()).setRawTokenInfo((toJsonString(authUser.getToken())))
.setNickname(authUser.getNickname()).setAvatar(authUser.getAvatar()).setRawUserInfo(toJsonString(authUser.getRawUserInfo()));
if (socialUser.getId() == null) {
socialUserMapper.insert(socialUser);
} else {
socialUserMapper.updateById(socialUser);
}
return socialUser;
}
}