Commit 6f757e5297d7e77f9906b7265cb3545ddb41a016
1 parent
d2562750
saas:支持社交应用的多租户配置(mp)
Showing
13 changed files
with
216 additions
and
46 deletions
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}} |
| 59 | 59 | POST {{appApi}}/member/auth/refresh-token?refreshToken=bc43d929094849a28b3a69f6e6940d70 |
| 60 | 60 | Content-Type: application/json |
| 61 | 61 | tenant-id: {{appTenentId}} |
| 62 | + | |
| 63 | +### 请求 /auth/create-weixin-jsapi-signature 接口 => 成功 | |
| 64 | +POST {{appApi}}/member/auth/create-weixin-jsapi-signature?url=http://www.iocoder.cn | |
| 65 | +Authorization: Bearer {{appToken}} | |
| 66 | +tenant-id: {{appTenentId}} | ... | ... |
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java
| 1 | 1 | package cn.iocoder.yudao.module.member.controller.app.auth; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.util.StrUtil; |
| 4 | +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | |
| 4 | 5 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 5 | 6 | import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; |
| 6 | 7 | import cn.iocoder.yudao.framework.security.config.SecurityProperties; |
| 7 | 8 | import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; |
| 8 | 9 | import cn.iocoder.yudao.module.member.controller.app.auth.vo.*; |
| 10 | +import cn.iocoder.yudao.module.member.convert.auth.AuthConvert; | |
| 9 | 11 | import cn.iocoder.yudao.module.member.service.auth.MemberAuthService; |
| 12 | +import cn.iocoder.yudao.module.system.api.social.SocialClientApi; | |
| 13 | +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO; | |
| 10 | 14 | import io.swagger.v3.oas.annotations.Operation; |
| 11 | 15 | import io.swagger.v3.oas.annotations.Parameter; |
| 12 | 16 | import io.swagger.v3.oas.annotations.Parameters; |
| ... | ... | @@ -34,6 +38,9 @@ public class AppAuthController { |
| 34 | 38 | private MemberAuthService authService; |
| 35 | 39 | |
| 36 | 40 | @Resource |
| 41 | + private SocialClientApi socialClientApi; | |
| 42 | + | |
| 43 | + @Resource | |
| 37 | 44 | private SecurityProperties securityProperties; |
| 38 | 45 | |
| 39 | 46 | @PostMapping("/login") |
| ... | ... | @@ -109,4 +116,13 @@ public class AppAuthController { |
| 109 | 116 | return success(authService.weixinMiniAppLogin(reqVO)); |
| 110 | 117 | } |
| 111 | 118 | |
| 119 | + @PostMapping("/create-weixin-jsapi-signature") | |
| 120 | + @Operation(summary = "创建微信 JS SDK 初始化所需的签名", | |
| 121 | + description = "参考 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html 文档") | |
| 122 | + public CommonResult<SocialWxJsapiSignatureRespDTO> createWeixinMpJsapiSignature(@RequestParam("url") String url) { | |
| 123 | + SocialWxJsapiSignatureRespDTO signature = socialClientApi.createWxMpJsapiSignature( | |
| 124 | + UserTypeEnum.MEMBER.getValue(), url); | |
| 125 | + return success(AuthConvert.INSTANCE.convert(signature)); | |
| 126 | + } | |
| 127 | + | |
| 112 | 128 | } | ... | ... |
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/vo/AuthWeixinJsapiSignatureRespVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.member.controller.app.auth.vo; | |
| 2 | + | |
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 4 | +import lombok.AllArgsConstructor; | |
| 5 | +import lombok.Builder; | |
| 6 | +import lombok.Data; | |
| 7 | +import lombok.NoArgsConstructor; | |
| 8 | + | |
| 9 | +@Schema(description = "用户 APP - 微信公众号 JSAPI 签名 Response VO") | |
| 10 | +@Data | |
| 11 | +@NoArgsConstructor | |
| 12 | +@AllArgsConstructor | |
| 13 | +@Builder | |
| 14 | +public class AuthWeixinJsapiSignatureRespVO { | |
| 15 | + | |
| 16 | + @Schema(description = "微信公众号的 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "hello") | |
| 17 | + private String appId; | |
| 18 | + | |
| 19 | + @Schema(description = "匿名串", requiredMode = Schema.RequiredMode.REQUIRED, example = "world") | |
| 20 | + private String nonceStr; | |
| 21 | + | |
| 22 | + @Schema(description = "时间戳", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") | |
| 23 | + private Long timestamp; | |
| 24 | + | |
| 25 | + @Schema(description = "URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn") | |
| 26 | + private String url; | |
| 27 | + | |
| 28 | + @Schema(description = "签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "阿巴阿巴") | |
| 29 | + private String signature; | |
| 30 | + | |
| 31 | +} | ... | ... |
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; |
| 9 | 9 | import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; |
| 10 | 10 | import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO; |
| 11 | 11 | import cn.iocoder.yudao.module.system.api.social.dto.SocialUserUnbindReqDTO; |
| 12 | +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO; | |
| 12 | 13 | import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; |
| 13 | 14 | import org.mapstruct.Mapper; |
| 14 | 15 | import org.mapstruct.factory.Mappers; |
| ... | ... | @@ -29,4 +30,6 @@ public interface AuthConvert { |
| 29 | 30 | |
| 30 | 31 | SmsCodeValidateReqDTO convert(AppAuthSmsValidateReqVO bean); |
| 31 | 32 | |
| 33 | + SocialWxJsapiSignatureRespDTO convert(SocialWxJsapiSignatureRespDTO bean); | |
| 34 | + | |
| 32 | 35 | } | ... | ... |
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApi.java
| 1 | 1 | package cn.iocoder.yudao.module.system.api.social; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO; | |
| 3 | 4 | import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; |
| 4 | 5 | |
| 5 | 6 | /** |
| ... | ... | @@ -19,4 +20,13 @@ public interface SocialClientApi { |
| 19 | 20 | */ |
| 20 | 21 | String getAuthorizeUrl(Integer type, Integer userType, String redirectUri); |
| 21 | 22 | |
| 23 | + /** | |
| 24 | + * 创建微信 JS SDK 初始化所需的签名 | |
| 25 | + * | |
| 26 | + * @param userType 用户类型 | |
| 27 | + * @param url 访问的 URL 地址 | |
| 28 | + * @return 签名 | |
| 29 | + */ | |
| 30 | + SocialWxJsapiSignatureRespDTO createWxMpJsapiSignature(Integer userType, String url); | |
| 31 | + | |
| 22 | 32 | } | ... | ... |
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxJsapiSignatureRespDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.system.api.social.dto; | |
| 2 | + | |
| 3 | +import lombok.Data; | |
| 4 | + | |
| 5 | +/** | |
| 6 | + * 微信公众号 JSAPI 签名 Response DTO | |
| 7 | + * | |
| 8 | + * @author 芋道源码 | |
| 9 | + */ | |
| 10 | +@Data | |
| 11 | +public class SocialWxJsapiSignatureRespDTO { | |
| 12 | + | |
| 13 | + /** | |
| 14 | + * 微信公众号的 appId | |
| 15 | + */ | |
| 16 | + private String appId; | |
| 17 | + /** | |
| 18 | + * 匿名串 | |
| 19 | + */ | |
| 20 | + private String nonceStr; | |
| 21 | + /** | |
| 22 | + * 时间戳 | |
| 23 | + */ | |
| 24 | + private Long timestamp; | |
| 25 | + /** | |
| 26 | + * URL | |
| 27 | + */ | |
| 28 | + private String url; | |
| 29 | + /** | |
| 30 | + * 签名 | |
| 31 | + */ | |
| 32 | + private String signature; | |
| 33 | + | |
| 34 | +} | ... | ... |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java
| 1 | 1 | package cn.iocoder.yudao.module.system.api.social; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO; | |
| 4 | +import cn.iocoder.yudao.module.system.convert.social.SocialClientConvert; | |
| 3 | 5 | import cn.iocoder.yudao.module.system.service.social.SocialClientService; |
| 6 | +import me.chanjar.weixin.common.bean.WxJsapiSignature; | |
| 4 | 7 | import org.springframework.stereotype.Service; |
| 5 | 8 | import org.springframework.validation.annotation.Validated; |
| 6 | 9 | |
| ... | ... | @@ -23,4 +26,10 @@ public class SocialClientApiImpl implements SocialClientApi { |
| 23 | 26 | return socialClientService.getAuthorizeUrl(type, userType, redirectUri); |
| 24 | 27 | } |
| 25 | 28 | |
| 29 | + @Override | |
| 30 | + public SocialWxJsapiSignatureRespDTO createWxMpJsapiSignature(Integer userType, String url) { | |
| 31 | + WxJsapiSignature signature = socialClientService.createWxMpJsapiSignature(userType, url); | |
| 32 | + return SocialClientConvert.INSTANCE.convert(signature); | |
| 33 | + } | |
| 34 | + | |
| 26 | 35 | } | ... | ... |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/package-info.java deleted
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/weixin/AppWxMpController.http deleted
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/weixin/AppWxMpController.java deleted
| 1 | -package cn.iocoder.yudao.module.system.controller.app.weixin; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 4 | -import io.swagger.v3.oas.annotations.tags.Tag; | |
| 5 | -import io.swagger.v3.oas.annotations.Operation; | |
| 6 | -import lombok.extern.slf4j.Slf4j; | |
| 7 | -import me.chanjar.weixin.common.bean.WxJsapiSignature; | |
| 8 | -import me.chanjar.weixin.common.error.WxErrorException; | |
| 9 | -import me.chanjar.weixin.mp.api.WxMpService; | |
| 10 | -import org.springframework.validation.annotation.Validated; | |
| 11 | -import org.springframework.web.bind.annotation.PostMapping; | |
| 12 | -import org.springframework.web.bind.annotation.RequestMapping; | |
| 13 | -import org.springframework.web.bind.annotation.RequestParam; | |
| 14 | -import org.springframework.web.bind.annotation.RestController; | |
| 15 | - | |
| 16 | -import javax.annotation.Resource; | |
| 17 | - | |
| 18 | -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | |
| 19 | - | |
| 20 | -@Tag(name = "微信公众号") | |
| 21 | -@RestController | |
| 22 | -@RequestMapping("/system/wx-mp") | |
| 23 | -@Validated | |
| 24 | -@Slf4j | |
| 25 | -public class AppWxMpController { | |
| 26 | - | |
| 27 | - @Resource | |
| 28 | - private WxMpService mpService; | |
| 29 | - | |
| 30 | - // TODO @芋艿:需要额外考虑个问题;多租户下,如果每个小程序一个微信公众号,则会存在多个 appid; | |
| 31 | - @PostMapping("/create-jsapi-signature") | |
| 32 | - @Operation(summary = "创建微信 JS SDK 初始化所需的签名", | |
| 33 | - description = "参考 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html 文档") | |
| 34 | - public CommonResult<WxJsapiSignature> createJsapiSignature(@RequestParam("url") String url) throws WxErrorException { | |
| 35 | - return success(mpService.createJsapiSignature(url)); | |
| 36 | - } | |
| 37 | - | |
| 38 | -} |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/social/SocialClientConvert.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.system.convert.social; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO; | |
| 4 | +import me.chanjar.weixin.common.bean.WxJsapiSignature; | |
| 5 | +import org.mapstruct.Mapper; | |
| 6 | +import org.mapstruct.factory.Mappers; | |
| 7 | + | |
| 8 | +@Mapper | |
| 9 | +public interface SocialClientConvert { | |
| 10 | + | |
| 11 | + SocialClientConvert INSTANCE = Mappers.getMapper(SocialClientConvert.class); | |
| 12 | + | |
| 13 | + SocialWxJsapiSignatureRespDTO convert(WxJsapiSignature bean); | |
| 14 | + | |
| 15 | +} | ... | ... |
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; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; |
| 4 | 4 | import com.xingyuv.jushauth.model.AuthUser; |
| 5 | +import me.chanjar.weixin.common.bean.WxJsapiSignature; | |
| 5 | 6 | |
| 6 | 7 | /** |
| 7 | 8 | * 社交应用 Service 接口 |
| ... | ... | @@ -31,4 +32,13 @@ public interface SocialClientService { |
| 31 | 32 | */ |
| 32 | 33 | AuthUser getAuthUser(Integer socialType, Integer userType, String code, String state); |
| 33 | 34 | |
| 35 | + /** | |
| 36 | + * 创建微信 JS SDK 初始化所需的签名 | |
| 37 | + * | |
| 38 | + * @param userType 用户类型 | |
| 39 | + * @param url 访问的 URL 地址 | |
| 40 | + * @return 签名 | |
| 41 | + */ | |
| 42 | + WxJsapiSignature createWxMpJsapiSignature(Integer userType, String url); | |
| 43 | + | |
| 34 | 44 | } | ... | ... |
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; |
| 4 | 4 | import cn.hutool.core.lang.Assert; |
| 5 | 5 | import cn.hutool.core.util.ReflectUtil; |
| 6 | 6 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 7 | +import cn.iocoder.yudao.framework.common.util.cache.CacheUtils; | |
| 7 | 8 | import cn.iocoder.yudao.framework.common.util.http.HttpUtils; |
| 8 | 9 | import cn.iocoder.yudao.framework.social.core.YudaoAuthRequestFactory; |
| 9 | 10 | import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO; |
| 10 | 11 | import cn.iocoder.yudao.module.system.dal.mysql.social.SocialClientMapper; |
| 11 | 12 | import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; |
| 13 | +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; | |
| 14 | +import com.google.common.cache.CacheLoader; | |
| 15 | +import com.google.common.cache.LoadingCache; | |
| 12 | 16 | import com.xingyuv.jushauth.config.AuthConfig; |
| 13 | 17 | import com.xingyuv.jushauth.model.AuthCallback; |
| 14 | 18 | import com.xingyuv.jushauth.model.AuthResponse; |
| 15 | 19 | import com.xingyuv.jushauth.model.AuthUser; |
| 16 | 20 | import com.xingyuv.jushauth.request.AuthRequest; |
| 17 | 21 | import com.xingyuv.jushauth.utils.AuthStateUtils; |
| 22 | +import lombok.SneakyThrows; | |
| 18 | 23 | import lombok.extern.slf4j.Slf4j; |
| 24 | +import me.chanjar.weixin.common.bean.WxJsapiSignature; | |
| 25 | +import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps; | |
| 26 | +import me.chanjar.weixin.mp.api.WxMpService; | |
| 27 | +import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl; | |
| 28 | +import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl; | |
| 29 | +import org.springframework.data.redis.core.StringRedisTemplate; | |
| 19 | 30 | import org.springframework.stereotype.Service; |
| 20 | 31 | |
| 21 | 32 | import javax.annotation.Resource; |
| 33 | +import java.time.Duration; | |
| 22 | 34 | import java.util.Objects; |
| 23 | 35 | |
| 24 | 36 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| ... | ... | @@ -38,6 +50,32 @@ public class SocialClientServiceImpl implements SocialClientService { |
| 38 | 50 | private YudaoAuthRequestFactory yudaoAuthRequestFactory; |
| 39 | 51 | |
| 40 | 52 | @Resource |
| 53 | + private WxMpService mpService; | |
| 54 | + @Resource | |
| 55 | + private StringRedisTemplate stringRedisTemplate; // WxMpService 需要使用到,所以在 Service 注入了它 | |
| 56 | + @Resource | |
| 57 | + private WxMpProperties mpProperties; | |
| 58 | + /** | |
| 59 | + * 缓存 WxMpService 对象 | |
| 60 | + * | |
| 61 | + * key:使用微信公众号的 appId + secret 拼接,即 {@link SocialClientDO} 的 clientId 和 clientSecret 属性。 | |
| 62 | + * 为什么 key 使用这种格式?因为 {@link SocialClientDO} 在管理后台可以变更,通过这个 key 存储它的单例。 | |
| 63 | + * | |
| 64 | + * 为什么要做 WxMpService 缓存?因为 WxMpService 构建成本比较大,所以尽量保证它是单例。 | |
| 65 | + */ | |
| 66 | + private final LoadingCache<String, WxMpService> mpServiceCache = CacheUtils.buildAsyncReloadingCache( | |
| 67 | + Duration.ofSeconds(10L), | |
| 68 | + new CacheLoader<String, WxMpService>() { | |
| 69 | + | |
| 70 | + @Override | |
| 71 | + public WxMpService load(String key) { | |
| 72 | + String[] keys = key.split(":"); | |
| 73 | + return buildMpService(keys[0], keys[1]); | |
| 74 | + } | |
| 75 | + | |
| 76 | + }); | |
| 77 | + | |
| 78 | + @Resource | |
| 41 | 79 | private SocialClientMapper socialClientMapper; |
| 42 | 80 | |
| 43 | 81 | @Override |
| ... | ... | @@ -91,4 +129,49 @@ public class SocialClientServiceImpl implements SocialClientService { |
| 91 | 129 | return request; |
| 92 | 130 | } |
| 93 | 131 | |
| 132 | + @Override | |
| 133 | + @SneakyThrows | |
| 134 | + public WxJsapiSignature createWxMpJsapiSignature(Integer userType, String url) { | |
| 135 | + WxMpService mpService = buildMpService(userType); | |
| 136 | + return mpService.createJsapiSignature(url); | |
| 137 | + } | |
| 138 | + | |
| 139 | + /** | |
| 140 | + * 创建 clientId + clientSecret 对应的 WxMpService 对象 | |
| 141 | + * | |
| 142 | + * @param userType 用户类型 | |
| 143 | + * @return WxMpService 对象 | |
| 144 | + */ | |
| 145 | + private WxMpService buildMpService(Integer userType) { | |
| 146 | + // 第一步,查询 DB 的配置项,获得对应的 WxMpService 对象 | |
| 147 | + SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType( | |
| 148 | + SocialTypeEnum.WECHAT_MP.getType(), userType); | |
| 149 | + if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { | |
| 150 | + return mpServiceCache.getUnchecked(client.getClientId() + ":" + client.getClientSecret()); | |
| 151 | + } | |
| 152 | + // 第二步,不存在 DB 配置项,则使用 application-*.yaml 对应的 WxMpService 对象 | |
| 153 | + return mpService; | |
| 154 | + } | |
| 155 | + | |
| 156 | + /** | |
| 157 | + * 创建 clientId + clientSecret 对应的 WxMpService 对象 | |
| 158 | + * | |
| 159 | + * @param clientId 微信公众号 appId | |
| 160 | + * @param clientSecret 微信公众号 secret | |
| 161 | + * @return WxMpService 对象 | |
| 162 | + */ | |
| 163 | + private WxMpService buildMpService(String clientId, String clientSecret) { | |
| 164 | + // 第一步,创建 WxMpRedisConfigImpl 对象 | |
| 165 | + WxMpRedisConfigImpl configStorage = new WxMpRedisConfigImpl( | |
| 166 | + new RedisTemplateWxRedisOps(stringRedisTemplate), | |
| 167 | + mpProperties.getConfigStorage().getKeyPrefix()); | |
| 168 | + configStorage.setAppId(clientId); | |
| 169 | + configStorage.setSecret(clientSecret); | |
| 170 | + | |
| 171 | + // 第二步,创建 WxMpService 对象 | |
| 172 | + WxMpService service = new WxMpServiceImpl(); | |
| 173 | + service.setWxMpConfigStorage(configStorage); | |
| 174 | + return service; | |
| 175 | + } | |
| 176 | + | |
| 94 | 177 | } | ... | ... |