From 39878e0aa53633c9fb85120c3cbb8a88839c8d0c Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Mon, 22 Jun 2026 17:32:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E5=8F=B0=E7=99=BB=E5=BD=95=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E4=BA=8C=E6=AC=A1=E8=AE=A4=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../support/PcWechatPasswordLoginGate.java | 120 +++++++++++ .../support/PcWechatSecurityProperties.java | 36 ++++ ...sourceOwnerBaseAuthenticationProvider.java | 15 ++ .../main/resources/application-pc-wechat.yml | 6 + .../src/main/resources/application.yml | 1 + .../common/core/constant/CacheConstants.java | 15 ++ .../core/constant/enums/LoginTypeEnum.java | 7 +- .../WechatBindRequiredException.java | 21 ++ .../WechatConfirmRequiredException.java | 21 ++ .../security/util/OAuth2ErrorCodesExpand.java | 6 + .../bsd/pjcloud/admin/api/entity/SysUser.java | 6 + .../admin/api/vo/SysSocialDetailsVO.java | 7 + .../com/bsd/pjcloud/admin/api/vo/UserVO.java | 6 + .../SysSocialDetailsController.java | 30 +++ .../admin/handler/WeChatPcLoginHandler.java | 78 +++++++ .../admin/handler/WxPcTicketLoginHandler.java | 62 ++++++ .../admin/service/PcWechatSocialService.java | 25 +++ .../impl/PcWechatSocialServiceImpl.java | 203 ++++++++++++++++++ .../impl/SysSocialDetailsServiceImpl.java | 34 ++- .../service/impl/SysUserServiceImpl.java | 2 + .../main/resources/mapper/SysUserMapper.xml | 5 + 21 files changed, 701 insertions(+), 5 deletions(-) create mode 100644 pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/PcWechatPasswordLoginGate.java create mode 100644 pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/PcWechatSecurityProperties.java create mode 100644 pjcloud-auth/src/main/resources/application-pc-wechat.yml create mode 100644 pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/exception/WechatBindRequiredException.java create mode 100644 pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/exception/WechatConfirmRequiredException.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WeChatPcLoginHandler.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WxPcTicketLoginHandler.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/PcWechatSocialService.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/PcWechatSocialServiceImpl.java diff --git a/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/PcWechatPasswordLoginGate.java b/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/PcWechatPasswordLoginGate.java new file mode 100644 index 00000000..54eded18 --- /dev/null +++ b/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/PcWechatPasswordLoginGate.java @@ -0,0 +1,120 @@ +package com.bsd.pjcloud.auth.support; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.bsd.pjcloud.admin.api.dto.UserInfo; +import com.bsd.pjcloud.admin.api.feign.RemoteUserService; +import com.bsd.pjcloud.common.core.constant.CacheConstants; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.common.core.util.RetOps; +import com.bsd.pjcloud.common.data.resolver.ParamResolver; +import com.bsd.pjcloud.common.security.exception.WechatBindRequiredException; +import com.bsd.pjcloud.common.security.exception.WechatConfirmRequiredException; +import com.bsd.pjcloud.common.security.service.PjcloudUser; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * PC 后台(pig 客户端)密码登录:可选绑定 / 扫码确认 PC 微信 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class PcWechatPasswordLoginGate { + + private static final String PC_PASSWORD_CLIENT_ID = "pig"; + + private static final String PARAM_BIND_REQUIRED = "PC_WECHAT_BIND_REQUIRED"; + + private static final String PARAM_CONFIRM_REQUIRED = "PC_WECHAT_CONFIRM_REQUIRED"; + + private static final long TICKET_TTL_MINUTES = 15; + + private final RemoteUserService remoteUserService; + + private final StringRedisTemplate redisTemplate; + + private final PcWechatSecurityProperties pcWechatSecurityProperties; + + public void check(RegisteredClient registeredClient, Authentication authentication) { + if (registeredClient == null || !PC_PASSWORD_CLIENT_ID.equals(registeredClient.getClientId())) { + return; + } + if (!isBindRequired() && !isConfirmRequired()) { + return; + } + Object principal = authentication.getPrincipal(); + if (!(principal instanceof PjcloudUser pjcloudUser)) { + return; + } + R userResult = remoteUserService.getInnerByUserId(pjcloudUser.getId()); + UserInfo userInfo = RetOps.of(userResult).getData().orElse(null); + if (userInfo == null || userInfo.getSysUser() == null) { + return; + } + String wxPcOpenid = userInfo.getSysUser().getWxPcOpenid(); + String ticket = IdUtil.simpleUUID(); + if (StrUtil.isBlank(wxPcOpenid)) { + if (!isBindRequired() && !isConfirmRequired()) { + return; + } + // 未绑定:只要开启了绑定或扫码确认,均须先扫码绑定 + redisTemplate.opsForValue().set(pwdVerifiedKey(ticket), String.valueOf(pjcloudUser.getId()), TICKET_TTL_MINUTES, + TimeUnit.MINUTES); + throw new WechatBindRequiredException(ticket); + } + if (!isConfirmRequired()) { + return; + } + redisTemplate.opsForValue().set(loginConfirmKey(ticket), String.valueOf(pjcloudUser.getId()), TICKET_TTL_MINUTES, + TimeUnit.MINUTES); + throw new WechatConfirmRequiredException(ticket); + } + + private boolean isBindRequired() { + return resolveFlag(PARAM_BIND_REQUIRED, pcWechatSecurityProperties.isBindRequired()); + } + + private boolean isConfirmRequired() { + return resolveFlag(PARAM_CONFIRM_REQUIRED, pcWechatSecurityProperties.isConfirmRequired()); + } + + private boolean resolveFlag(String paramKey, boolean defaultValue) { + try { + String value = ParamResolver.getStr(paramKey); + if (StrUtil.isNotBlank(value)) { + return parseBool(value); + } + } + catch (Exception e) { + log.debug("读取公共参数 {} 失败,使用配置文件默认值", paramKey); + } + return defaultValue; + } + + private boolean parseBool(String value) { + if ("1".equals(value) || "Y".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value)) { + return true; + } + if ("0".equals(value) || "N".equalsIgnoreCase(value) || "no".equalsIgnoreCase(value)) { + return false; + } + return BooleanUtil.toBoolean(value); + } + + private String pwdVerifiedKey(String ticket) { + return CacheConstants.PC_PWD_VERIFIED + ticket; + } + + private String loginConfirmKey(String ticket) { + return CacheConstants.PC_LOGIN_CONFIRM + ticket; + } + +} diff --git a/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/PcWechatSecurityProperties.java b/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/PcWechatSecurityProperties.java new file mode 100644 index 00000000..a82298be --- /dev/null +++ b/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/PcWechatSecurityProperties.java @@ -0,0 +1,36 @@ +package com.bsd.pjcloud.auth.support; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.stereotype.Component; + +/** + * PC 后台密码登录微信校验(开发/生产可关) + *

+ * 配置示例(pjcloud-auth application.yml 或 Nacos): + *

+ * security:
+ *   pc-wechat:
+ *     bind-required: false
+ *     confirm-required: false
+ * 
+ * 也可在「参数管理」覆盖:PC_WECHAT_BIND_REQUIRED、PC_WECHAT_CONFIRM_REQUIRED(true/false 或 1/0) + */ +@Data +@Component +@RefreshScope +@ConfigurationProperties("security.pc-wechat") +public class PcWechatSecurityProperties { + + /** + * 密码登录后未绑定 PC 微信时是否拦截(须先扫码绑定) + */ + private boolean bindRequired = false; + + /** + * 已绑定 PC 微信时,密码登录是否须微信扫码确认 + */ + private boolean confirmRequired = false; + +} diff --git a/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/base/OAuth2ResourceOwnerBaseAuthenticationProvider.java b/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/base/OAuth2ResourceOwnerBaseAuthenticationProvider.java index 359ac085..afb4a3e2 100644 --- a/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/base/OAuth2ResourceOwnerBaseAuthenticationProvider.java +++ b/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/base/OAuth2ResourceOwnerBaseAuthenticationProvider.java @@ -2,7 +2,10 @@ package com.bsd.pjcloud.auth.support.base; import cn.hutool.core.util.BooleanUtil; import cn.hutool.extra.spring.SpringUtil; +import com.bsd.pjcloud.auth.support.PcWechatPasswordLoginGate; import com.bsd.pjcloud.common.core.constant.CommonConstants; +import com.bsd.pjcloud.common.security.exception.WechatBindRequiredException; +import com.bsd.pjcloud.common.security.exception.WechatConfirmRequiredException; import com.bsd.pjcloud.common.security.service.PjcloudRedisOAuth2AuthorizationService; import com.bsd.pjcloud.common.security.util.OAuth2ErrorCodesExpand; import com.bsd.pjcloud.common.security.util.ScopeException; @@ -148,6 +151,10 @@ public abstract class OAuth2ResourceOwnerBaseAuthenticationProvider implements Serializable { @Schema(description = "微信openid") private String wxOpenid; + /** + * PC 后台微信 openid(网站应用) + */ + @Schema(description = "PC微信openid") + private String wxPcOpenid; + /** * 企微微信 userid */ diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/SysSocialDetailsVO.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/SysSocialDetailsVO.java index 73561d75..00a53ef9 100644 --- a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/SysSocialDetailsVO.java +++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/SysSocialDetailsVO.java @@ -29,4 +29,11 @@ public class SysSocialDetailsVO { */ @Schema(description = "扩展字段") private String ext; + + /** + * 授权回调地址(PC 微信 qrconnect 使用,不含 #) + */ + @Schema(description = "回调地址") + private String redirectUrl; + } 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 9ef93898..b0e5fd8b 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 @@ -91,6 +91,12 @@ public class UserVO implements BaseFacesInterface, Serializable { @Schema(description = "微信open id") private String wxOpenid; + /** + * PC 微信 openid + */ + @Schema(description = "PC微信open id") + private String wxPcOpenid; + /** * 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 4c903343..b035f286 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 @@ -22,6 +22,7 @@ import cn.hutool.core.util.ArrayUtil; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.admin.api.entity.SysSocialDetails; +import com.bsd.pjcloud.admin.service.PcWechatSocialService; import com.bsd.pjcloud.admin.service.SysSocialDetailsService; import com.bsd.pjcloud.admin.api.vo.WxSocialResolveVO; import com.bsd.pjcloud.common.core.util.R; @@ -55,6 +56,8 @@ public class SysSocialDetailsController { private final SysSocialDetailsService sysSocialDetailsService; + private final PcWechatSocialService pcWechatSocialService; + /** * 社交登录账户简单分页查询 * @param page 分页对象 @@ -168,6 +171,33 @@ public class SysSocialDetailsController { return R.ok(sysSocialDetailsService.bindSocialByTicket(state, ticket)); } + /** + * PC 微信:解析 code,返回绑定状态与 ticket + */ + @Inner(value = false) + @PostMapping("/pc/wx/resolve") + public R resolveWxPcCode(String code) { + return R.ok(pcWechatSocialService.resolveWxPcCode(code)); + } + + /** + * PC 密码已校验、未绑定:扫码绑定并登录 + */ + @Inner(value = false) + @PostMapping("/pc/bind-by-pwd-ticket") + public R> bindPcByPwdTicket(String pwdTicket, String code) { + return R.ok(pcWechatSocialService.bindByPwdTicket(pwdTicket, code)); + } + + /** + * PC 密码已校验、已绑定:扫码确认登录 + */ + @Inner(value = false) + @PostMapping("/pc/confirm-login") + public R> confirmPcLogin(String ticket, String code) { + return R.ok(pcWechatSocialService.confirmLogin(ticket, code)); + } + /** * 导出 */ diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WeChatPcLoginHandler.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WeChatPcLoginHandler.java new file mode 100644 index 00000000..9304e13b --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WeChatPcLoginHandler.java @@ -0,0 +1,78 @@ +/* + * PC 后台微信登录(网站应用 qrconnect,与移动端 WX 分离) + */ +package com.bsd.pjcloud.admin.handler; + +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +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.mapper.SysSocialDetailsMapper; +import com.bsd.pjcloud.admin.service.SysUserService; +import com.bsd.pjcloud.common.core.constant.SecurityConstants; +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; + +@Slf4j +@Component("WX_PC") +@AllArgsConstructor +public class WeChatPcLoginHandler extends AbstractLoginHandler { + + private final SysUserService sysUserService; + + private final SysSocialDetailsMapper sysSocialDetailsMapper; + + @Override + public String identify(String code) { + SysSocialDetails condition = new SysSocialDetails(); + condition.setType(LoginTypeEnum.WECHAT_PC.getType()); + SysSocialDetails socialDetails = sysSocialDetailsMapper.selectOne(new QueryWrapper<>(condition)); + if (socialDetails == null) { + throw new CheckedException("未配置 PC 微信登录,请在后台添加 type=WX_PC 的网站应用"); + } + + String url = String.format(SecurityConstants.WX_AUTHORIZATION_CODE_URL, socialDetails.getAppId(), + socialDetails.getAppSecret(), code); + String result = HttpUtil.get(url); + log.debug("PC微信响应报文:{}", result); + + JSONObject resultJsonObj = JSONUtil.parseObj(result); + if (resultJsonObj.containsKey("errcode")) { + log.error("PC微信授权失败:{}", result); + String errMsg = resultJsonObj.getStr("errmsg", "微信授权失败"); + throw new CheckedException("微信授权失败:" + errMsg); + } + + Object obj = resultJsonObj.get("openid"); + if (obj == null) { + log.error("PC微信响应缺少openid:{}", result); + throw new CheckedException("微信授权失败,未获取到用户标识"); + } + return obj.toString(); + } + + @Override + public UserInfo info(String openId) { + SysUser user = sysUserService.getOne(Wrappers.query().lambda().eq(SysUser::getWxPcOpenid, openId)); + if (user == null) { + log.info("PC微信未绑定:{}", openId); + return null; + } + return sysUserService.findUserInfo(user); + } + + @Override + public Boolean bind(SysUser user, String identify) { + user.setWxPcOpenid(identify); + sysUserService.updateById(user); + return true; + } + +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WxPcTicketLoginHandler.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WxPcTicketLoginHandler.java new file mode 100644 index 00000000..992b3188 --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WxPcTicketLoginHandler.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; + +/** + * PC 微信 ticket 登录(mobile=WX_PC_TICKET@{ticket}) + */ +@Slf4j +@Component("WX_PC_TICKET") +@RequiredArgsConstructor +public class WxPcTicketLoginHandler implements LoginHandler { + + private final StringRedisTemplate redisTemplate; + + private final SysUserService sysUserService; + + private String ticketKey(String ticket) { + return CacheConstants.WX_PC_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("微信授权已过期,请重新授权"); + } + redisTemplate.delete(ticketKey(ticket)); + return openid; + } + + @Override + public UserInfo info(String openid) { + SysUser user = sysUserService.getOne(Wrappers.lambdaQuery().eq(SysUser::getWxPcOpenid, openid)); + if (user == null) { + log.info("PC微信未绑定:{}", openid); + 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/service/PcWechatSocialService.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/PcWechatSocialService.java new file mode 100644 index 00000000..646491c6 --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/PcWechatSocialService.java @@ -0,0 +1,25 @@ +package com.bsd.pjcloud.admin.service; + +import java.util.Map; + +/** + * PC 后台微信:密码登录后的绑定 / 扫码确认 + */ +public interface PcWechatSocialService { + + /** + * 密码已校验、未绑定 PC 微信:扫码绑定并登录 + */ + Map bindByPwdTicket(String pwdTicket, String code); + + /** + * 密码已校验、已绑定:扫码确认后登录 + */ + Map confirmLogin(String confirmTicket, String code); + + /** + * 解析 PC 微信 code,返回绑定状态与 ticket + */ + com.bsd.pjcloud.admin.api.vo.WxSocialResolveVO resolveWxPcCode(String code); + +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/PcWechatSocialServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/PcWechatSocialServiceImpl.java new file mode 100644 index 00000000..887f87d2 --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/PcWechatSocialServiceImpl.java @@ -0,0 +1,203 @@ +package com.bsd.pjcloud.admin.service.impl; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.IdUtil; +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.dto.UserTokenDTO; +import com.bsd.pjcloud.admin.api.entity.SysUser; +import com.bsd.pjcloud.admin.api.feign.RemoteTokenService; +import com.bsd.pjcloud.admin.api.vo.WxSocialResolveVO; +import com.bsd.pjcloud.admin.handler.LoginHandler; +import com.bsd.pjcloud.admin.service.PcWechatSocialService; +import com.bsd.pjcloud.admin.service.SysUserService; +import com.bsd.pjcloud.common.core.constant.CacheConstants; +import com.bsd.pjcloud.common.core.constant.SecurityConstants; +import com.bsd.pjcloud.common.core.exception.CheckedException; +import com.bsd.pjcloud.common.core.util.R; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.CacheManager; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PcWechatSocialServiceImpl implements PcWechatSocialService { + + private static final long TICKET_TTL_MINUTES = 15; + + private final Map loginHandlerMap; + + private final StringRedisTemplate redisTemplate; + + private final SysUserService sysUserService; + + private final RemoteTokenService remoteTokenService; + + private final CacheManager cacheManager; + + private String wxPcTicketKey(String ticket) { + return CacheConstants.WX_PC_SOCIAL_TICKET + ticket; + } + + private String pwdVerifiedKey(String ticket) { + return CacheConstants.PC_PWD_VERIFIED + ticket; + } + + private String loginConfirmKey(String ticket) { + return CacheConstants.PC_LOGIN_CONFIRM + ticket; + } + + @Override + public WxSocialResolveVO resolveWxPcCode(String code) { + LoginHandler wxPcHandler = loginHandlerMap.get("WX_PC"); + String openid = wxPcHandler.identify(code); + UserInfo userInfo = wxPcHandler.info(openid); + String ticket = IdUtil.simpleUUID(); + redisTemplate.opsForValue().set(wxPcTicketKey(ticket), openid, TICKET_TTL_MINUTES, TimeUnit.MINUTES); + WxSocialResolveVO vo = new WxSocialResolveVO(); + vo.setBound(userInfo != null); + vo.setTicket(ticket); + return vo; + } + + @Override + public Map bindByPwdTicket(String pwdTicket, String code) { + String userIdStr = redisTemplate.opsForValue().get(pwdVerifiedKey(pwdTicket)); + if (StrUtil.isBlank(userIdStr)) { + throw new CheckedException("登录已过期,请重新输入账号密码"); + } + LoginHandler wxPcHandler = loginHandlerMap.get("WX_PC"); + String openid = wxPcHandler.identify(code); + UserInfo existing = wxPcHandler.info(openid); + if (existing != null && !existing.getSysUser().getUserId().equals(Long.valueOf(userIdStr))) { + throw new CheckedException("该微信已绑定其他账号"); + } + SysUser user = sysUserService.getById(Long.valueOf(userIdStr)); + if (user == null) { + throw new CheckedException("用户不存在"); + } + wxPcHandler.bind(user, openid); + cacheManager.getCache(CacheConstants.USER_DETAILS).evict(user.getUsername()); + redisTemplate.delete(pwdVerifiedKey(pwdTicket)); + return issueToken(user); + } + + @Override + public Map confirmLogin(String confirmTicket, String code) { + String userIdStr = redisTemplate.opsForValue().get(loginConfirmKey(confirmTicket)); + if (StrUtil.isBlank(userIdStr)) { + throw new CheckedException("登录已过期,请重新输入账号密码"); + } + SysUser user = sysUserService.getById(Long.valueOf(userIdStr)); + if (user == null) { + throw new CheckedException("用户不存在"); + } + if (StrUtil.isBlank(user.getWxPcOpenid())) { + throw new CheckedException("未绑定 PC 微信,请先绑定"); + } + LoginHandler wxPcHandler = loginHandlerMap.get("WX_PC"); + String openid = wxPcHandler.identify(code); + if (!StrUtil.equals(openid, user.getWxPcOpenid())) { + throw new CheckedException("微信账号与绑定不一致,请使用已绑定的微信扫码"); + } + redisTemplate.delete(loginConfirmKey(confirmTicket)); + return issueToken(user); + } + + private Map issueToken(SysUser user) { + UserInfo userInfo = sysUserService.findUserInfo(user); + UserTokenDTO userTokenDTO = new UserTokenDTO(); + userTokenDTO.setSysUser(userInfo.getSysUser()); + userTokenDTO.setUsername(user.getUsername()); + userTokenDTO.setAuthorities(buildTokenAuthorities(userInfo)); + R tokenResult = remoteTokenService.generateToken(userTokenDTO); + if (tokenResult == null || tokenResult.getCode() != 0 || tokenResult.getData() == null) { + log.error("PC微信签发 token 失败: {}", tokenResult); + throw new CheckedException("签发登录令牌失败"); + } + Object data = tokenResult.getData(); + if (data instanceof OAuth2AccessTokenAuthenticationToken authToken) { + OAuth2AccessToken accessToken = authToken.getAccessToken(); + OAuth2RefreshToken refreshToken = authToken.getRefreshToken(); + Map map = new HashMap<>(2); + map.put("access_token", accessToken.getTokenValue()); + if (refreshToken != null) { + map.put("refresh_token", refreshToken.getTokenValue()); + } + return map; + } + if (data instanceof Map map) { + String accessToken = extractTokenValue(map.get("accessToken")); + if (StrUtil.isBlank(accessToken)) { + accessToken = extractTokenValue(map.get("access_token")); + } + String refreshToken = extractTokenValue(map.get("refreshToken")); + if (StrUtil.isBlank(refreshToken)) { + refreshToken = extractTokenValue(map.get("refresh_token")); + } + if (StrUtil.isBlank(accessToken)) { + log.error("PC微信签发 token 解析失败, data={}", map); + throw new CheckedException("签发登录令牌失败"); + } + Map result = new HashMap<>(2); + result.put("access_token", accessToken); + if (StrUtil.isNotBlank(refreshToken)) { + result.put("refresh_token", refreshToken); + } + return result; + } + log.error("PC微信签发 token 未知数据类型: {}", data.getClass()); + throw new CheckedException("签发登录令牌失败"); + } + + /** 与 PjcloudUserDetailsService 一致:角色 ROLE_{id} + 权限标识,否则菜单接口 getRoleIds 为空 */ + private List buildTokenAuthorities(UserInfo userInfo) { + Set authSet = new HashSet<>(); + if (ArrayUtil.isNotEmpty(userInfo.getRoles())) { + Arrays.stream(userInfo.getRoles()).forEach(roleId -> authSet.add(SecurityConstants.ROLE + roleId)); + } + if (ArrayUtil.isNotEmpty(userInfo.getPermissions())) { + authSet.addAll(Arrays.asList(userInfo.getPermissions())); + } + return new ArrayList<>(authSet); + } + + /** Feign 反序列化 OAuth2AccessToken 常为 Map,需取 tokenValue */ + private String extractTokenValue(Object tokenObj) { + if (tokenObj == null) { + return null; + } + if (tokenObj instanceof OAuth2AccessToken accessToken) { + return accessToken.getTokenValue(); + } + if (tokenObj instanceof OAuth2RefreshToken refreshToken) { + return refreshToken.getTokenValue(); + } + if (tokenObj instanceof Map tokenMap) { + Object tokenValue = tokenMap.get("tokenValue"); + if (tokenValue == null) { + tokenValue = tokenMap.get("value"); + } + return tokenValue != null ? String.valueOf(tokenValue) : null; + } + String str = String.valueOf(tokenObj); + return StrUtil.isBlank(str) || str.startsWith("{") ? null : str; + } + +} 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 bedb9f03..a7b63f5e 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,10 +66,17 @@ public class SysSocialDetailsServiceImpl extends ServiceImpl { SysSocialDetailsVO newSysSocialDetails = new SysSocialDetailsVO(); BeanUtils.copyProperties(sysSocialDetails, newSysSocialDetails); + if ("WX_PC".equals(sysSocialDetails.getType())) { + newSysSocialDetails.setRedirectUrl(normalizeWxPcRedirectUrl(sysSocialDetails.getRedirectUrl())); + } return newSysSocialDetails; }).collect(Collectors.toList()); } + + /** PC 微信 qrconnect:redirect_uri 不能含 # */ + private static String normalizeWxPcRedirectUrl(String redirectUrl) { + if (StrUtil.isBlank(redirectUrl)) { + return redirectUrl; + } + String url = redirectUrl.trim(); + int hashIdx = url.indexOf('#'); + if (hashIdx >= 0) { + url = url.substring(0, hashIdx); + } + while (url.endsWith("/")) { + url = url.substring(0, url.length() - 1); + } + return url; + } + } diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java index 55505f7c..b63c72b9 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java @@ -907,6 +907,8 @@ public class SysUserServiceImpl extends ServiceImpl impl LambdaUpdateWrapper wrapper = null; if (type.equals("wechat")) { 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("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 a20dcc14..625362b4 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 @@ -33,6 +33,7 @@ + @@ -73,6 +74,7 @@ u.gender, u.avatar, u.wx_openid, + u.wx_pc_openid, u.qq_openid, u.gitee_login, u.osc_id, @@ -107,6 +109,7 @@ u.gender, u.avatar, u.wx_openid, + u.wx_pc_openid, u.qq_openid, u.gitee_login, u.osc_id, @@ -163,6 +166,7 @@ u.gender, u.avatar, u.wx_openid, + u.wx_pc_openid, u.qq_openid, u.gitee_login, u.osc_id, @@ -263,6 +267,7 @@ u.phone, u.avatar, u.wx_openid, + u.wx_pc_openid, u.qq_openid, u.gitee_login, u.osc_id,