From a28234b1084981d71d9cb550c764650063e1b01d Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Fri, 7 Aug 2026 11:49:25 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9D=83=E9=99=90sql?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/mapper/CanteenDailyMenuMapper.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml b/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml index 593026e89..e20481f91 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml @@ -47,7 +47,9 @@ t.hour_type, sp.price, sp.category_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 From a023c5c52b7ae846d278a12bf910e7deb66cd988 Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Fri, 7 Aug 2026 17:07:06 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E8=8F=9C=E5=93=81=E6=9D=83=E9=99=90?= =?UTF-8?q?=EF=BC=8Capple=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CanteenCategoryController.java | 15 +- .../controller/CanteenSpuController.java | 3 +- .../impl/CanteenCategoryServiceImpl.java | 8 +- .../service/impl/CanteenSpuServiceImpl.java | 9 ++ .../util/CanteenMerchantScopeHelper.java | 152 ++++++++++++++++++ .../common/core/constant/CacheConstants.java | 5 + .../core/constant/enums/LoginTypeEnum.java | 12 +- .../bsd/pjcloud/admin/api/entity/SysUser.java | 6 + .../com/bsd/pjcloud/admin/api/vo/UserVO.java | 6 + .../SysSocialDetailsController.java | 9 ++ .../admin/handler/AppleLoginHandler.java | 59 +++++++ .../handler/AppleTicketLoginHandler.java | 62 +++++++ .../apple/AppleIdentityTokenVerifier.java | 124 ++++++++++++++ .../service/SysSocialDetailsService.java | 5 + .../impl/SysSocialDetailsServiceImpl.java | 29 +++- .../service/impl/SysUserServiceImpl.java | 2 + .../main/resources/mapper/SysUserMapper.xml | 5 + 17 files changed, 497 insertions(+), 14 deletions(-) create mode 100644 pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/util/CanteenMerchantScopeHelper.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/AppleLoginHandler.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/AppleTicketLoginHandler.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/apple/AppleIdentityTokenVerifier.java diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenCategoryController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenCategoryController.java index 2b0f25316..96072b71f 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenCategoryController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenCategoryController.java @@ -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 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> 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 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 list = canteenCategoryService.list(wrapper); + wrapper.apply(StringUtils.isNotEmpty(categoryType), "FIND_IN_SET({0}, category_type)", categoryType); + List list = canteenCategoryService.listScope(wrapper); return R.ok(list); } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenSpuController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenSpuController.java index 0f7adc135..77ea3e24c 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenSpuController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenSpuController.java @@ -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); diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenCategoryServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenCategoryServiceImpl.java index 2e6df1cef..25bfcde82 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenCategoryServiceImpl.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenCategoryServiceImpl.java @@ -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 selectPageByScope(Page page, LambdaQueryWrapper wrapper) { return baseMapper.selectPageByScope(page, wrapper, DataScope.of()); @@ -39,6 +42,7 @@ public class CanteenCategoryServiceImpl extends ServiceImpl wrapper) { @@ -125,6 +127,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl 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 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 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 canteenOnlineSpuEntityLambdaQueryWrapper = new LambdaQueryWrapper<>(); canteenOnlineSpuEntityLambdaQueryWrapper.isNotNull(CanteenOnlineSpuEntity::getLinkSpuId); + merchantScopeHelper.applyMerchantIdScope(canteenOnlineSpuEntityLambdaQueryWrapper, canteenSpu.getMerchantId(), + CanteenOnlineSpuEntity::getMerchantId); List canteenOnlineSpuEntities = canteenOnlineSpuMapper.selectListByScope(canteenOnlineSpuEntityLambdaQueryWrapper, DataScope.of()); Set spuIds = canteenOnlineSpuEntities.stream().map(CanteenOnlineSpuEntity::getLinkSpuId).collect(Collectors.toSet()); @@ -288,6 +295,7 @@ public class CanteenSpuServiceImpl extends ServiceImpllambdaQuery() .eq(CanteenSpuComboItemEntity::getComboSpuId, entity.getId())); diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/util/CanteenMerchantScopeHelper.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/util/CanteenMerchantScopeHelper.java new file mode 100644 index 000000000..477e29351 --- /dev/null +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/util/CanteenMerchantScopeHelper.java @@ -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 roleIds = SecurityUtils.getRoleIds(); + if (CollUtil.isEmpty(roleIds)) { + return false; + } + List 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 resolveAllowedMerchantIds() { + PjcloudUser user = SecurityUtils.getUser(); + if (user == null || user.getDeptId() == null) { + return Collections.emptyList(); + } + Long rootDeptId = user.getDeptId(); + List deptIds; + R> 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> 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 void applyMerchantIdScope(LambdaQueryWrapper wrapper, Long queryMerchantId, + SFunction merchantIdColumn) { + if (canViewAllMerchants()) { + if (queryMerchantId != null) { + wrapper.eq(merchantIdColumn, queryMerchantId); + } + return; + } + List 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 deptIdSetter) { + if (merchantId == null || deptIdSetter == null) { + return; + } + try { + R 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); + } + } + +} diff --git a/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/CacheConstants.java b/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/CacheConstants.java index 6bdb9227f..de3784cad 100644 --- a/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/CacheConstants.java +++ b/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/CacheConstants.java @@ -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) */ diff --git a/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/enums/LoginTypeEnum.java b/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/enums/LoginTypeEnum.java index 8fb9cfc18..ad117d5ba 100644 --- a/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/enums/LoginTypeEnum.java +++ b/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/enums/LoginTypeEnum.java @@ -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登录"); /** * 类型 diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SysUser.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SysUser.java index 111abe946..df8354aa9 100644 --- a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SysUser.java +++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SysUser.java @@ -194,6 +194,12 @@ public class SysUser extends Model implements Serializable { @Schema(description = "PC微信openid") private String wxPcOpenid; + /** + * Apple 登录唯一标识(identityToken.sub) + */ + @Schema(description = "Apple openid") + private String appleOpenid; + /** * 企微微信 userid */ diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/UserVO.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/UserVO.java index e7bb7254b..172a44b77 100644 --- a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/UserVO.java +++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/UserVO.java @@ -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 */ diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysSocialDetailsController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysSocialDetailsController.java index 309adecb4..99ef55e1f 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysSocialDetailsController.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysSocialDetailsController.java @@ -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 resolveAppleIdentityToken(String identityToken) { + return R.ok(sysSocialDetailsService.resolveAppleIdentityToken(identityToken)); + } + /** * App 微信:使用 ticket 绑定(需已登录) */ diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/AppleLoginHandler.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/AppleLoginHandler.java new file mode 100644 index 000000000..2d7b913da --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/AppleLoginHandler.java @@ -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.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; + } + +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/AppleTicketLoginHandler.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/AppleTicketLoginHandler.java new file mode 100644 index 000000000..7f760850f --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/AppleTicketLoginHandler.java @@ -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.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); + } + +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/apple/AppleIdentityTokenVerifier.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/apple/AppleIdentityTokenVerifier.java new file mode 100644 index 000000000..0b0fde49c --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/apple/AppleIdentityTokenVerifier.java @@ -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 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 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) { + } + +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysSocialDetailsService.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysSocialDetailsService.java index a74155834..31272dff1 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysSocialDetailsService.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysSocialDetailsService.java @@ -51,6 +51,11 @@ public interface SysSocialDetailsService extends IService { */ WxSocialResolveVO resolveWxCode(String code); + /** + * 解析 Apple identityToken(仅校验一次),返回是否已绑定及 ticket + */ + WxSocialResolveVO resolveAppleIdentityToken(String identityToken); + /** * 根据入参查询用户信息 * @param inStr diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysSocialDetailsServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysSocialDetailsServiceImpl.java index a7b63f5e8..7bd8ef2c9 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysSocialDetailsServiceImpl.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysSocialDetailsServiceImpl.java @@ -66,15 +66,22 @@ public class SysSocialDetailsServiceImpl extends ServiceImpl impl wrapper = Wrappers.lambdaUpdate().set(SysUser::getWxOpenid, null).eq(SysUser::getUserId, user.getId()); } else if (type.equals("wechat_pc")) { wrapper = Wrappers.lambdaUpdate().set(SysUser::getWxPcOpenid, null).eq(SysUser::getUserId, user.getId()); + } else if (type.equals("apple")) { + wrapper = Wrappers.lambdaUpdate().set(SysUser::getAppleOpenid, null).eq(SysUser::getUserId, user.getId()); } else if (type.equals("gitee")) { wrapper = Wrappers.lambdaUpdate().set(SysUser::getGiteeLogin, null).eq(SysUser::getUserId, user.getId()); } else if (type.equals("osc")) { diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml b/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml index f0afa0509..45feeaad8 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml @@ -34,6 +34,7 @@ + @@ -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,