Bläddra i källkod

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

YunaiV 1 år sedan
förälder
incheckning
6f757e5297
13 ändrade filer med 216 tillägg och 46 borttagningar
  1. 5 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.http
  2. 16 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java
  3. 31 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/vo/AuthWeixinJsapiSignatureRespVO.java
  4. 3 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/convert/auth/AuthConvert.java
  5. 10 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApi.java
  6. 34 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxJsapiSignatureRespDTO.java
  7. 9 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java
  8. 0 4
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/package-info.java
  9. 0 4
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/weixin/AppWxMpController.http
  10. 0 38
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/weixin/AppWxMpController.java
  11. 15 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/social/SocialClientConvert.java
  12. 10 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java
  13. 83 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java

+ 5 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.http

@@ -59,3 +59,8 @@ tenant-id: {{appTenentId}}
 POST {{appApi}}/member/auth/refresh-token?refreshToken=bc43d929094849a28b3a69f6e6940d70
 Content-Type: application/json
 tenant-id: {{appTenentId}}
+
+### 请求 /auth/create-weixin-jsapi-signature 接口 => 成功
+POST {{appApi}}/member/auth/create-weixin-jsapi-signature?url=http://www.iocoder.cn
+Authorization: Bearer {{appToken}}
+tenant-id: {{appTenentId}}

+ 16 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java

@@ -1,12 +1,16 @@
 package cn.iocoder.yudao.module.member.controller.app.auth;
 
 import cn.hutool.core.util.StrUtil;
+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;
 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
 import cn.iocoder.yudao.module.member.controller.app.auth.vo.*;
+import cn.iocoder.yudao.module.member.convert.auth.AuthConvert;
 import cn.iocoder.yudao.module.member.service.auth.MemberAuthService;
+import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.Parameters;
@@ -33,6 +37,9 @@ public class AppAuthController {
     @Resource
     private MemberAuthService authService;
 
+    @Resource
+    private SocialClientApi socialClientApi;
+
     @Resource
     private SecurityProperties securityProperties;
 
@@ -109,4 +116,13 @@ public class AppAuthController {
         return success(authService.weixinMiniAppLogin(reqVO));
     }
 
+    @PostMapping("/create-weixin-jsapi-signature")
+    @Operation(summary = "创建微信 JS SDK 初始化所需的签名",
+            description = "参考 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html 文档")
+    public CommonResult<SocialWxJsapiSignatureRespDTO> createWeixinMpJsapiSignature(@RequestParam("url") String url) {
+        SocialWxJsapiSignatureRespDTO signature = socialClientApi.createWxMpJsapiSignature(
+                UserTypeEnum.MEMBER.getValue(), url);
+        return success(AuthConvert.INSTANCE.convert(signature));
+    }
+
 }

+ 31 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/vo/AuthWeixinJsapiSignatureRespVO.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.member.controller.app.auth.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Schema(description = "用户 APP - 微信公众号 JSAPI 签名 Response VO")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class AuthWeixinJsapiSignatureRespVO {
+
+    @Schema(description = "微信公众号的 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "hello")
+    private String appId;
+
+    @Schema(description = "匿名串", requiredMode = Schema.RequiredMode.REQUIRED, example = "world")
+    private String nonceStr;
+
+    @Schema(description = "时间戳", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long timestamp;
+
+    @Schema(description = "URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn")
+    private String url;
+
+    @Schema(description = "签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "阿巴阿巴")
+    private String signature;
+
+}

+ 3 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/convert/auth/AuthConvert.java

@@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
 import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeValidateReqDTO;
 import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
 import cn.iocoder.yudao.module.system.api.social.dto.SocialUserUnbindReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO;
 import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
@@ -29,4 +30,6 @@ public interface AuthConvert {
 
     SmsCodeValidateReqDTO convert(AppAuthSmsValidateReqVO bean);
 
+    SocialWxJsapiSignatureRespDTO convert(SocialWxJsapiSignatureRespDTO bean);
+
 }

+ 10 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApi.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.system.api.social;
 
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO;
 import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
 
 /**
@@ -19,4 +20,13 @@ public interface SocialClientApi {
      */
     String getAuthorizeUrl(Integer type, Integer userType, String redirectUri);
 
+    /**
+     * 创建微信 JS SDK 初始化所需的签名
+     *
+     * @param userType 用户类型
+     * @param url 访问的 URL 地址
+     * @return 签名
+     */
+    SocialWxJsapiSignatureRespDTO createWxMpJsapiSignature(Integer userType, String url);
+
 }

+ 34 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxJsapiSignatureRespDTO.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.system.api.social.dto;
+
+import lombok.Data;
+
+/**
+ * 微信公众号 JSAPI 签名 Response DTO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class SocialWxJsapiSignatureRespDTO {
+
+    /**
+     * 微信公众号的 appId
+     */
+    private String appId;
+    /**
+     * 匿名串
+     */
+    private String nonceStr;
+    /**
+     * 时间戳
+     */
+    private Long timestamp;
+    /**
+     * URL
+     */
+    private String url;
+    /**
+     * 签名
+     */
+    private String signature;
+
+}

+ 9 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java

@@ -1,6 +1,9 @@
 package cn.iocoder.yudao.module.system.api.social;
 
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO;
+import cn.iocoder.yudao.module.system.convert.social.SocialClientConvert;
 import cn.iocoder.yudao.module.system.service.social.SocialClientService;
+import me.chanjar.weixin.common.bean.WxJsapiSignature;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
@@ -23,4 +26,10 @@ public class SocialClientApiImpl implements SocialClientApi {
         return socialClientService.getAuthorizeUrl(type, userType, redirectUri);
     }
 
+    @Override
+    public SocialWxJsapiSignatureRespDTO createWxMpJsapiSignature(Integer userType, String url) {
+        WxJsapiSignature signature = socialClientService.createWxMpJsapiSignature(userType, url);
+        return SocialClientConvert.INSTANCE.convert(signature);
+    }
+
 }

+ 0 - 4
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/package-info.java

@@ -1,4 +0,0 @@
-/**
- * 占位,避免 package 无法提交到 Git 仓库
- */
-package cn.iocoder.yudao.module.system.controller.app;

+ 0 - 4
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/weixin/AppWxMpController.http

@@ -1,4 +0,0 @@
-### 请求 /login 接口 => 成功
-POST {{appApi}}/system/wx-mp/create-jsapi-signature?url=http://www.iocoder.cn
-Authorization: Bearer {{appToken}}
-tenant-id: {{appTenentId}}

+ 0 - 38
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/weixin/AppWxMpController.java

@@ -1,38 +0,0 @@
-package cn.iocoder.yudao.module.system.controller.app.weixin;
-
-import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import io.swagger.v3.oas.annotations.Operation;
-import lombok.extern.slf4j.Slf4j;
-import me.chanjar.weixin.common.bean.WxJsapiSignature;
-import me.chanjar.weixin.common.error.WxErrorException;
-import me.chanjar.weixin.mp.api.WxMpService;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-
-import javax.annotation.Resource;
-
-import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-
-@Tag(name = "微信公众号")
-@RestController
-@RequestMapping("/system/wx-mp")
-@Validated
-@Slf4j
-public class AppWxMpController {
-
-    @Resource
-    private WxMpService mpService;
-
-    // TODO @芋艿:需要额外考虑个问题;多租户下,如果每个小程序一个微信公众号,则会存在多个 appid;
-    @PostMapping("/create-jsapi-signature")
-    @Operation(summary = "创建微信 JS SDK 初始化所需的签名",
-        description = "参考 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html 文档")
-    public CommonResult<WxJsapiSignature> createJsapiSignature(@RequestParam("url") String url) throws WxErrorException {
-        return success(mpService.createJsapiSignature(url));
-    }
-
-}

+ 15 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/social/SocialClientConvert.java

@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.module.system.convert.social;
+
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO;
+import me.chanjar.weixin.common.bean.WxJsapiSignature;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface SocialClientConvert {
+
+    SocialClientConvert INSTANCE = Mappers.getMapper(SocialClientConvert.class);
+
+    SocialWxJsapiSignatureRespDTO convert(WxJsapiSignature bean);
+
+}

+ 10 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.service.social;
 
 import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
 import com.xingyuv.jushauth.model.AuthUser;
+import me.chanjar.weixin.common.bean.WxJsapiSignature;
 
 /**
  * 社交应用 Service 接口
@@ -31,4 +32,13 @@ public interface SocialClientService {
      */
     AuthUser getAuthUser(Integer socialType, Integer userType, String code, String state);
 
+    /**
+     * 创建微信 JS SDK 初始化所需的签名
+     *
+     * @param userType 用户类型
+     * @param url 访问的 URL 地址
+     * @return 签名
+     */
+    WxJsapiSignature createWxMpJsapiSignature(Integer userType, String url);
+
 }

+ 83 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java

@@ -4,21 +4,33 @@ 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.cache.CacheUtils;
 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.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
 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.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.bean.WxJsapiSignature;
+import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
+import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
+import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
+import java.time.Duration;
 import java.util.Objects;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -37,6 +49,32 @@ public class SocialClientServiceImpl implements SocialClientService {
     @Resource // 由于自定义了 YudaoAuthRequestFactory 无法覆盖默认的 AuthRequestFactory,所以只能注入它
     private YudaoAuthRequestFactory yudaoAuthRequestFactory;
 
+    @Resource
+    private WxMpService mpService;
+    @Resource
+    private StringRedisTemplate stringRedisTemplate; // WxMpService 需要使用到,所以在 Service 注入了它
+    @Resource
+    private WxMpProperties mpProperties;
+    /**
+     * 缓存 WxMpService 对象
+     *
+     * key:使用微信公众号的 appId + secret 拼接,即 {@link SocialClientDO} 的 clientId 和 clientSecret 属性。
+     * 为什么 key 使用这种格式?因为 {@link SocialClientDO} 在管理后台可以变更,通过这个 key 存储它的单例。
+     *
+     * 为什么要做 WxMpService 缓存?因为 WxMpService 构建成本比较大,所以尽量保证它是单例。
+     */
+    private final LoadingCache<String, WxMpService> mpServiceCache = CacheUtils.buildAsyncReloadingCache(
+            Duration.ofSeconds(10L),
+            new CacheLoader<String, WxMpService>() {
+
+                @Override
+                public WxMpService load(String key) {
+                    String[] keys = key.split(":");
+                    return buildMpService(keys[0], keys[1]);
+                }
+
+            });
+
     @Resource
     private SocialClientMapper socialClientMapper;
 
@@ -91,4 +129,49 @@ public class SocialClientServiceImpl implements SocialClientService {
         return request;
     }
 
+    @Override
+    @SneakyThrows
+    public WxJsapiSignature createWxMpJsapiSignature(Integer userType, String url) {
+        WxMpService mpService = buildMpService(userType);
+        return mpService.createJsapiSignature(url);
+    }
+
+    /**
+     * 创建 clientId + clientSecret 对应的 WxMpService 对象
+     *
+     * @param userType 用户类型
+     * @return WxMpService 对象
+     */
+    private WxMpService buildMpService(Integer userType) {
+        // 第一步,查询 DB 的配置项,获得对应的 WxMpService 对象
+        SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType(
+                SocialTypeEnum.WECHAT_MP.getType(), userType);
+        if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
+            return mpServiceCache.getUnchecked(client.getClientId() + ":" + client.getClientSecret());
+        }
+        // 第二步,不存在 DB 配置项,则使用 application-*.yaml 对应的 WxMpService 对象
+        return mpService;
+    }
+
+    /**
+     * 创建 clientId + clientSecret 对应的 WxMpService 对象
+     *
+     * @param clientId 微信公众号 appId
+     * @param clientSecret 微信公众号 secret
+     * @return WxMpService 对象
+     */
+    private WxMpService buildMpService(String clientId, String clientSecret) {
+        // 第一步,创建 WxMpRedisConfigImpl 对象
+        WxMpRedisConfigImpl configStorage = new WxMpRedisConfigImpl(
+                new RedisTemplateWxRedisOps(stringRedisTemplate),
+                mpProperties.getConfigStorage().getKeyPrefix());
+        configStorage.setAppId(clientId);
+        configStorage.setSecret(clientSecret);
+
+        // 第二步,创建 WxMpService 对象
+        WxMpService service = new WxMpServiceImpl();
+        service.setWxMpConfigStorage(configStorage);
+        return service;
+    }
+
 }