qmj 3 дней назад
Родитель
Сommit
cb420d3bca

Разница между файлами не показана из-за своего большого размера
+ 16 - 0
.claude/homunculus/observations.jsonl


+ 25 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/user/InfoUserController.java

@@ -43,6 +43,8 @@ import com.ruoyi.system.mapper.InfoUserOauthMapper;
 
 import eu.bitwalker.useragentutils.UserAgent;
 import org.apache.commons.lang3.RandomStringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
@@ -73,6 +75,8 @@ import java.util.stream.Collectors;
 @RestController
 @RequestMapping("/infouser/user")
 public class InfoUserController extends BaseController {
+    private static final Logger log = LoggerFactory.getLogger(InfoUserController.class);
+
     @Autowired
     private IInfoUserService infoUserService;
     @Autowired
@@ -910,6 +914,7 @@ public class InfoUserController extends BaseController {
         if (dto == null || dto.getProvider() == null || dto.getProvider().isEmpty()) {
             return error(MessageUtils.message("no.oauth.provider.blank"));
         }
+        log.info("[OAuth] oauthLogin provider={}", dto.getProvider());
         String providerUid = oauthVerifyService.verify(dto.getProvider(), dto.getCredential());
 
         // 查是否已绑定
@@ -919,10 +924,12 @@ public class InfoUserController extends BaseController {
                         .eq(InfoUserOauth::getProviderUid, providerUid));
         if (bind != null) {
             // 已绑定:校验用户正常后直接登录
+            log.info("[OAuth] 已绑定 provider={}, providerUid={}, userId={}", dto.getProvider(), providerUid, bind.getUserId());
             QueryWrapper<InfoUser> q = new QueryWrapper<>();
             q.eq("user_id", bind.getUserId()).eq("status", 0).eq("del_flag", "0");
             InfoUser u = infoUserService.getOne(q);
             if (u == null) {
+                log.warn("[OAuth] 已绑定但账号已停用 userId={}", bind.getUserId());
                 return error(MessageUtils.message("no.user.stop"));
             }
             u.setCid(dto.getCid());
@@ -934,6 +941,7 @@ public class InfoUserController extends BaseController {
         }
 
         // 未绑定:缓存 {provider, providerUid},返回 needPhone + tempKey
+        log.info("[OAuth] 未绑定,返回 needPhone provider={}, providerUid={}", dto.getProvider(), providerUid);
         String tempKey = UUID.randomUUID().toString().replace("-", "");
         redisCache.setCacheObject(OAUTH_TEMP_PREFIX + tempKey,
                 dto.getProvider() + "@" + providerUid, 5, TimeUnit.MINUTES);
@@ -950,15 +958,18 @@ public class InfoUserController extends BaseController {
     @PostMapping("/oauthBindPhone")
     public AjaxResult oauthBindPhone(@RequestBody OAuthBindDto dto) {
         if (dto == null || dto.getTempKey() == null || dto.getTempKey().isEmpty()) {
+            log.warn("[OAuth] oauthBindPhone 缺 tempKey");
             return error(MessageUtils.message("no.oauth.tempkey.missing"));
         }
         String cached = redisCache.getCacheObject(OAUTH_TEMP_PREFIX + dto.getTempKey());
         if (cached == null) {
+            log.warn("[OAuth] tempKey 已过期/不存在 tempKey={}", dto.getTempKey());
             return error(MessageUtils.message("no.oauth.tempkey.expired"));
         }
         int at = cached.indexOf('@');
         String provider = cached.substring(0, at);
         String providerUid = cached.substring(at + 1);
+        log.info("[OAuth] 绑定流程 provider={}, providerUid={}", provider, providerUid);
 
         // 容错:若期间已被绑定,直接登录
         InfoUserOauth exist = infoUserOauthMapper.selectOne(
@@ -966,6 +977,7 @@ public class InfoUserController extends BaseController {
                         .eq(InfoUserOauth::getProvider, provider)
                         .eq(InfoUserOauth::getProviderUid, providerUid));
         if (exist != null) {
+            log.info("[OAuth] 绑定期间已被绑定,直接登录 userId={}", exist.getUserId());
             InfoUser u = infoUserService.getById(exist.getUserId());
             return issueOauthToken(u, provider);
         }
@@ -975,6 +987,7 @@ public class InfoUserController extends BaseController {
         String xcode = redisCache.getCacheObject(phone.trim().replaceAll("\\+", ""));
         boolean codeOk = (xcode != null && xcode.equals(dto.getCode())) || "8888".equals(dto.getCode());
         if (!codeOk) {
+            log.warn("[OAuth] 短信验证码错误 phone={}", maskPhone(phone));
             return error(MessageUtils.message("no.user.jcaptcha.error"));
         }
 
@@ -982,6 +995,7 @@ public class InfoUserController extends BaseController {
         InfoUser user = infoUserService.getuser(phone);
         if (user != null && !"0".equals(user.getStatus())) {
             // 停用账号不允许绑定三方(与 lodeing 一致,防止绕过停用)
+            log.warn("[OAuth] 账号已停用 phone={}", maskPhone(phone));
             return error(MessageUtils.message("no.user.stop"));
         }
         if (user == null) {
@@ -998,12 +1012,14 @@ public class InfoUserController extends BaseController {
             infoUserService.saveOrUpdate(info);
             user = infoUserService.getuser(phone);
             createUserWallet(user.getUserId());
+            log.info("[OAuth] 新建账号 userId={}", user.getUserId());
         } else {
             user.setCid(dto.getCid());
             user.setCidType(dto.getCidType());
             user.setDeviceToken(dto.getDeviceToken());
             user.setVoIPToken(dto.getVoIPToken());
             infoUserService.saveOrUpdate(user);
+            log.info("[OAuth] 关联已有账号 userId={}", user.getUserId());
         }
 
         // 写三方绑定
@@ -1013,11 +1029,20 @@ public class InfoUserController extends BaseController {
         bind.setProviderUid(providerUid);
         bind.setCreateTime(new Date());
         infoUserOauthMapper.insert(bind);
+        log.info("[OAuth] 写入绑定 provider={}, providerUid={}, userId={}", provider, providerUid, user.getUserId());
 
         redisCache.deleteObject(OAUTH_TEMP_PREFIX + dto.getTempKey());
         return issueOauthToken(user, provider);
     }
 
+    /** 手机号脱敏:保留前 3 后 4,中间 ****(仅日志用)。 */
+    private static String maskPhone(String phone) {
+        if (phone == null || phone.length() < 8) {
+            return phone;
+        }
+        return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
+    }
+
     /** 三方登录签发 token(claim provider=本次登录渠道),复用现有 JwtUtil + Redis 会话。 */
     private AjaxResult issueOauthToken(InfoUser user, String provider) {
         if (user == null) {

+ 29 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/utils/oauth/OAuthVerifyService.java

@@ -18,6 +18,8 @@ import org.apache.http.client.methods.HttpGet;
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.HttpClients;
 import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
@@ -42,6 +44,8 @@ import java.util.List;
 @Service
 public class OAuthVerifyService {
 
+    private static final Logger log = LoggerFactory.getLogger(OAuthVerifyService.class);
+
     @Value("${oauth.apple.client-id}")
     private String appleClientId;
     @Value("${oauth.apple.jwks-url}")
@@ -66,6 +70,8 @@ public class OAuthVerifyService {
         if (credential == null || credential.isEmpty()) {
             throw new ServiceException(MessageUtils.message("no.oauth.credential.blank"));
         }
+        log.info("[OAuth] 校验凭证 provider={}", provider);
+        log.debug("[OAuth] {} credential={}", provider, credential);
         switch (provider) {
             case "apple":
                 return verifyApple(credential);
@@ -81,12 +87,14 @@ public class OAuthVerifyService {
     /** Apple:验 ES256 identityToken,返回 sub(不使用邮箱信息)。 */
     private String verifyApple(String idToken) {
         try {
+            log.debug("[OAuth][Apple] identityToken={}", idToken);
             SignedJWT jwt = SignedJWT.parse(idToken);
             String kid = jwt.getHeader().getKeyID();
             // 拉取 Apple 公钥(JWKS)并按 kid 匹配
             JWKSet jwkSet = JWKSet.load(new URL(appleJwksUrl));
             JWK jwk = jwkSet.getKeyByKeyId(kid);
             if (jwk == null) {
+                log.warn("[OAuth][Apple] 公钥未匹配 kid={}", kid);
                 throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "Apple"));
             }
             // Apple identityToken 实测 alg=RS256(RSA);按 JWK 类型选验签器,兼容 EC
@@ -96,31 +104,39 @@ public class OAuthVerifyService {
             } else if (jwk instanceof ECKey) {
                 verifier = new ECDSAVerifier(((ECKey) jwk).toECPublicKey());
             } else {
+                log.warn("[OAuth][Apple] 不支持的公钥类型 kid={}", kid);
                 throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "Apple"));
             }
             if (!jwt.verify(verifier)) {
+                log.warn("[OAuth][Apple] 验签失败 kid={}", kid);
                 throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "Apple"));
             }
             JWTClaimsSet claims = jwt.getJWTClaimsSet();
             if (!"https://appleid.apple.com".equals(claims.getIssuer())) {
+                log.warn("[OAuth][Apple] iss 非法: {}", claims.getIssuer());
                 throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "Apple"));
             }
             List<String> aud = claims.getAudience();
             if (aud == null || !aud.contains(appleClientId)) {
+                log.warn("[OAuth][Apple] aud 不匹配: expected={}, got={}", appleClientId, aud);
                 throw new ServiceException(MessageUtils.message("no.oauth.audience.mismatch", "Apple"));
             }
             Date exp = claims.getExpirationTime();
             if (exp == null || exp.before(new Date())) {
+                log.warn("[OAuth][Apple] token 已过期: exp={}", exp);
                 throw new ServiceException(MessageUtils.message("no.oauth.token.expired", "Apple"));
             }
             String sub = claims.getSubject();
             if (sub == null || sub.isEmpty()) {
+                log.warn("[OAuth][Apple] sub 为空");
                 throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "Apple"));
             }
+            log.info("[OAuth][Apple] 校验通过 sub={}", sub);
             return sub;
         } catch (ServiceException se) {
             throw se;
         } catch (Exception e) {
+            log.error("[OAuth][Apple] 校验异常", e);
             throw new ServiceException(MessageUtils.message("no.oauth.verify.fail", "Apple", e.getMessage()));
         }
     }
@@ -128,23 +144,30 @@ public class OAuthVerifyService {
     /** Google:tokeninfo HTTP 验真 + 校 audience,返回 sub。 */
     private String verifyGoogle(String idToken) {
         try {
+            log.debug("[OAuth][Google] idToken={}", idToken);
             String url = googleTokeninfoUrl + "?id_token=" + URLEncoder.encode(idToken, "UTF-8");
             JSONObject json = JSONObject.parseObject(httpGet(url, null));
+            log.debug("[OAuth][Google] tokeninfo 返回: {}", json);
             if (json == null || json.containsKey("error") || json.containsKey("error_description")) {
+                log.warn("[OAuth][Google] tokeninfo 返回错误: {}", json);
                 throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "Google"));
             }
             String aud = json.getString("aud");
             if (!googleClientId.equals(aud)) {
+                log.warn("[OAuth][Google] aud 不匹配: expected={}, got={}", googleClientId, aud);
                 throw new ServiceException(MessageUtils.message("no.oauth.audience.mismatch", "Google"));
             }
             String sub = json.getString("sub");
             if (sub == null || sub.isEmpty()) {
+                log.warn("[OAuth][Google] sub 为空");
                 throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "Google"));
             }
+            log.info("[OAuth][Google] 校验通过 sub={}", sub);
             return sub;
         } catch (ServiceException se) {
             throw se;
         } catch (Exception e) {
+            log.error("[OAuth][Google] 校验异常", e);
             throw new ServiceException(MessageUtils.message("no.oauth.verify.fail", "Google", e.getMessage()));
         }
     }
@@ -152,18 +175,24 @@ public class OAuthVerifyService {
     /** LINE:v2/profile HTTP(Bearer accessToken),返回 userId。 */
     private String verifyLine(String accessToken) {
         try {
+            log.debug("[OAuth][LINE] accessToken={}", accessToken);
             JSONObject json = JSONObject.parseObject(httpGet(lineProfileUrl, accessToken));
+            log.debug("[OAuth][LINE] profile 返回: {}", json);
             if (json == null) {
+                log.warn("[OAuth][LINE] profile 返回空");
                 throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "LINE"));
             }
             String userId = json.getString("userId");
             if (userId == null || userId.isEmpty()) {
+                log.warn("[OAuth][LINE] 无 userId: {}", json);
                 throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "LINE"));
             }
+            log.info("[OAuth][LINE] 校验通过 userId={}", userId);
             return userId;
         } catch (ServiceException se) {
             throw se;
         } catch (Exception e) {
+            log.error("[OAuth][LINE] 校验异常", e);
             throw new ServiceException(MessageUtils.message("no.oauth.verify.fail", "LINE", e.getMessage()));
         }
     }

Некоторые файлы не были показаны из-за большого количества измененных файлов