package com.tcm.training.admin.app.auth.service; import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.crypto.RSASSAVerifier; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import com.tcm.training.admin.app.auth.config.AppAppleAuthProperties; import com.tcm.training.admin.app.auth.dto.AppAppleLoginRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.client.RestOperations; import org.springframework.web.client.RestTemplate; import java.text.ParseException; import java.util.Date; import java.util.List; /** * 苹果登录验签客户端,负责校验 identityToken 并提取最小身份标识。 */ @Service public class AppAppleAuthClient { private static final Logger log = LoggerFactory.getLogger(AppAppleAuthClient.class); private final AppAppleAuthProperties properties; private final ObjectMapper objectMapper; private final RestOperations restOperations; /** * 使用默认 HTTP 客户端构造苹果认证客户端。 */ @Autowired public AppAppleAuthClient(AppAppleAuthProperties properties, ObjectMapper objectMapper) { this(properties, objectMapper, new RestTemplate()); } /** * 注入苹果配置、JSON 解析器与 HTTP 客户端。 */ AppAppleAuthClient(AppAppleAuthProperties properties, ObjectMapper objectMapper, RestOperations restOperations) { this.properties = properties; this.objectMapper = objectMapper; this.restOperations = restOperations; } /** * 校验苹果 identityToken 并提取本地登录所需的最小身份资料。 */ public AppAppleUserProfile resolveUserProfile(AppAppleLoginRequest request) { if (request == null) { throw new IllegalArgumentException("苹果授权信息不完整"); } if (!properties.isConfigured()) { throw new IllegalArgumentException("苹果登录暂未配置,请稍后重试"); } String identityToken = trimToNull(request.getIdentityToken()); String userIdentifier = trimToNull(request.getUserIdentifier()); String authorizationCode = trimToNull(request.getAuthorizationCode()); if (identityToken == null) { throw new IllegalArgumentException("苹果授权信息不完整"); } SignedJWT signedJwt = parseSignedJwt(identityToken); verifySignature(signedJwt); JWTClaimsSet claimsSet = parseClaims(signedJwt); validateClaims(claimsSet); String subject = trimToNull(claimsSet.getSubject()); if (subject == null) { throw new IllegalArgumentException("苹果登录授权失败"); } if (userIdentifier != null && !userIdentifier.equals(subject)) { throw new IllegalArgumentException("苹果登录授权失败"); } AppAppleUserProfile profile = new AppAppleUserProfile(); profile.setSubject(subject); profile.setUserIdentifier(userIdentifier == null ? subject : userIdentifier); profile.setAuthorizationCode(authorizationCode); return profile; } /** * 解析苹果返回的 JWT。 */ private SignedJWT parseSignedJwt(String identityToken) { try { return SignedJWT.parse(identityToken); } catch (ParseException ex) { throw new IllegalArgumentException("苹果登录授权失败"); } } /** * 校验苹果 JWT 签名。 */ private void verifySignature(SignedJWT signedJwt) { try { if (!JWSAlgorithm.RS256.equals(signedJwt.getHeader().getAlgorithm())) { throw new IllegalArgumentException("苹果登录授权失败"); } String keyId = trimToNull(signedJwt.getHeader().getKeyID()); if (keyId == null) { throw new IllegalArgumentException("苹果登录授权失败"); } RSAKey rsaKey = loadRsaKey(keyId); if (!signedJwt.verify(new RSASSAVerifier(rsaKey.toRSAPublicKey()))) { throw new IllegalArgumentException("苹果登录授权失败"); } } catch (JOSEException ex) { throw new IllegalArgumentException("苹果登录授权失败"); } } /** * 读取并匹配苹果公钥。 */ private RSAKey loadRsaKey(String keyId) { JWKSet jwkSet = loadJwkSet(); List keys = jwkSet.getKeys(); for (JWK key : keys) { if (!(key instanceof RSAKey)) { continue; } if (keyId.equals(key.getKeyID())) { return (RSAKey) key; } } throw new IllegalArgumentException("苹果登录授权失败"); } /** * 拉取苹果 JWKS 公钥集合。 */ private JWKSet loadJwkSet() { try { String responseBody = restOperations.getForObject(properties.getJwksUrl(), String.class); if (responseBody == null || responseBody.trim().isEmpty()) { throw new IllegalArgumentException("苹果登录授权失败"); } objectMapper.readTree(responseBody); return JWKSet.parse(responseBody); } catch (IllegalArgumentException ex) { throw ex; } catch (Exception ex) { log.error("拉取苹果 JWKS 失败: url={}", properties.getJwksUrl(), ex); throw new IllegalArgumentException("苹果登录授权失败"); } } /** * 解析 JWT Claims。 */ private JWTClaimsSet parseClaims(SignedJWT signedJwt) { try { return signedJwt.getJWTClaimsSet(); } catch (ParseException ex) { throw new IllegalArgumentException("苹果登录授权失败"); } } /** * 校验 JWT Claims 是否满足本系统登录要求。 */ private void validateClaims(JWTClaimsSet claimsSet) { String issuer = trimToNull(claimsSet.getIssuer()); if (!properties.getIssuer().equals(issuer)) { throw new IllegalArgumentException("苹果登录授权失败"); } String audience = resolveAudience(claimsSet.getAudience()); if (!properties.getClientId().equals(audience)) { throw new IllegalArgumentException("苹果登录授权失败"); } Date expireAt = claimsSet.getExpirationTime(); if (expireAt == null || expireAt.before(new Date())) { throw new IllegalArgumentException("苹果登录授权失败"); } } /** * 读取 audience,当前仅接受单值 clientId。 */ private String resolveAudience(List audienceList) { if (audienceList == null || audienceList.isEmpty()) { return null; } return trimToNull(audienceList.get(0)); } /** * 将空白字符串归一化为 null。 */ private String trimToNull(String value) { if (value == null) { return null; } String trimmed = value.trim(); return trimmed.isEmpty() ? null : trimmed; } }