|
@@ -1,5 +1,6 @@
|
|
|
package com.qqflow.engine.domain.ewechat.service;
|
|
package com.qqflow.engine.domain.ewechat.service;
|
|
|
|
|
|
|
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
import com.qqflow.engine.common.exception.BusinessException;
|
|
import com.qqflow.engine.common.exception.BusinessException;
|
|
|
import com.qqflow.engine.common.util.JwtUtils;
|
|
import com.qqflow.engine.common.util.JwtUtils;
|
|
|
import com.qqflow.engine.common.util.RedisCache;
|
|
import com.qqflow.engine.common.util.RedisCache;
|
|
@@ -13,7 +14,9 @@ import com.qqflow.engine.domain.system.entity.SysUser;
|
|
|
import com.qqflow.engine.domain.system.mapper.SysDeptMapper;
|
|
import com.qqflow.engine.domain.system.mapper.SysDeptMapper;
|
|
|
import com.qqflow.engine.domain.system.mapper.SysUserMapper;
|
|
import com.qqflow.engine.domain.system.mapper.SysUserMapper;
|
|
|
import com.qqflow.engine.domain.system.service.SysUserService;
|
|
import com.qqflow.engine.domain.system.service.SysUserService;
|
|
|
|
|
+import lombok.Getter;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.Setter;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
@@ -33,7 +36,7 @@ import static com.qqflow.engine.common.constant.SecurityConstants.USER_TYPE_SYST
|
|
|
*
|
|
*
|
|
|
* <p>流程:
|
|
* <p>流程:
|
|
|
* <ol>
|
|
* <ol>
|
|
|
- * <li>H5 调 {@link #buildAuthorizeUrl(String)} 对应的接口 → 302 到企微 snsapi_base 静默授权;</li>
|
|
|
|
|
|
|
+ * <li>H5 调 {@link #buildAuthorizeUrl(String, String)} 对应的接口 → 302 到企微 snsapi_base 静默授权;</li>
|
|
|
* <li>企微回调 H5,带 code + state;</li>
|
|
* <li>企微回调 H5,带 code + state;</li>
|
|
|
* <li>H5 调 {@link #exchange(String, String)}:校验一次性 state → code 换 userid →
|
|
* <li>H5 调 {@link #exchange(String, String)}:校验一次性 state → code 换 userid →
|
|
|
* 匹配 sys_user.wecom_user_id → 签发与 PC 端同款的 JWT。</li>
|
|
* 匹配 sys_user.wecom_user_id → 签发与 PC 端同款的 JWT。</li>
|
|
@@ -66,6 +69,7 @@ public class WecomOAuthService {
|
|
|
private final SysUserService sysUserService;
|
|
private final SysUserService sysUserService;
|
|
|
private final JwtUtils jwtUtils;
|
|
private final JwtUtils jwtUtils;
|
|
|
private final RedisCache redisCache;
|
|
private final RedisCache redisCache;
|
|
|
|
|
+ private final ObjectMapper objectMapper;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 允许的 H5 地址前缀(逗号分隔;第一个同时作为审批消息 textcard 的跳转地址)
|
|
* 允许的 H5 地址前缀(逗号分隔;第一个同时作为审批消息 textcard 的跳转地址)
|
|
@@ -73,23 +77,47 @@ public class WecomOAuthService {
|
|
|
@Value("${wecom.h5-base-url:}")
|
|
@Value("${wecom.h5-base-url:}")
|
|
|
private String h5BaseUrlConfig;
|
|
private String h5BaseUrlConfig;
|
|
|
|
|
|
|
|
|
|
+ /** state 中携带的内容:redirect + 用户所属企业(多企业免登按企业取 appid/token) */
|
|
|
|
|
+ @Getter
|
|
|
|
|
+ @Setter
|
|
|
|
|
+ static class OAuthState {
|
|
|
|
|
+ private String redirect;
|
|
|
|
|
+ private String corpId;
|
|
|
|
|
+
|
|
|
|
|
+ OAuthState() {
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ OAuthState(String redirect, String corpId) {
|
|
|
|
|
+ this.redirect = redirect;
|
|
|
|
|
+ this.corpId = corpId;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 生成企微静默授权跳转地址(带一次性 state)
|
|
* 生成企微静默授权跳转地址(带一次性 state)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param corpId 用户所属企业(可选;为空回退默认授权企业,兼容存量 H5 入口)
|
|
|
*/
|
|
*/
|
|
|
- public String buildAuthorizeUrl(String redirect) {
|
|
|
|
|
|
|
+ public String buildAuthorizeUrl(String redirect, String corpId) {
|
|
|
validateRedirect(redirect);
|
|
validateRedirect(redirect);
|
|
|
- EnterpriseWechatCorp corp = wechatMsgService.getAuthorizedCorp();
|
|
|
|
|
|
|
+ EnterpriseWechatCorp corp = wechatMsgService.getCorpById(corpId);
|
|
|
if (corp == null) {
|
|
if (corp == null) {
|
|
|
throw new BusinessException("企业微信未授权,请联系管理员");
|
|
throw new BusinessException("企业微信未授权,请联系管理员");
|
|
|
}
|
|
}
|
|
|
String state = UUID.randomUUID().toString().replace("-", "");
|
|
String state = UUID.randomUUID().toString().replace("-", "");
|
|
|
- redisCache.setCacheObject(STATE_KEY_PREFIX + state, redirect, STATE_TTL_MINUTES, TimeUnit.MINUTES);
|
|
|
|
|
- return AUTHORIZE_URL
|
|
|
|
|
- + "?appid=" + corp.getCorpId()
|
|
|
|
|
- + "&redirect_uri=" + URLEncoder.encode(redirect, StandardCharsets.UTF_8)
|
|
|
|
|
- + "&response_type=code&scope=snsapi_base"
|
|
|
|
|
- + "&state=" + state
|
|
|
|
|
- + "#wechat_redirect";
|
|
|
|
|
|
|
+ // state 携带 corpId:企微回跳只带 code+state,exchange 据此按企业取 token 换 userinfo
|
|
|
|
|
+ redisCache.setCacheObject(STATE_KEY_PREFIX + state, writeState(new OAuthState(redirect, corp.getCorpId())),
|
|
|
|
|
+ STATE_TTL_MINUTES, TimeUnit.MINUTES);
|
|
|
|
|
+ StringBuilder url = new StringBuilder(AUTHORIZE_URL)
|
|
|
|
|
+ .append("?appid=").append(corp.getCorpId())
|
|
|
|
|
+ .append("&redirect_uri=").append(URLEncoder.encode(redirect, StandardCharsets.UTF_8))
|
|
|
|
|
+ .append("&response_type=code&scope=snsapi_base")
|
|
|
|
|
+ .append("&state=").append(state);
|
|
|
|
|
+ Integer agentId = wechatMsgService.resolveAgentId(corp);
|
|
|
|
|
+ if (agentId != null) {
|
|
|
|
|
+ url.append("&agentid=").append(agentId);
|
|
|
|
|
+ }
|
|
|
|
|
+ return url.append("#wechat_redirect").toString();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -104,9 +132,16 @@ public class WecomOAuthService {
|
|
|
if (!StringUtils.hasText(state) || redisCache.getCacheObject(stateKey) == null) {
|
|
if (!StringUtils.hasText(state) || redisCache.getCacheObject(stateKey) == null) {
|
|
|
throw new BusinessException(40002, "授权状态已失效,请重新进入");
|
|
throw new BusinessException(40002, "授权状态已失效,请重新进入");
|
|
|
}
|
|
}
|
|
|
|
|
+ String stateValue = redisCache.getCacheObject(stateKey);
|
|
|
redisCache.deleteObject(stateKey);
|
|
redisCache.deleteObject(stateKey);
|
|
|
|
|
|
|
|
- String accessToken = wechatMsgService.getCorpAccessToken();
|
|
|
|
|
|
|
+ OAuthState oauthState = readState(stateValue);
|
|
|
|
|
+ // 按 state 里的企业取 access_token(多企业部署),未带 corpId 的存量 state 回退默认企业
|
|
|
|
|
+ EnterpriseWechatCorp corp = wechatMsgService.getCorpById(oauthState.getCorpId());
|
|
|
|
|
+ if (corp == null) {
|
|
|
|
|
+ throw new BusinessException("企业微信未授权,请联系管理员");
|
|
|
|
|
+ }
|
|
|
|
|
+ String accessToken = wechatMsgService.getCorpAccessToken(corp);
|
|
|
if (accessToken == null) {
|
|
if (accessToken == null) {
|
|
|
throw new BusinessException("企业微信未授权,请联系管理员");
|
|
throw new BusinessException("企业微信未授权,请联系管理员");
|
|
|
}
|
|
}
|
|
@@ -114,7 +149,15 @@ public class WecomOAuthService {
|
|
|
if (userInfo == null || !StringUtils.hasText(userInfo.getUserid())) {
|
|
if (userInfo == null || !StringUtils.hasText(userInfo.getUserid())) {
|
|
|
throw new BusinessException(40001, "该企微账号未绑定审批系统账号");
|
|
throw new BusinessException(40001, "该企微账号未绑定审批系统账号");
|
|
|
}
|
|
}
|
|
|
- SysUser user = sysUserMapper.selectByWecomUserId(userInfo.getUserid());
|
|
|
|
|
|
|
+ // 优先按企业精确匹配(多企业下 userid 可能重复),匹配不到回退全局匹配
|
|
|
|
|
+ // (兼容 corp_id 未回填的存量用户)
|
|
|
|
|
+ SysUser user = null;
|
|
|
|
|
+ if (StringUtils.hasText(oauthState.getCorpId())) {
|
|
|
|
|
+ user = sysUserMapper.selectByWecomUserIdAndCorpId(userInfo.getUserid(), oauthState.getCorpId());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (user == null) {
|
|
|
|
|
+ user = sysUserMapper.selectByWecomUserId(userInfo.getUserid());
|
|
|
|
|
+ }
|
|
|
if (user == null || (user.getStatus() != null && user.getStatus() != 0)) {
|
|
if (user == null || (user.getStatus() != null && user.getStatus() != 0)) {
|
|
|
throw new BusinessException(40001, "该企微账号未绑定审批系统账号");
|
|
throw new BusinessException(40001, "该企微账号未绑定审批系统账号");
|
|
|
}
|
|
}
|
|
@@ -147,6 +190,28 @@ public class WecomOAuthService {
|
|
|
return loginUser;
|
|
return loginUser;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** state 序列化(JSON:redirect + corpId) */
|
|
|
|
|
+ private String writeState(OAuthState oauthState) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return objectMapper.writeValueAsString(oauthState);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("[企微免登] state 序列化失败,降级为仅存 redirect", e);
|
|
|
|
|
+ return oauthState.getRedirect();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** state 反序列化;解析失败按旧版兼容处理(旧 state 只存 redirect,无 corpId) */
|
|
|
|
|
+ private OAuthState readState(String raw) {
|
|
|
|
|
+ if (!StringUtils.hasText(raw)) {
|
|
|
|
|
+ return new OAuthState(null, null);
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return objectMapper.readValue(raw, OAuthState.class);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ return new OAuthState(raw, null);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* redirect 必须命中配置的 H5 地址前缀(逗号分隔,支持多环境),防开放重定向
|
|
* redirect 必须命中配置的 H5 地址前缀(逗号分隔,支持多环境),防开放重定向
|
|
|
*/
|
|
*/
|