苹果登录后端处理方案
基于你提供的信息,这是一个标准的 Apple Sign In 流程
一、数据解析说明
你收到的关键数据:
- identityToken: JWT格式,包含用户信息,需要验证和解析
- authorizationCode: 一次性授权码,可用于获取refresh token
- User ID (sub):
000925.53c122378700481a8535d1545e774116.1004- Apple用户唯一标识 - Email:
7hmrtwtwyd@privaterelay.appleid.com- 隐私邮箱(首次登录才有)
二、整体流程
客户端 → 后端
1. identityToken (必须)
2. authorizationCode (可选,用于server-to-server通信)
3. user (首次登录时的用户信息)
后端处理流程:
1. 验证 identityToken 的签名和有效性
2. 解析 identityToken 获取用户信息
3. 检查用户是否存在
4. 创建/更新用户
5. 生成业务系统的 token
6. 返回登录结果
三、核心代码实现
1. Maven依赖配置
<dependency>
<groupId>com.apple.itunes.storekit</groupId>
<artifactId>app-store-server-library</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
2. 配置类
@Configuration
@ConfigurationProperties(prefix = "apple.signin")
@Data
public class AppleSignInConfig {
// 你的 App Bundle ID
private String clientId = "com.wanmei.boyanotra";
// Apple 公钥获取地址
private String publicKeyUrl = "https://appleid.apple.com/auth/keys";
// Token 签发者
private String issuer = "https://appleid.apple.com";
}
3. 核心服务类
@Service
@Slf4j
public class AppleSignInService {
@Autowired
private AppleSignInConfig config;
@Autowired
private UserService userService;
// 缓存 Apple 公钥,避免频繁请求
private volatile List<PublicKey> cachedPublicKeys;
private volatile long lastFetchTime = 0;
private static final long CACHE_DURATION = 24 * 60 * 60 * 1000; // 24小时
/**
* 处理苹果登录
*/
public LoginResponse handleAppleSignIn(AppleSignInRequest request) {
try {
// 1. 验证并解析 identityToken
DecodedJWT jwt = verifyAndDecodeToken(request.getIdentityToken());
// 2. 提取用户信息
String appleUserId = jwt.getSubject(); // sub字段
String email = jwt.getClaim("email").asString();
Boolean isPrivateEmail = jwt.getClaim("is_private_email").asBoolean();
Boolean emailVerified = jwt.getClaim("email_verified").asBoolean();
log.info("Apple登录 - UserID: {}, Email: {}, IsPrivate: {}",
appleUserId, email, isPrivateEmail);
// 3. 查找或创建用户
User user = userService.findByAppleId(appleUserId);
if (user == null) {
user = createNewUser(appleUserId, email, isPrivateEmail, request);
} else {
// 更新最后登录时间等
updateUserLoginInfo(user, email);
}
// 4. 生成业务系统token
String accessToken = generateAccessToken(user);
// 5. 返回登录结果
return LoginResponse.builder()
.accessToken(accessToken)
.userId(user.getId())
.isNewUser(user.getCreatedAt().equals(user.getUpdatedAt()))
.build();
} catch (JWTVerificationException e) {
log.error("Apple Token验证失败", e);
throw new BusinessException("苹果登录验证失败");
} catch (Exception e) {
log.error("Apple登录处理异常", e);
throw new BusinessException("登录失败");
}
}
/**
* 验证并解析 identityToken
* 重点:验证签名、issuer、audience、过期时间
*/
private DecodedJWT verifyAndDecodeToken(String identityToken) throws Exception {
// 1. 先解码获取 header 中的 kid
DecodedJWT unverifiedJwt = JWT.decode(identityToken);
String kid = unverifiedJwt.getKeyId();
// 2. 获取对应的公钥
PublicKey publicKey = getPublicKey(kid);
// 3. 验证 Token
Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) publicKey, null);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(config.getIssuer()) // 验证签发者
.withAudience(config.getClientId()) // 验证接收者(你的App ID)
.build();
// 4. 验证并返回
return verifier.verify(identityToken);
}
/**
* 获取Apple公钥
* 从 Apple 服务器获取公钥用于验证 JWT 签名
*/
private PublicKey getPublicKey(String kid) throws Exception {
// 检查缓存
if (cachedPublicKeys != null &&
System.currentTimeMillis() - lastFetchTime < CACHE_DURATION) {
return findKeyByKid(cachedPublicKeys, kid);
}
// 从Apple获取公钥
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject(config.getPublicKeyUrl(), String.class);
JSONObject jsonObject = new JSONObject(response);
JSONArray keys = jsonObject.getJSONArray("keys");
List<PublicKey> publicKeys = new ArrayList<>();
for (int i = 0; i < keys.length(); i++) {
JSONObject key = keys.getJSONObject(i);
if (kid.equals(key.getString("kid"))) {
PublicKey publicKey = buildPublicKey(key);
publicKeys.add(publicKey);
// 更新缓存
cachedPublicKeys = publicKeys;
lastFetchTime = System.currentTimeMillis();
return publicKey;
}
}
throw new Exception("未找到对应的公钥 kid: " + kid);
}
/**
* 构建RSA公钥
*/
private PublicKey buildPublicKey(JSONObject key) throws Exception {
String n = key.getString("n"); // modulus
String e = key.getString("e"); // exponent
byte[] nBytes = Base64.getUrlDecoder().decode(n);
byte[] eBytes = Base64.getUrlDecoder().decode(e);
BigInteger modulus = new BigInteger(1, nBytes);
BigInteger exponent = new BigInteger(1, eBytes);
RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
}
/**
* 创建新用户
*/
private User createNewUser(String appleUserId, String email,
Boolean isPrivateEmail, AppleSignInRequest request) {
User user = new User();
user.setAppleUserId(appleUserId);
user.setEmail(email);
user.setIsPrivateEmail(isPrivateEmail);
user.setLoginType("apple");
// 首次登录时,客户端可能传了用户名
if (request.getFullName() != null) {
user.setNickname(request.getFullName());
} else {
user.setNickname("Apple用户" + appleUserId.substring(0, 8));
}
return userService.save(user);
}
/**
* 更新用户登录信息
*/
private void updateUserLoginInfo(User user, String email) {
// 如果email从null变成有值,更新
if (user.getEmail() == null && email != null) {
user.setEmail(email);
}
user.setLastLoginTime(new Date());
userService.update(user);
}
}
4. 请求和响应对象
@Data
public class AppleSignInRequest {
@NotBlank(message = "identityToken不能为空")
private String identityToken;
private String authorizationCode; // 可选
private String fullName; // 首次登录时客户端可能提供
private String state; // 防CSRF
}
@Data
@Builder
public class LoginResponse {
private String accessToken;
private Long userId;
private Boolean isNewUser;
private String email;
}
5. Controller
@RestController
@RequestMapping("/api/auth")
@Slf4j
public class AuthController {
@Autowired
private AppleSignInService appleSignInService;
@PostMapping("/apple/signin")
public Result<LoginResponse> appleSignIn(@Valid @RequestBody AppleSignInRequest request) {
log.info("收到Apple登录请求");
LoginResponse response = appleSignInService.handleAppleSignIn(request);
return Result.success(response);
}
}
四、关键要点总结
✅ 必须验证的内容
- JWT签名验证 - 使用Apple公钥验证
- issuer验证 - 必须是
https://appleid.apple.com - audience验证 - 必须是你的Bundle ID
- 过期时间验证 - exp字段
- kid匹配 - header中的kid要能在Apple公钥列表中找到
⚠️ 注意事项
- Email获取时机: 用户首次授权时才会返回email,后续登录email可能为null
- 隐私邮箱:
is_private_email=true时是Apple中继邮箱,需要特殊处理 - 公钥缓存: Apple公钥不经常变化,建议缓存24小时
- authorizationCode: 一次性使用,可用于server-to-server获取refresh token
- 用户唯一标识: 使用
sub字段作为用户唯一ID
🔐 安全建议
- 总是验证JWT签名,不要信任未验证的token
- 验证audience防止token被其他应用冒用
- 检查token过期时间
- 记录登录日志,监控异常登录
五、数据库设计建议
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`apple_user_id` VARCHAR(100) UNIQUE COMMENT 'Apple用户ID(sub字段)',
`email` VARCHAR(255) COMMENT '邮箱',
`is_private_email` TINYINT(1) COMMENT '是否隐私邮箱',
`nickname` VARCHAR(50),
`login_type` VARCHAR(20) COMMENT '登录类型:apple/wechat等',
`last_login_time` DATETIME,
`created_at` DATETIME,
`updated_at` DATETIME,
INDEX `idx_apple_user_id` (`apple_user_id`)