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<JWK> 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<String> 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;
|
}
|
}
|