This commit is contained in:
dzc committed 2026-08-10 10:21:21 +08:00
commit bc0f21a6f6
18 files changed
+499 -14

No files matched your search

@@ -1,6 +1,5 @@
package com.bsd.pjcloud.canteen.controller;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.collection.CollUtil;
@@ -9,10 +8,10 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.canteen.entity.CanteenCategoryEntity;
import com.bsd.pjcloud.canteen.service.CanteenCategoryService;
import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.excel.annotation.ResponseExcel;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@@ -40,6 +39,8 @@ public class CanteenCategoryController {
private final CanteenCategoryService canteenCategoryService;
private final CanteenMerchantScopeHelper merchantScopeHelper;
/**
* 分页查询
*
@@ -53,6 +54,7 @@ public class CanteenCategoryController {
public R getCanteenCategoryPage(@ParameterObject Page page, @ParameterObject CanteenCategoryEntity canteenCategory) {
LambdaQueryWrapper<CanteenCategoryEntity> wrapper = Wrappers.lambdaQuery();
wrapper.like(StrUtil.isNotBlank(canteenCategory.getName()), CanteenCategoryEntity::getName, canteenCategory.getName());
merchantScopeHelper.applyMerchantIdScope(wrapper, canteenCategory.getMerchantId(), CanteenCategoryEntity::getMerchantId);
return R.ok(canteenCategoryService.selectPageByScope(page, wrapper));
}
@@ -81,7 +83,6 @@ public class CanteenCategoryController {
@PostMapping
@PreAuthorize("@pms.hasPermission('canteen_canteenCategory_add')")
public R save(@RequestBody CanteenCategoryEntity canteenCategory) {
canteenCategory.setDeptId(SecurityUtils.getUser().getDeptId());
return R.ok(canteenCategoryService.create(canteenCategory));
}
@@ -143,14 +144,14 @@ public class CanteenCategoryController {
public R<List<CanteenCategoryEntity>> getAllSimple(
@RequestParam(value = "categoryType", required = false) String categoryType,
@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "merchantId", required = false) String merchantId
@RequestParam(value = "merchantId", required = false) Long merchantId
) {
LambdaQueryWrapper<CanteenCategoryEntity> wrapper = Wrappers.lambdaQuery();
wrapper.like(StringUtils.isNotBlank(name), CanteenCategoryEntity::getName, name);
wrapper.eq(ObjectUtil.isNotEmpty(merchantId), CanteenCategoryEntity::getMerchantId, merchantId);
merchantScopeHelper.applyMerchantIdScope(wrapper, merchantId, CanteenCategoryEntity::getMerchantId);
wrapper.orderByAsc(CanteenCategoryEntity::getSort);
wrapper.apply(StringUtils.isNotEmpty(categoryType), "FIND_IN_SET({0}, your_column_name)", categoryType);
List<CanteenCategoryEntity> list = canteenCategoryService.list(wrapper);
wrapper.apply(StringUtils.isNotEmpty(categoryType), "FIND_IN_SET({0}, category_type)", categoryType);
List<CanteenCategoryEntity> list = canteenCategoryService.listScope(wrapper);
return R.ok(list);
}
@@ -148,8 +148,6 @@ public class CanteenSpuController {
@PreAuthorize("@pms.hasPermission('canteen_canteenSpu_add')")
@Transactional
public R save(@RequestBody CanteenSpuEntity canteenSpu) {
Long deptId = SecurityUtils.getUser().getDeptId();
canteenSpu.setDeptId(deptId);
try {
boolean save = canteenSpuService.saveDeep(canteenSpu);
CanteenStockEntity canteenStockEntity = new CanteenStockEntity();
@@ -159,6 +157,7 @@ public class CanteenSpuController {
canteenStockEntity.setInventoryUpperLimit(0);
canteenStockEntity.setInventoryLowerLimit(0);
canteenStockEntity.setCurrentStock(0D);
Long deptId = canteenSpu.getDeptId() != null ? canteenSpu.getDeptId() : SecurityUtils.getUser().getDeptId();
canteenStockEntity.setDeptId(deptId);
canteenStockService.save(canteenStockEntity);
return R.ok(save);
@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.canteen.entity.CanteenCategoryEntity;
import com.bsd.pjcloud.canteen.mapper.CanteenCategoryMapper;
import com.bsd.pjcloud.canteen.service.CanteenCategoryService;
import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper;
import com.bsd.pjcloud.common.data.datascope.DataScope;
import com.bsd.pjcloud.mqtt.api.feign.RemoteMqttEventService;
import lombok.RequiredArgsConstructor;
@@ -27,6 +28,8 @@ public class CanteenCategoryServiceImpl extends ServiceImpl<CanteenCategoryMappe
private final RemoteMqttEventService remoteMqttEventService;
private final CanteenMerchantScopeHelper merchantScopeHelper;
@Override
public Page<CanteenCategoryEntity> selectPageByScope(Page page, LambdaQueryWrapper<CanteenCategoryEntity> wrapper) {
return baseMapper.selectPageByScope(page, wrapper, DataScope.of());
@@ -39,6 +42,7 @@ public class CanteenCategoryServiceImpl extends ServiceImpl<CanteenCategoryMappe
@Override
public Long create(CanteenCategoryEntity canteenCategory) {
merchantScopeHelper.fillDeptIdByMerchantId(canteenCategory.getMerchantId(), canteenCategory::setDeptId);
baseMapper.insert(canteenCategory);
// 发送mq消息
remoteMqttEventService.sendCanteenClassifyChangeMsg(canteenCategory.getDeptId());
@@ -49,9 +53,11 @@ public class CanteenCategoryServiceImpl extends ServiceImpl<CanteenCategoryMappe
public Boolean edit(CanteenCategoryEntity canteenCategory) {
CanteenCategoryEntity detail = baseMapper.selectById(canteenCategory.getId());
if (ObjectUtil.isEmpty(detail)) throw new RuntimeException("菜品分类不存在");
merchantScopeHelper.fillDeptIdByMerchantId(canteenCategory.getMerchantId(), canteenCategory::setDeptId);
baseMapper.updateById(canteenCategory);
// 发送mq消息
remoteMqttEventService.sendCanteenClassifyChangeMsg(detail.getDeptId());
remoteMqttEventService.sendCanteenClassifyChangeMsg(
canteenCategory.getDeptId() != null ? canteenCategory.getDeptId() : detail.getDeptId());
return true;
}
@@ -20,6 +20,7 @@ import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO;
import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuSimpleVO;
import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuStockVO;
import com.bsd.pjcloud.canteen.vo.ADCanteenSpuListVO;
import com.bsd.pjcloud.canteen.util.CanteenMerchantScopeHelper;
import com.bsd.pjcloud.common.core.util.BeanUtils;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.datascope.DataScope;
@@ -49,6 +50,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
private final CanteenStockMapper canteenStockMapper;
private final CanteenOnlineSpuMapper canteenOnlineSpuMapper;
private final CanteenSpuComboItemMapper comboItemMapper;
private final CanteenMerchantScopeHelper merchantScopeHelper;
@Override
public Object selectFoodList(LambdaQueryWrapper<CanteenSpuEntity> wrapper) {
@@ -125,6 +127,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
LambdaQueryWrapper<CanteenSpuEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StrUtil.isNotBlank(canteenSpu.getName()), CanteenSpuEntity::getName, canteenSpu.getName());
wrapper.eq(Objects.nonNull(canteenSpu.getCategoryId()), CanteenSpuEntity::getCategoryId, canteenSpu.getCategoryId());
merchantScopeHelper.applyMerchantIdScope(wrapper, canteenSpu.getMerchantId(), CanteenSpuEntity::getMerchantId);
wrapper.orderByAsc(CanteenSpuEntity::getSort).orderByDesc(CanteenSpuEntity::getId);
Page pageData = baseMapper.selectPageByScope(page, wrapper, DataScope.of());
@@ -154,6 +157,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
List<Long> caIds = canteenCategoryEntities.stream().map(CanteenCategoryEntity::getId).collect(Collectors.toList());
wrapper.in(!CollectionUtils.isEmpty(caIds), CanteenSpuEntity::getCategoryId, caIds);
merchantScopeHelper.applyMerchantIdScope(wrapper, canteenDailySpuDTO.getMerchantId(), CanteenSpuEntity::getMerchantId);
// wrapper.orderByAsc(CanteenSpuEntity::getSort);
return baseMapper.selectPageByScope(page, wrapper, DataScope.of().only(true));
@@ -252,10 +256,13 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
LambdaQueryWrapper<CanteenSpuEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StrUtil.isNotBlank(canteenSpu.getName()), CanteenSpuEntity::getName, canteenSpu.getName());
wrapper.eq(Objects.nonNull(canteenSpu.getCategoryId()), CanteenSpuEntity::getCategoryId, canteenSpu.getCategoryId());
merchantScopeHelper.applyMerchantIdScope(wrapper, canteenSpu.getMerchantId(), CanteenSpuEntity::getMerchantId);
wrapper.orderByAsc(CanteenSpuEntity::getSort).orderByDesc(CanteenSpuEntity::getId);
LambdaQueryWrapper<CanteenOnlineSpuEntity> canteenOnlineSpuEntityLambdaQueryWrapper = new LambdaQueryWrapper<>();
canteenOnlineSpuEntityLambdaQueryWrapper.isNotNull(CanteenOnlineSpuEntity::getLinkSpuId);
merchantScopeHelper.applyMerchantIdScope(canteenOnlineSpuEntityLambdaQueryWrapper, canteenSpu.getMerchantId(),
CanteenOnlineSpuEntity::getMerchantId);
List<CanteenOnlineSpuEntity> canteenOnlineSpuEntities = canteenOnlineSpuMapper.selectListByScope(canteenOnlineSpuEntityLambdaQueryWrapper, DataScope.of());
Set<Long> spuIds = canteenOnlineSpuEntities.stream().map(CanteenOnlineSpuEntity::getLinkSpuId).collect(Collectors.toSet());
@@ -288,6 +295,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
public Boolean saveDeep(CanteenSpuEntity entity) {
normalizeSpuType(entity);
validateCombo(entity, null);
merchantScopeHelper.fillDeptIdByMerchantId(entity.getMerchantId(), entity::setDeptId);
baseMapper.insert(entity);
saveComboItems(entity.getId(), entity.getSpuType(), entity.getComboItems());
return Boolean.TRUE;
@@ -298,6 +306,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
public Boolean updateDeep(CanteenSpuEntity entity) {
normalizeSpuType(entity);
validateCombo(entity, entity.getId());
merchantScopeHelper.fillDeptIdByMerchantId(entity.getMerchantId(), entity::setDeptId);
baseMapper.updateById(entity);
comboItemMapper.delete(Wrappers.<CanteenSpuComboItemEntity>lambdaQuery()
.eq(CanteenSpuComboItemEntity::getComboSpuId, entity.getId()));
@@ -0,0 +1,152 @@
package com.bsd.pjcloud.canteen.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.bsd.pjcloud.admin.api.entity.SysDept;
import com.bsd.pjcloud.admin.api.entity.SysRole;
import com.bsd.pjcloud.admin.api.feign.RemoteDataScopeService;
import com.bsd.pjcloud.admin.api.feign.RemoteDeptService;
import com.bsd.pjcloud.common.core.constant.CommonRoleConstant;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.core.util.RetOps;
import com.bsd.pjcloud.common.data.datascope.DataScopeTypeEnum;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* 食堂后台商户数据隔离:数据权限为「全部」或 ROLE_ADMIN 可见全部;其余账号仅可见本部门及下级关联商户。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CanteenMerchantScopeHelper {
private final RemoteMchService remoteMchService;
private final RemoteDeptService remoteDeptService;
private final RemoteDataScopeService remoteDataScopeService;
/**
* 是否可查看全部商户数据(管理员 / 数据权限=全部)
*/
public boolean canViewAllMerchants() {
PjcloudUser user = SecurityUtils.getUser();
if (user == null) {
return false;
}
List<Long> roleIds = SecurityUtils.getRoleIds();
if (CollUtil.isEmpty(roleIds)) {
return false;
}
List<SysRole> roleList = RetOps.of(remoteDataScopeService.getRoleList(roleIds))
.getData()
.orElseGet(Collections::emptyList);
if (CollUtil.isEmpty(roleList)) {
return false;
}
for (SysRole role : roleList) {
if (role == null) {
continue;
}
if (StrUtil.equals(role.getRoleCode(), CommonRoleConstant.ROLE_ADMIN)) {
return true;
}
if (Objects.equals(role.getDsType(), DataScopeTypeEnum.ALL.getType())) {
return true;
}
}
return false;
}
/**
* 当前登录用户可访问的食堂商户 ID 列表(部门及下级 → mch_manage)
*/
public List<Long> resolveAllowedMerchantIds() {
PjcloudUser user = SecurityUtils.getUser();
if (user == null || user.getDeptId() == null) {
return Collections.emptyList();
}
Long rootDeptId = user.getDeptId();
List<Long> deptIds;
R<List<SysDept>> deptR = remoteDeptService.getDescendantList(rootDeptId);
if (deptR != null && deptR.isOk() && CollUtil.isNotEmpty(deptR.getData())) {
deptIds = deptR.getData().stream().map(SysDept::getDeptId).filter(Objects::nonNull).distinct().toList();
}
else {
deptIds = List.of(rootDeptId);
}
R<List<MchManageVo>> mchR = remoteMchService.getByDeptIds(deptIds);
if (mchR == null || !mchR.isOk() || CollUtil.isEmpty(mchR.getData())) {
return Collections.emptyList();
}
return mchR.getData().stream()
.map(MchManageVo::getId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
}
/**
* 列表查询商户隔离:管理员可不传 merchantId 看全部;非管理员强制限制在可访问商户内。
*
* @param queryMerchantId 前端可选筛选商户;非管理员时若传了且不在允许范围则查空
*/
public <T> void applyMerchantIdScope(LambdaQueryWrapper<T> wrapper, Long queryMerchantId,
SFunction<T, ?> merchantIdColumn) {
if (canViewAllMerchants()) {
if (queryMerchantId != null) {
wrapper.eq(merchantIdColumn, queryMerchantId);
}
return;
}
List<Long> allowed = resolveAllowedMerchantIds();
if (CollUtil.isEmpty(allowed)) {
wrapper.apply("1 = 0");
return;
}
if (queryMerchantId != null) {
if (allowed.contains(queryMerchantId)) {
wrapper.eq(merchantIdColumn, queryMerchantId);
}
else {
wrapper.apply("1 = 0");
}
return;
}
wrapper.in(merchantIdColumn, allowed);
}
/**
* 按商户回填 deptId(写入时与商户部门对齐,保证 DataScope 与商户隔离一致)
*/
public void fillDeptIdByMerchantId(Long merchantId, Consumer<Long> deptIdSetter) {
if (merchantId == null || deptIdSetter == null) {
return;
}
try {
R<MchManageVo> r = remoteMchService.getById(merchantId);
if (r != null && r.isOk() && ObjectUtil.isNotEmpty(r.getData()) && r.getData().getDeptId() != null) {
deptIdSetter.accept(r.getData().getDeptId());
}
}
catch (Exception e) {
log.warn("按商户回填部门失败, merchantId={}", merchantId, e);
}
}
}
@@ -47,7 +47,9 @@
t.hour_type,
sp.price,
sp.category_id,
<!-- 数据权限拦截器外层按 create_by / dept_id 过滤,须透传到临时结果集 -->
sp.dept_id,
t.create_by,
IFNULL(sp.spu_type, 1) as spu_type
FROM canteen_daily_menu t
LEFT JOIN canteen_spu sp
@@ -137,6 +137,11 @@ public interface CacheConstants {
*/
String WX_PC_SOCIAL_TICKET = GLOBALLY + "wx:pc:social:ticket:";
/**
* App Apple 社交登录一次性 ticket(sub 缓存)
*/
String APPLE_SOCIAL_TICKET = GLOBALLY + "apple:social:ticket:";
/**
* PC 密码已校验待绑定微信(value=userId)
*/
@@ -83,7 +83,17 @@ public enum LoginTypeEnum {
/**
* CAS 登录
*/
CAS("CAS", "CAS 登录");
CAS("CAS", "CAS 登录"),
/**
* Apple 登录(移动端 App,identify 入参为 identityToken)
*/
APPLE("APPLE", "Apple登录"),
/**
* Apple identityToken 兑换后的一次性 ticket 登录
*/
APPLE_TICKET("APPLE_TICKET", "Apple Ticket登录");
/**
* 类型
@@ -194,6 +194,12 @@ public class SysUser extends Model<SysUser> implements Serializable {
@Schema(description = "PC微信openid")
private String wxPcOpenid;
/**
* Apple 登录唯一标识(identityToken.sub)
*/
@Schema(description = "Apple openid")
private String appleOpenid;
/**
* 企微微信 userid
*/
@@ -98,6 +98,12 @@ public class UserVO implements BaseFacesInterface, Serializable {
@Schema(description = "PC微信open id")
private String wxPcOpenid;
/**
* Apple openid
*/
@Schema(description = "Apple open id")
private String appleOpenid;
/**
* QQ openid
*/
@@ -163,6 +163,15 @@ public class SysSocialDetailsController {
return R.ok(sysSocialDetailsService.resolveWxCode(code));
}
/**
* App Apple:解析 identityToken(一次性),返回绑定状态与 ticket
*/
@Inner(value = false)
@PostMapping("/apple/resolve")
public R<WxSocialResolveVO> resolveAppleIdentityToken(String identityToken) {
return R.ok(sysSocialDetailsService.resolveAppleIdentityToken(identityToken));
}
/**
* App 微信:使用 ticket 绑定(需已登录)
*/
@@ -0,0 +1,59 @@
package com.bsd.pjcloud.admin.handler;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bsd.pjcloud.admin.api.dto.UserInfo;
import com.bsd.pjcloud.admin.api.entity.SysSocialDetails;
import com.bsd.pjcloud.admin.api.entity.SysUser;
import com.bsd.pjcloud.admin.handler.apple.AppleIdentityTokenVerifier;
import com.bsd.pjcloud.admin.mapper.SysSocialDetailsMapper;
import com.bsd.pjcloud.admin.service.SysUserService;
import com.bsd.pjcloud.common.core.constant.enums.LoginTypeEnum;
import com.bsd.pjcloud.common.core.exception.CheckedException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* Apple 登录:identify 入参为 App 端 identityToken
*/
@Slf4j
@Component("APPLE")
@AllArgsConstructor
public class AppleLoginHandler extends AbstractLoginHandler {
private final SysUserService sysUserService;
private final SysSocialDetailsMapper sysSocialDetailsMapper;
@Override
public String identify(String identityToken) {
SysSocialDetails condition = new SysSocialDetails();
condition.setType(LoginTypeEnum.APPLE.getType());
SysSocialDetails socialDetails = sysSocialDetailsMapper.selectOne(new QueryWrapper<>(condition));
if (socialDetails == null) {
throw new CheckedException("未配置Apple登录");
}
String sub = AppleIdentityTokenVerifier.verifyAndGetSubject(identityToken, socialDetails.getAppId());
log.debug("Apple identityToken 校验通过, sub={}", sub);
return sub;
}
@Override
public UserInfo info(String appleOpenid) {
SysUser user = sysUserService.getOne(Wrappers.<SysUser>query().lambda().eq(SysUser::getAppleOpenid, appleOpenid));
if (user == null) {
log.info("Apple未绑定:{}", appleOpenid);
return null;
}
return sysUserService.findUserInfo(user);
}
@Override
public Boolean bind(SysUser user, String identify) {
user.setAppleOpenid(identify);
sysUserService.updateById(user);
return true;
}
}
@@ -0,0 +1,62 @@
package com.bsd.pjcloud.admin.handler;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bsd.pjcloud.admin.api.dto.UserInfo;
import com.bsd.pjcloud.admin.api.entity.SysUser;
import com.bsd.pjcloud.admin.service.SysUserService;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.common.core.exception.CheckedException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
/**
* Apple 登录 ticket 处理器(mobile=APPLE_TICKET@{ticket})
*/
@Slf4j
@Component("APPLE_TICKET")
@RequiredArgsConstructor
public class AppleTicketLoginHandler implements LoginHandler {
private final StringRedisTemplate redisTemplate;
private final SysUserService sysUserService;
private String ticketKey(String ticket) {
return CacheConstants.APPLE_SOCIAL_TICKET + ticket;
}
@Override
public Boolean check(String loginStr) {
return true;
}
@Override
public String identify(String ticket) {
String openid = redisTemplate.opsForValue().get(ticketKey(ticket));
if (StrUtil.isBlank(openid)) {
throw new CheckedException("Apple授权已过期,请重新授权");
}
redisTemplate.delete(ticketKey(ticket));
return openid;
}
@Override
public UserInfo info(String appleOpenid) {
SysUser user = sysUserService.getOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getAppleOpenid, appleOpenid));
if (user == null) {
log.info("Apple未绑定:{}", appleOpenid);
return null;
}
return sysUserService.findUserInfo(user);
}
@Override
public UserInfo handle(String loginStr) {
String openid = identify(loginStr);
return info(openid);
}
}
@@ -0,0 +1,124 @@
package com.bsd.pjcloud.admin.handler.apple;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.bsd.pjcloud.common.core.exception.CheckedException;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
/**
* 校验 Apple identityToken(JWKS),返回 sub
*/
@Slf4j
public final class AppleIdentityTokenVerifier {
private static final String APPLE_ISS = "https://appleid.apple.com";
private static final String APPLE_KEYS_URL = "https://appleid.apple.com/auth/keys";
private static final long CACHE_TTL_MS = 6L * 60 * 60 * 1000;
private static final AtomicReference<CachedJwkSet> CACHE = new AtomicReference<>();
private AppleIdentityTokenVerifier() {
}
public static String verifyAndGetSubject(String identityToken, String audience) {
if (StrUtil.isBlank(identityToken)) {
throw new CheckedException("Apple身份令牌为空");
}
if (StrUtil.isBlank(audience)) {
throw new CheckedException("未配置Apple appId(Bundle ID)");
}
try {
SignedJWT jwt = SignedJWT.parse(identityToken.trim());
String kid = jwt.getHeader().getKeyID();
RSAKey rsaKey = findRsaKey(kid, false);
if (rsaKey == null) {
rsaKey = findRsaKey(kid, true);
}
if (rsaKey == null) {
throw new CheckedException("Apple公钥不匹配,请稍后重试");
}
JWSVerifier verifier = new RSASSAVerifier(rsaKey.toRSAPublicKey());
if (!jwt.verify(verifier)) {
throw new CheckedException("Apple身份令牌验签失败");
}
JWTClaimsSet claims = jwt.getJWTClaimsSet();
if (!APPLE_ISS.equals(claims.getIssuer())) {
throw new CheckedException("Apple身份令牌发行方无效");
}
List<String> aud = claims.getAudience();
if (aud == null || !aud.contains(audience)) {
throw new CheckedException("Apple身份令牌audience不匹配");
}
Date exp = claims.getExpirationTime();
if (exp == null || exp.before(new Date())) {
throw new CheckedException("Apple身份令牌已过期");
}
String sub = claims.getSubject();
if (StrUtil.isBlank(sub)) {
throw new CheckedException("Apple身份令牌缺少用户标识");
}
return sub;
}
catch (CheckedException e) {
throw e;
}
catch (Exception e) {
log.error("校验Apple identityToken失败", e);
throw new CheckedException("Apple身份令牌无效");
}
}
private static RSAKey findRsaKey(String kid, boolean forceRefresh) throws Exception {
JWKSet jwkSet = loadJwkSet(forceRefresh);
if (jwkSet == null || StrUtil.isBlank(kid)) {
return null;
}
for (JWK jwk : jwkSet.getKeys()) {
if (kid.equals(jwk.getKeyID()) && jwk instanceof RSAKey) {
return (RSAKey) jwk;
}
}
return null;
}
private static JWKSet loadJwkSet(boolean forceRefresh) throws Exception {
CachedJwkSet cached = CACHE.get();
long now = System.currentTimeMillis();
if (!forceRefresh && cached != null && now - cached.loadedAt < CACHE_TTL_MS) {
return cached.jwkSet;
}
String body = HttpUtil.get(APPLE_KEYS_URL);
if (StrUtil.isBlank(body)) {
throw new CheckedException("获取Apple公钥失败");
}
// 兼容部分环境返回结构校验
JSONObject root = JSONUtil.parseObj(body);
JSONArray keys = root.getJSONArray("keys");
if (keys == null || keys.isEmpty()) {
throw new CheckedException("Apple公钥为空");
}
JWKSet jwkSet = JWKSet.parse(body);
CACHE.set(new CachedJwkSet(jwkSet, now));
return jwkSet;
}
private record CachedJwkSet(JWKSet jwkSet, long loadedAt) {
}
}
@@ -51,6 +51,11 @@ public interface SysSocialDetailsService extends IService<SysSocialDetails> {
*/
WxSocialResolveVO resolveWxCode(String code);
/**
* 解析 Apple identityToken(仅校验一次),返回是否已绑定及 ticket
*/
WxSocialResolveVO resolveAppleIdentityToken(String identityToken);
/**
* 根据入参查询用户信息
* @param inStr
@@ -66,15 +66,22 @@ public class SysSocialDetailsServiceImpl extends ServiceImpl<SysSocialDetailsMap
private static final long WX_TICKET_TTL_MINUTES = 15;
private String wxTicketKey(String type, String ticket) {
private String socialTicketKey(String type, String ticket) {
if ("WX_PC".equals(type)) {
return CacheConstants.WX_PC_SOCIAL_TICKET + ticket;
}
if ("APPLE".equals(type)) {
return CacheConstants.APPLE_SOCIAL_TICKET + ticket;
}
return CacheConstants.WX_SOCIAL_TICKET + ticket;
}
private String wxTicketKey(String type, String ticket) {
return socialTicketKey(type, ticket);
}
private String wxTicketKey(String ticket) {
return wxTicketKey("WX", ticket);
return socialTicketKey("WX", ticket);
}
/**
@@ -101,7 +108,7 @@ public class SysSocialDetailsServiceImpl extends ServiceImpl<SysSocialDetailsMap
public Boolean bindSocialByTicket(String type, String ticket) {
String openid = redisTemplate.opsForValue().get(wxTicketKey(type, ticket));
if (StrUtil.isBlank(openid)) {
throw new CheckedException("微信授权已过期,请重新授权");
throw new CheckedException("授权已过期,请重新授权");
}
LoginHandler loginHandler = loginHandlerMap.get(type);
SysUser sysUser = sysUserMapper.selectById(SecurityUtils.getUser().getId());
@@ -124,6 +131,22 @@ public class SysSocialDetailsServiceImpl extends ServiceImpl<SysSocialDetailsMap
return vo;
}
@Override
public WxSocialResolveVO resolveAppleIdentityToken(String identityToken) {
LoginHandler appleHandler = loginHandlerMap.get("APPLE");
if (appleHandler == null) {
throw new CheckedException("未启用Apple登录");
}
String openid = appleHandler.identify(identityToken);
UserInfo userInfo = appleHandler.info(openid);
String ticket = IdUtil.simpleUUID();
redisTemplate.opsForValue().set(socialTicketKey("APPLE", ticket), openid, WX_TICKET_TTL_MINUTES, TimeUnit.MINUTES);
WxSocialResolveVO vo = new WxSocialResolveVO();
vo.setBound(userInfo != null);
vo.setTicket(ticket);
return vo;
}
/**
* 根据入参查询用户信息
*
@@ -1196,6 +1196,8 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
wrapper = Wrappers.<SysUser>lambdaUpdate().set(SysUser::getWxOpenid, null).eq(SysUser::getUserId, user.getId());
} else if (type.equals("wechat_pc")) {
wrapper = Wrappers.<SysUser>lambdaUpdate().set(SysUser::getWxPcOpenid, null).eq(SysUser::getUserId, user.getId());
} else if (type.equals("apple")) {
wrapper = Wrappers.<SysUser>lambdaUpdate().set(SysUser::getAppleOpenid, null).eq(SysUser::getUserId, user.getId());
} else if (type.equals("gitee")) {
wrapper = Wrappers.<SysUser>lambdaUpdate().set(SysUser::getGiteeLogin, null).eq(SysUser::getUserId, user.getId());
} else if (type.equals("osc")) {
@@ -34,6 +34,7 @@
<result column="avatar" property="avatar"/>
<result column="wx_openid" property="wxOpenid"/>
<result column="wx_pc_openid" property="wxPcOpenid"/>
<result column="apple_openid" property="appleOpenid"/>
<result column="qq_openid" property="qqOpenid"/>
<result column="gitee_login" property="giteeOpenId"/>
<result column="osc_id" property="oscOpenId"/>
@@ -75,6 +76,7 @@
u.avatar,
u.wx_openid,
u.wx_pc_openid,
u.apple_openid,
u.qq_openid,
u.gitee_login,
u.osc_id,
@@ -110,6 +112,7 @@
u.avatar,
u.wx_openid,
u.wx_pc_openid,
u.apple_openid,
u.qq_openid,
u.gitee_login,
u.osc_id,
@@ -167,6 +170,7 @@
u.avatar,
u.wx_openid,
u.wx_pc_openid,
u.apple_openid,
u.qq_openid,
u.gitee_login,
u.osc_id,
@@ -268,6 +272,7 @@
u.avatar,
u.wx_openid,
u.wx_pc_openid,
u.apple_openid,
u.qq_openid,
u.gitee_login,
u.osc_id,