feat(wechat): 重写 code2Session 实现 5 步防御性处理
解决问题: - 微信API返回 text/plain 时 JSONObject.parseObject 抛异常导致用户500 - 网络异常时 RestClientException 未被捕获 - 业务错误码(errcode != 0)未处理 实现 5 步防御: 1. 网络层:捕获 ResourceAccessException / RestClientException -> 502 2. HTTP 状态:手动检查 is2xxSuccessful(因 WechatResponseErrorHandler 让 4xx/5xx 不抛)-> 502 3. Content-Type:检查 isCompatibleWith(APPLICATION_JSON) -> 502 4. JSON 解析:try/catch 包裹 parseObject -> 500 5. 业务码:检查 errcode == 0 -> 400 TDD:先写 10 测试场景覆盖所有分支,再实现 - 正常 JSON 成功 - HTTP 4xx / 5xx - Content-Type: text/plain / text/html - JSON 解析失败 - 业务错误码 40029 / 40163 - 网络异常(ResourceAccessException) - 响应缺少 session_key
This commit is contained in:
+110
@@ -0,0 +1,110 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.emotion.config.WechatMiniProgramProperties;
|
||||
import com.emotion.dto.wechat.WechatCodeSessionResponse;
|
||||
import com.emotion.exception.BusinessException;
|
||||
import com.emotion.exception.WechatApiException;
|
||||
import com.emotion.service.WechatMiniProgramService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WechatMiniProgramServiceImpl implements WechatMiniProgramService {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final WechatMiniProgramProperties properties;
|
||||
|
||||
@Override
|
||||
public WechatCodeSessionResponse code2Session(String code) {
|
||||
validateConfigured();
|
||||
|
||||
String url = UriComponentsBuilder.fromHttpUrl(properties.getBaseUrl() + "/sns/jscode2session")
|
||||
.queryParam("appid", properties.getAppId())
|
||||
.queryParam("secret", properties.getAppSecret())
|
||||
.queryParam("js_code", code)
|
||||
.queryParam("grant_type", "authorization_code")
|
||||
.toUriString();
|
||||
|
||||
ResponseEntity<String> responseEntity;
|
||||
try {
|
||||
responseEntity = restTemplate.getForEntity(url, String.class);
|
||||
} catch (ResourceAccessException e) {
|
||||
// 网络层异常(连接超时、连接拒绝、DNS 失败)
|
||||
log.error("[WeChatAPI] 网络异常,jsCode={}", code, e);
|
||||
throw new WechatApiException(502, "微信API网络异常: " + e.getMessage(), null);
|
||||
} catch (RestClientException e) {
|
||||
// 其他 RestTemplate 异常(如 SSL、URI 解析)
|
||||
log.error("[WeChatAPI] 客户端异常,jsCode={}", code, e);
|
||||
throw new WechatApiException(502, "微信API客户端异常: " + e.getMessage(), null);
|
||||
}
|
||||
|
||||
// Step 2: HTTP 状态码检查(4xx/5xx 不会抛异常,因为 WechatResponseErrorHandler 处理了)
|
||||
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
|
||||
log.error("[WeChatAPI] HTTP 非 2xx 响应,status={}", responseEntity.getStatusCode());
|
||||
throw new WechatApiException(502,
|
||||
"微信API返回非2xx状态: " + responseEntity.getStatusCode(), responseEntity.getBody());
|
||||
}
|
||||
|
||||
// Step 3: Content-Type 检查(防御 WAF 返回 text/plain 拦截页)
|
||||
MediaType contentType = responseEntity.getHeaders().getContentType();
|
||||
if (contentType == null || !MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {
|
||||
log.error("[WeChatAPI] Content-Type 不是 JSON,contentType={}, body={}",
|
||||
contentType, responseEntity.getBody());
|
||||
throw new WechatApiException(502,
|
||||
"微信API返回非JSON Content-Type: " + contentType, responseEntity.getBody());
|
||||
}
|
||||
|
||||
// Step 4: JSON 解析
|
||||
JSONObject result;
|
||||
try {
|
||||
result = JSONObject.parseObject(responseEntity.getBody());
|
||||
} catch (Exception e) {
|
||||
log.error("[WeChatAPI] JSON 解析失败,body={}", responseEntity.getBody(), e);
|
||||
throw new WechatApiException(500, "微信API响应JSON解析失败: " + e.getMessage(), responseEntity.getBody());
|
||||
}
|
||||
|
||||
// Step 5: 业务错误码检查
|
||||
int errcode = result.getIntValue("errcode");
|
||||
if (errcode != 0) {
|
||||
String errmsg = result.getString("errmsg");
|
||||
log.error("[WeChatAPI] 业务错误,errcode={}, errmsg={}", errcode, errmsg);
|
||||
throw new WechatApiException(400,
|
||||
"微信API业务错误,errcode=" + errcode + ", errmsg=" + errmsg, responseEntity.getBody());
|
||||
}
|
||||
|
||||
// 成功:提取 openid 和 session_key
|
||||
String openid = result.getString("openid");
|
||||
String sessionKey = result.getString("session_key");
|
||||
if (openid == null || sessionKey == null) {
|
||||
log.error("[WeChatAPI] 响应缺少必要字段 (openid 或 session_key)");
|
||||
throw new WechatApiException(502,
|
||||
"微信API响应缺少 openid 或 session_key", responseEntity.getBody());
|
||||
}
|
||||
|
||||
WechatCodeSessionResponse response = new WechatCodeSessionResponse();
|
||||
response.setOpenid(openid);
|
||||
response.setSessionKey(sessionKey);
|
||||
response.setUnionid(result.getString("unionid"));
|
||||
return response;
|
||||
}
|
||||
|
||||
private void validateConfigured() {
|
||||
if (!StringUtils.hasText(properties.getAppId()) || !StringUtils.hasText(properties.getAppSecret())) {
|
||||
throw new BusinessException("微信小程序登录未配置");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user