|
|
@@ -0,0 +1,197 @@
|
|
|
+package cn.start.tz.module.pressure2.framework.appauth.core.service;
|
|
|
+
|
|
|
+import cn.hutool.core.util.IdUtil;
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
+import cn.hutool.crypto.digest.DigestUtil;
|
|
|
+import cn.start.tz.framework.common.pojo.CommonResult;
|
|
|
+import cn.start.tz.framework.common.util.json.JsonUtils;
|
|
|
+import cn.start.tz.framework.security.core.LoginUser;
|
|
|
+import cn.start.tz.module.member.api.user.MemberUserApi;
|
|
|
+import cn.start.tz.module.member.api.user.dto.MemberUserRespDTO;
|
|
|
+import cn.start.tz.module.member.api.user.dto.UseUnitRelationSaveReqVO;
|
|
|
+import cn.start.tz.module.pressure2.framework.appauth.core.context.AppAuthUserInfo;
|
|
|
+import cn.start.tz.module.system.api.clientunit.ClientUnitApi;
|
|
|
+import cn.start.tz.module.system.api.clientunit.dto.ClientUnitDTO;
|
|
|
+import com.fasterxml.jackson.core.type.TypeReference;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.context.annotation.Lazy;
|
|
|
+import org.springframework.data.redis.core.StringRedisTemplate;
|
|
|
+import org.springframework.http.*;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.web.client.RestTemplate;
|
|
|
+
|
|
|
+import jakarta.annotation.Resource;
|
|
|
+
|
|
|
+import java.time.Duration;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+import static cn.start.tz.framework.security.core.util.SecurityFrameworkUtils.getLoginUser;
|
|
|
+
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class AppAuthServiceImpl implements AppAuthService {
|
|
|
+
|
|
|
+ private static final int MAX_RETRY_TIMES = 3;
|
|
|
+ private static final long RETRY_INTERVAL_MS = 100;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * nonce Redis Key 模板: app_auth_nonce:{appId}:{nonce}
|
|
|
+ */
|
|
|
+ private static final String NONCE_KEY_FORMAT = "app_auth_nonce:%s:%s";
|
|
|
+
|
|
|
+ private static final TypeReference<CommonResult<AppAuthUserInfo>> AUTH_RESULT_TYPE =
|
|
|
+ new TypeReference<CommonResult<AppAuthUserInfo>>() {
|
|
|
+ };
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private RestTemplate restTemplate;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ @Lazy
|
|
|
+ private MemberUserApi memberUserApi;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ @Lazy
|
|
|
+ private ClientUnitApi clientUnitApi;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private StringRedisTemplate stringRedisTemplate;
|
|
|
+
|
|
|
+ @Value("${app-auth.base-url:http://localhost:48080}")
|
|
|
+ private String baseUrl;
|
|
|
+
|
|
|
+ @Value("${app-auth.enabled:true}")
|
|
|
+ private boolean enabled;
|
|
|
+
|
|
|
+ @Value("${app-auth.app-id:xxxxxx}")
|
|
|
+ private String appId;
|
|
|
+
|
|
|
+ @Value("${app-auth.app-secret:yyyyyy}")
|
|
|
+ private String appSecret;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public AppAuthUserInfo authenticate(String accessToken) {
|
|
|
+ if (StrUtil.isEmpty(accessToken)) {
|
|
|
+ log.warn("[authenticate][accessToken 为空,鉴权失败]");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!enabled) {
|
|
|
+ log.info("[authenticate][鉴权服务未启用,返回模拟用户]");
|
|
|
+ return buildMockUser(accessToken);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 最多重试3次,每次间隔100ms
|
|
|
+ for (int attempt = 1; attempt <= MAX_RETRY_TIMES; attempt++) {
|
|
|
+ try {
|
|
|
+ AppAuthUserInfo userInfo = doAuthenticate(accessToken);
|
|
|
+ if (userInfo != null) {
|
|
|
+ return userInfo;
|
|
|
+ }
|
|
|
+ log.warn("[authenticate][鉴权失败,第 {} 次尝试,共 {} 次]", attempt, MAX_RETRY_TIMES);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("[authenticate][鉴权异常,第 {} 次尝试,共 {} 次, accessToken={}]",
|
|
|
+ attempt, MAX_RETRY_TIMES, accessToken, e);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 未达到最大重试次数时,等待后重试
|
|
|
+ if (attempt < MAX_RETRY_TIMES) {
|
|
|
+ try {
|
|
|
+ Thread.sleep(RETRY_INTERVAL_MS);
|
|
|
+ } catch (InterruptedException ie) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ log.warn("[authenticate][重试等待被中断]");
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ log.error("[authenticate][鉴权失败,已达到最大重试次数 {} 次, accessToken={}]",
|
|
|
+ MAX_RETRY_TIMES, accessToken);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private AppAuthUserInfo buildMockUser(String accessToken) {
|
|
|
+ AppAuthUserInfo user = new AppAuthUserInfo();
|
|
|
+ LoginUser loginUser = getLoginUser();
|
|
|
+ if (loginUser != null) {
|
|
|
+ CommonResult<MemberUserRespDTO> userResult = memberUserApi.getUser(loginUser.getId());
|
|
|
+ if (userResult == null || !userResult.isSuccess() || userResult.getData() == null) {
|
|
|
+ log.warn("[buildMockUser][获取用户信息失败]");
|
|
|
+ return user;
|
|
|
+ }
|
|
|
+ MemberUserRespDTO checkedData = userResult.getData();
|
|
|
+
|
|
|
+ CommonResult<List<UseUnitRelationSaveReqVO>> relationResult = memberUserApi.getUseUnitRelation(checkedData.getId());
|
|
|
+ String unitId = null;
|
|
|
+ if (relationResult != null && relationResult.isSuccess() && relationResult.getData() != null && !relationResult.getData().isEmpty()) {
|
|
|
+ List<UseUnitRelationSaveReqVO> useUnitRelations = relationResult.getData();
|
|
|
+ UseUnitRelationSaveReqVO useUnitRelation = useUnitRelations.get(0);
|
|
|
+ unitId = useUnitRelation.getUnitId();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (unitId == null) {
|
|
|
+ log.warn("[buildMockUser][单位ID为空]");
|
|
|
+ }
|
|
|
+
|
|
|
+ user.setId(loginUser.getId());
|
|
|
+ user.setNickname(checkedData.getNickname());
|
|
|
+ user.setMobile(checkedData.getMobile());
|
|
|
+ user.setUnitType(loginUser.getUnitType());
|
|
|
+ user.setUnitId(unitId);
|
|
|
+ user.setName(checkedData.getNickname());
|
|
|
+ }
|
|
|
+ return user;
|
|
|
+ }
|
|
|
+
|
|
|
+ private AppAuthUserInfo doAuthenticate(String accessToken) {
|
|
|
+ // 1. 生成 timestamp 和 nonce(32位UUID,长度 >= 10)
|
|
|
+ long timestamp = System.currentTimeMillis();
|
|
|
+ String nonce = IdUtil.fastSimpleUUID();
|
|
|
+
|
|
|
+ // 2. nonce 存入 Redis 防重放,过期时间 120 秒(服务器时间偏差窗口 60 秒的 2 倍)
|
|
|
+ String nonceKey = String.format(NONCE_KEY_FORMAT, appId, nonce);
|
|
|
+ stringRedisTemplate.opsForValue().set(nonceKey, "1", Duration.ofSeconds(120));
|
|
|
+
|
|
|
+ // 3. 计算签名
|
|
|
+ // GET 请求,无查询参数(第1步为空),无请求体(第2步为空)
|
|
|
+ // 第3步:Header 参数按 key 字典序排列 → appId < nonce < timestamp
|
|
|
+ // 第4步:末尾拼接 appSecret
|
|
|
+ // 最终:sign = SHA-256("appId={appId}&nonce={nonce}×tamp={timestamp}{appSecret}")
|
|
|
+ String signString = "appId=" + appId + "&nonce=" + nonce + "×tamp=" + timestamp + appSecret;
|
|
|
+ String sign = DigestUtil.sha256Hex(signString);
|
|
|
+
|
|
|
+ log.debug("[doAuthenticate][签名计算完成, appId={}, nonce={}, timestamp={}]", appId, nonce, timestamp);
|
|
|
+
|
|
|
+ // 4. 设置请求头并发起调用
|
|
|
+ HttpHeaders headers = new HttpHeaders();
|
|
|
+ headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
|
|
|
+ headers.set("appId", appId);
|
|
|
+ headers.set("timestamp", String.valueOf(timestamp));
|
|
|
+ headers.set("nonce", nonce);
|
|
|
+ headers.set("sign", sign);
|
|
|
+
|
|
|
+ HttpEntity<Void> entity = new HttpEntity<>(headers);
|
|
|
+
|
|
|
+ String url = baseUrl + "/external-api/member/auth/auth";
|
|
|
+ ResponseEntity<String> response = restTemplate.exchange(
|
|
|
+ url, HttpMethod.GET, entity, String.class);
|
|
|
+
|
|
|
+ if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
|
|
+ CommonResult<AppAuthUserInfo> result = JsonUtils.parseObject(
|
|
|
+ response.getBody(), AUTH_RESULT_TYPE);
|
|
|
+ if (result != null && result.isSuccess()) {
|
|
|
+ return result.getData();
|
|
|
+ }
|
|
|
+ log.warn("[doAuthenticate][鉴权接口返回失败: code={}, msg={}]",
|
|
|
+ result != null ? result.getCode() : null,
|
|
|
+ result != null ? result.getMsg() : null);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ log.warn("[doAuthenticate][鉴权接口HTTP状态异常: {}]", response.getStatusCode());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+}
|