|
|
@@ -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()));
|
|
|
}
|
|
|
}
|