使用Apple Java用户验证登录

时间:2019-10-05 20:30:07

标签: java spring-boot jwt apple-sign-in

我已经实现了新的Apple功能“使用Apple登录”的应用程序端,但是我无法在后端使用AuthorizationCode进行验证。我的后端是用Java编写的,无法生成JWT并无法与Apple服务器通信。

2 个答案:

答案 0 :(得分:2)

首先访问developer.apple.com->证书,标识符和配置文件->密钥。为Apple登录生成密钥并下载该密钥。您无法再次下载此密钥,因此请将其保存在安全的地方,不要与他人共享。另外,此处显示的“密钥ID”也要注意这一点,以后将需要它。您还需要团队ID。如果您不知道,它会写在页面的右上角,例如YOURNAME-XX0XX00XXX。

您将基本上遵循以下步骤。

1。通过密钥生成JWT

2。使用令牌发送身份验证代码

3。解码响应

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

/*
* Returns unique user id from apple
* */
public static String appleAuth(String authorizationCode) throws Exception {

    String token = generateJWT();

    HttpResponse<String> response = Unirest.post(APPLE_AUTH_URL)
            .header("Content-Type", "application/x-www-form-urlencoded")
            .field("client_id", CLIENT_ID)
            .field("client_secret", token)
            .field("grant_type", "authorization_code")
            .field("code", authorizationCode)
            .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;

..getters and setters}

答案 1 :(得分:0)

我也有一个错误,但是经过一些调整后,它的工作正常,请在下面找到我的调整,注意,它在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")
    }
}