苹果登录信息解析


苹果登录后端处理方案

基于你提供的信息,这是一个标准的 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);
    }
}

四、关键要点总结

✅ 必须验证的内容

  1. JWT签名验证 - 使用Apple公钥验证
  2. issuer验证 - 必须是 https://appleid.apple.com
  3. audience验证 - 必须是你的Bundle ID
  4. 过期时间验证 - exp字段
  5. kid匹配 - header中的kid要能在Apple公钥列表中找到

⚠️ 注意事项

  1. Email获取时机: 用户首次授权时才会返回email,后续登录email可能为null
  2. 隐私邮箱: is_private_email=true时是Apple中继邮箱,需要特殊处理
  3. 公钥缓存: Apple公钥不经常变化,建议缓存24小时
  4. authorizationCode: 一次性使用,可用于server-to-server获取refresh token
  5. 用户唯一标识: 使用sub字段作为用户唯一ID

🔐 安全建议

  1. 总是验证JWT签名,不要信任未验证的token
  2. 验证audience防止token被其他应用冒用
  3. 检查token过期时间
  4. 记录登录日志,监控异常登录

五、数据库设计建议



  `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`)


扫描二维码,在手机上阅读
收藏

测试接口

ip说明

评 论
请登录后再评论