我已经实现了新的苹果功能“使用 Apple 登录”的应用程序端,但我无法在后端使用授权码进行验证。我的后端是用 java 编写的,我无法生成 JWT 并与 Apple 服务器通信。
2 回答
首先去 developer.apple.com -> Certificates, Identifiers & Profiles -> Keys。为 Apple 登录生成密钥并下载此密钥。您无法再次下载此密钥,因此请将其保存在安全的地方,不要与他人共享。此外,您在此处显示的密钥 ID 请注意这一点,稍后您将需要它。您还需要团队 ID。如果你不知道,它写在页面的右上角,如 YOURNAME - XX0XX00XXX。
您将基本上遵循这些步骤。
1.从您的密钥生成 JWT
2.使用您的令牌发送验证码
3.解码响应
使用网络和移动设备的更新
如果您想使用 Apple 登录网页版,您还需要执行几个步骤。
4. 添加新的网络标识符
转到 developer.apple.com -> 证书、标识符和配置文件 -> 标识符。单击加号按钮注册新标识符。选择服务 ID 并继续。提供描述和标识符。标识符必须是唯一的,并且与您的捆绑包 ID 不同。(例如,您可以使用 com.your.bundle.id.web)。点击继续点击注册。然后你需要配置这个服务ID。选择服务 ID(它位于搜索图标附近的右上角)您在下面列出的新创建的服务 ID 单击它并
启用“使用 Apple 登录”复选框。然后你需要配置你的域。提供您的域名并返回网址。
如果您忘记传递有效的redirect_url或尝试多次使用相同的authorization_code,您可能会收到invalid_grant错误。
public class AppleLoginUtil {
private static String APPLE_AUTH_URL = "https://appleid.apple.com/auth/token";
private static String KEY_ID = "**********";
private static String TEAM_ID = "**********";
private static String CLIENT_ID = "com.your.bundle.id";
private static String WEB_CLIENT_ID = "com.your.bundle.id.web";
private static String WEB_REDIRECT_URL = "https://bundle.your.com/";
private static PrivateKey pKey;
private static PrivateKey getPrivateKey() throws Exception {
//read your key
String path = new ClassPathResource("apple/AuthKey.p8").getFile().getAbsolutePath();
final PEMParser pemParser = new PEMParser(new FileReader(path));
final JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
final PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject();
final PrivateKey pKey = converter.getPrivateKey(object);
return pKey;
}
private static String generateJWT() throws Exception {
if (pKey == null) {
pKey = getPrivateKey();
}
String token = Jwts.builder()
.setHeaderParam(JwsHeader.KEY_ID, KEY_ID)
.setIssuer(TEAM_ID)
.setAudience("https://appleid.apple.com")
.setSubject(CLIENT_ID)
.setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 5)))
.setIssuedAt(new Date(System.currentTimeMillis()))
.signWith(pKey, SignatureAlgorithm.ES256)
.compact();
return token;
}
private static String generateWebJWT() throws Exception {
String token = Jwts.builder()
.setHeaderParam(JwsHeader.KEY_ID, KEY_ID)
.setIssuer(TEAM_ID)
.setAudience("https://appleid.apple.com")
.setSubject(WEB_CLIENT_ID)
.setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 5)))
.setIssuedAt(new Date(System.currentTimeMillis()))
.signWith(getPrivateKey(), SignatureAlgorithm.ES256)
.compact();
return token;
}
/*
* Returns unique user id from apple
* */
public static String appleAuth(String authorizationCode, boolean forWeb) throws Exception {
HttpResponse<String> response = Unirest.post(APPLE_AUTH_URL)
.header("Content-Type", "application/x-www-form-urlencoded")
.field("client_id", forWeb ? WEB_CLIENT_ID : CLIENT_ID)
.field("client_secret", forWeb ? generateWebJWT() : generateJWT())
.field("grant_type", "authorization_code")
.field("code", authorizationCode)
.field("redirect_uri", forWeb ? WEB_REDIRECT_URL : null)
.asString();
TokenResponse tokenResponse=new Gson().fromJson(response.getBody(),TokenResponse.class);
String idToken = tokenResponse.getId_token();
String payload = idToken.split("\\.")[1];//0 is header we ignore it for now
String decoded = new String(Decoders.BASE64.decode(payload));
IdTokenPayload idTokenPayload = new Gson().fromJson(decoded,IdTokenPayload.class);
return idTokenPayload.getSub();
}
}
我使用 BouncyCastle jjwt 来生成令牌。还有 unirest 和 gson 用于休息电话。
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.63</version>
</dependency>
<!--JJWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<!--UNIREST-->
<dependency>
<groupId>com.mashape.unirest</groupId>
<artifactId>unirest-java</artifactId>
<version>1.4.9</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.3.6</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpasyncclient</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.3.6</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20140107</version>
</dependency>
如果您想知道,我还解析了对这些类的响应。
public class TokenResponse {
private String access_token;
private String token_type;
private Long expires_in;
private String refresh_token;
private String id_token;
..getters and setters
}
public class IdTokenPayload {
private String iss;
private String aud;
private Long exp;
private Long iat;
private String sub;//users unique id
private String at_hash;
private Long auth_time;
private Boolean nonce_supported;
private Boolean email_verified;
private String email;
..getters and setters
}
我也有一个错误,但经过一些调整,它的工作,在下面找到我的调整,注意,它在 kotlin
private suspend fun getPrivateKey(): Status {
return awaitBlocking {
val authKeyFile = appleConfig.getString("auth_private_key_file", "")
val authTokenFilePath = getDataDir()!!.resolve(authKeyFile).absolutePath
val pemParser = PEMParser(FileReader(authTokenFilePath))
val converter = JcaPEMKeyConverter()
val obj = pemParser.readObject() as PrivateKeyInfo
val privateKey = converter.getPrivateKey(obj)
successStatus(data = privateKey)
}
}
/**
* generateSecretKey
*/
suspend fun generateSecretKey() : Status{
val getAuthPrivateKey = getPrivateKey()
if(getAuthPrivateKey.isError()){
logger.fatal(getAuthPrivateKey.message)
return errorStatus("system_busy")
}
val privateKeyData = getAuthPrivateKey.getData<PrivateKey>()
val clientId = "com.company.app"
//team id found in apple developer portal
val teamId = appleConfig.getString("team_id","")
//apple sign in key ID found in app developer portal
val authKeyId = appleConfig.getString("auth_key_id","")
val header = mutableMapOf<String,Any>(
"alg" to "E256",
"kid" to authKeyId
)
val now = Instant.now().epochSecond
val claims = mutableMapOf<String,Any>(
"iss" to teamId,
"iat" to now,
"exp" to now + 86400*180,
"aud" to "https://appleid.apple.com",
"sub" to clientId
)
println("header - $header")
println("claims - $claims")
val token = Jwts.builder()
.setHeader(header)
.setClaims(claims)
.signWith(privateKeyData,SignatureAlgorithm.ES256)
.compact();
return successStatus(data = token)
} //end fun
/**
* fetchApplePublicKeys
*/
private suspend fun fetchAccessToken(authInfo: JsonObject): Status {
return try{
val authCode = authInfo.getString("auth_code")
val clientIdToken = authInfo.getString("id_token")
val accessTokenEndpoint =
appleConfig.getString("access_token_endpoint")
val secretKeyTokenStatus = generateSecretKey()
if(secretKeyTokenStatus.isError()){
logger.fatal(secretKeyTokenStatus.message)
return errorStatus("system_busy")
}
val clientSecret = secretKeyTokenStatus.getData<String>()
val redirectUrl = ""
val clientId = appleConfig.getString("client_id")
val formData = MultiMap.caseInsensitiveMultiMap()
formData.add("client_secret",clientSecret)
.add("client_id",clientId)
.add("redirect_uri",redirectUrl)
.add("grant_type","authorization_code")
.add("code",authCode)
println("accessTokenEndpoint - $accessTokenEndpoint")
println("formData - $formData")
val responseData = httpClient(this::class)
.postAbs(accessTokenEndpoint)
.putHeader("Content-Type","application/x-www-form-urlencoded")
.sendFormAwait(formData)
.bodyAsJsonObject()
println("responseData - ${responseData}")
if(responseData.containsKey("error")){
logger.fatal(responseData.getString("error"))
return errorStatus("social_auth_failed")
}
//val responseIdToken = responseData.getString("id_token","")
return successStatus(data = responseData)
} catch (e: Exception){
logger.fatal(e.message,e)
errorStatus("system_busy")
}
}