Cloud Endpoints + Firebase Admin SDK:通过用户的 FirebaseUser 令牌对用户进行身份验证
Cloud Endpoints + Firebase Admin SDK: authenticating users via their FirebaseUser tokens
我已经使用 google 云端点(版本 1,使用 android studio)为我的 android 应用程序构建了一个移动后端。我希望通过 email/password 对我的用户进行身份验证,因此我使用 Firebase 身份验证来进行此操作。 Firebase Authentication sdk 允许我在客户端获取每个用户的令牌(在 android 中),而 firebase admin sdk 允许我检查后端令牌的有效性。我知道在云端点中我可以提供自己的自定义身份验证器(请参阅:Google Cloud Endpoints and user's authentication),并且我计划在我的自定义身份验证器中调用 firebase admin sdk 来验证用户提供的令牌。
我的问题是,因为我正在使用 google 云端点来构建我的后端,所以我不知道在哪里插入代码来初始化 firebase 管理对象,这是我可以验证之前所需的任何令牌。在常规应用程序引擎环境中,您将在 HTTPServlet 的 init() 方法中执行此初始化(请参阅 https://github.com/GoogleCloudPlatform/firebase-appengine-backend/blob/master/src/main/java/com/google/cloud/solutions/flexenv/backend/MessageProcessorServlet.java ),但云端点通过自动提供 "SystemServiceServlet" 作为 HTTPServlet 来向您隐藏此操作。我已经尝试子类化 SystemServiceServlet 并覆盖 init() 方法,但是随后将端点部署到 App Engine 失败,因为显然,android 客户端库的创建要求必须使用 SystemServiceServlet(而且它必须被命名为 "SystemServiceServlet").
我可以在云端点提供的每个 api 方法中初始化 firebase 管理应用程序(例如,在我的 api 的插入方法中),但这似乎是这样将是极其低效的。我将如何在使用 google 云端点构建的后端中使用 Firebase admin sdk?
非常感谢您的宝贵时间
由于我无法在 Cloud Endpoints 中找到合适的位置来初始化 Firebase 管理代码,因此我编写了自己的服务器端 Java 代码来根据 https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_a_third-party_jwt_library[= 验证 Firebase 令牌13=]
这是一个帮助程序 class,您可以使用它来验证用户的 Firebase 令牌并获取他们的 Firebase 用户 uid(此代码使用 https://bitbucket.org/b_c/jose4j/wiki/Home 中的 jose.4.j 库来执行此操作JWT 操作):
public class TokenManager {
private final static String PROJECT_ID = "your_firebase_project_id";
private final static String AUDIENCE = PROJECT_ID;
private final static String ISSUER = "https://securetoken.google.com/" + PROJECT_ID;
private final static String KEYS_URL = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com";
/**
* Parses and verifies a FirebaseUser ID token (JWT) and returns the associated user's uid
*
* @param token the firebase user's token
* @return the firebase user UID
* @throws UnauthorizedException if the token is invalid.
*/
public static String verfiyToken(String token) throws UnauthorizedException{
JwtConsumer firstPassJwtConsumer = new JwtConsumerBuilder()
.setSkipAllValidators()
.setDisableRequireSignature()
.setSkipSignatureVerification()
.build();
//The first JwtConsumer is basically just used to parse the JWT into a JwtContext object.
JwtContext jwtContext;
try {
jwtContext = firstPassJwtConsumer.process(token);
} catch (InvalidJwtException e) {
throw new UnauthorizedException(e.getMessage());
}
// get the key id from the header of the JWT
List<JsonWebStructure> list = jwtContext.getJoseObjects();
String kid = list.get(0).getKeyIdHeaderValue();
String keyAsString;
try {
keyAsString = getPublicKey(kid);
} catch (IOException e) {
throw new UnauthorizedException(e.getMessage());
}
// decode the key into proper format
InputStream certIs = new ByteArrayInputStream(Charset.forName("UTF-8").encode(keyAsString).array());
CertificateFactory certificateFactory;
try {
certificateFactory = CertificateFactory.getInstance("X.509");
} catch (CertificateException e) {
throw new UnauthorizedException(e.getMessage());
}
X509Certificate cert;
try {
cert = (X509Certificate) certificateFactory.generateCertificate(certIs);
} catch (CertificateException e) {
throw new UnauthorizedException(e.getMessage());
}
PublicKey key = cert.getPublicKey();
// now that we have the public key, we can verify the JWT
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
.setRequireExpirationTime() // the JWT must have an expiration time
.setMaxFutureValidityInMinutes(300) // but the expiration time can't be too crazy
.setAllowedClockSkewInSeconds(30) // allow some leeway in validating time based claims to account for clock skew
.setRequireSubject() // the JWT must have a subject claim
.setExpectedIssuer(ISSUER) // whom the JWT needs to have been issued by
.setExpectedAudience(AUDIENCE) // to whom the JWT is intended for
.setVerificationKey(key) // verify the signature with the public key
.build(); // create the JwtConsumer instance
JwtClaims jwtClaims;
try {
// Validate the JWT and process it to the Claims
jwtClaims = jwtConsumer.processToClaims(token);
} catch (InvalidJwtException e) {
throw new UnauthorizedException(e.getMessage());
}
String userUid;
try {
userUid = jwtClaims.getSubject();
} catch(MalformedClaimException e) {
throw new UnauthorizedException(e.getMessage());
}
return userUid;
}
/**
* Grab the certificate corresponding to the keyid specified in the JWT
*
* @param kid key id corresponding to one of the public keys listed at public keys listed at
* https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com
* @return the certificate
* @throws IOException if the process fails
*/
private static String getPublicKey(String kid) throws IOException {
URL url = new URL(KEYS_URL);
HttpURLConnection request = (HttpURLConnection) url.openConnection();
request.connect();
JsonParser jp = new JsonParser(); //from gson
JsonElement root = jp.parse(new InputStreamReader((InputStream) request.getContent()));
JsonObject rootobj = root.getAsJsonObject();
String publicKey = rootobj.get(kid).getAsString();
return publicKey;
}
}
@Dan7620,提出了另一种方法,但没有解决问题。这是一个简单的解决方案,它使用 Firebase Admin SDK,在 Cloud Endpoints 模块中正确配置和初始化。我将在这里总结一下步骤:
- 将您的 serviceAccountKey.json 放入应用程序的 WEB-INF 文件夹中。
将此插入您的 appengineweb.xml:
<resource-files>
<include path="/**.json" />
</resource-files>
在某处定义一个 class,如下所示。强制有一个单独的 init() 方法:
public class FirebaseService {
public static void init() {
try {
FileInputStream serviceAccount = new FileInputStream(new File("WEB-INF/serviceAccountKey.json"));
FirebaseOptions options = new FirebaseOptions.Builder()
.setCredential(FirebaseCredentials.fromCertificate(serviceAccount))
.setDatabaseUrl("https://[YOUR_APP_NAME].firebaseio.com/")
.build();
FirebaseApp.initializeApp(options);
System.out.print("In Firebase Init module...!!");
} catch(FileNotFoundException ignored) {}
}
在您定义的任何端点的任何 static{} 代码中调用此方法。例如:
static {
ObjectifyService.register(FCMTokenMap.class);
FirebaseService.init();
}
- 您可以从任何地方调用涉及数据库、FCM 等的其他 Firebase 方法..!
我已经使用 google 云端点(版本 1,使用 android studio)为我的 android 应用程序构建了一个移动后端。我希望通过 email/password 对我的用户进行身份验证,因此我使用 Firebase 身份验证来进行此操作。 Firebase Authentication sdk 允许我在客户端获取每个用户的令牌(在 android 中),而 firebase admin sdk 允许我检查后端令牌的有效性。我知道在云端点中我可以提供自己的自定义身份验证器(请参阅:Google Cloud Endpoints and user's authentication),并且我计划在我的自定义身份验证器中调用 firebase admin sdk 来验证用户提供的令牌。
我的问题是,因为我正在使用 google 云端点来构建我的后端,所以我不知道在哪里插入代码来初始化 firebase 管理对象,这是我可以验证之前所需的任何令牌。在常规应用程序引擎环境中,您将在 HTTPServlet 的 init() 方法中执行此初始化(请参阅 https://github.com/GoogleCloudPlatform/firebase-appengine-backend/blob/master/src/main/java/com/google/cloud/solutions/flexenv/backend/MessageProcessorServlet.java ),但云端点通过自动提供 "SystemServiceServlet" 作为 HTTPServlet 来向您隐藏此操作。我已经尝试子类化 SystemServiceServlet 并覆盖 init() 方法,但是随后将端点部署到 App Engine 失败,因为显然,android 客户端库的创建要求必须使用 SystemServiceServlet(而且它必须被命名为 "SystemServiceServlet").
我可以在云端点提供的每个 api 方法中初始化 firebase 管理应用程序(例如,在我的 api 的插入方法中),但这似乎是这样将是极其低效的。我将如何在使用 google 云端点构建的后端中使用 Firebase admin sdk?
非常感谢您的宝贵时间
由于我无法在 Cloud Endpoints 中找到合适的位置来初始化 Firebase 管理代码,因此我编写了自己的服务器端 Java 代码来根据 https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_a_third-party_jwt_library[= 验证 Firebase 令牌13=]
这是一个帮助程序 class,您可以使用它来验证用户的 Firebase 令牌并获取他们的 Firebase 用户 uid(此代码使用 https://bitbucket.org/b_c/jose4j/wiki/Home 中的 jose.4.j 库来执行此操作JWT 操作):
public class TokenManager {
private final static String PROJECT_ID = "your_firebase_project_id";
private final static String AUDIENCE = PROJECT_ID;
private final static String ISSUER = "https://securetoken.google.com/" + PROJECT_ID;
private final static String KEYS_URL = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com";
/**
* Parses and verifies a FirebaseUser ID token (JWT) and returns the associated user's uid
*
* @param token the firebase user's token
* @return the firebase user UID
* @throws UnauthorizedException if the token is invalid.
*/
public static String verfiyToken(String token) throws UnauthorizedException{
JwtConsumer firstPassJwtConsumer = new JwtConsumerBuilder()
.setSkipAllValidators()
.setDisableRequireSignature()
.setSkipSignatureVerification()
.build();
//The first JwtConsumer is basically just used to parse the JWT into a JwtContext object.
JwtContext jwtContext;
try {
jwtContext = firstPassJwtConsumer.process(token);
} catch (InvalidJwtException e) {
throw new UnauthorizedException(e.getMessage());
}
// get the key id from the header of the JWT
List<JsonWebStructure> list = jwtContext.getJoseObjects();
String kid = list.get(0).getKeyIdHeaderValue();
String keyAsString;
try {
keyAsString = getPublicKey(kid);
} catch (IOException e) {
throw new UnauthorizedException(e.getMessage());
}
// decode the key into proper format
InputStream certIs = new ByteArrayInputStream(Charset.forName("UTF-8").encode(keyAsString).array());
CertificateFactory certificateFactory;
try {
certificateFactory = CertificateFactory.getInstance("X.509");
} catch (CertificateException e) {
throw new UnauthorizedException(e.getMessage());
}
X509Certificate cert;
try {
cert = (X509Certificate) certificateFactory.generateCertificate(certIs);
} catch (CertificateException e) {
throw new UnauthorizedException(e.getMessage());
}
PublicKey key = cert.getPublicKey();
// now that we have the public key, we can verify the JWT
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
.setRequireExpirationTime() // the JWT must have an expiration time
.setMaxFutureValidityInMinutes(300) // but the expiration time can't be too crazy
.setAllowedClockSkewInSeconds(30) // allow some leeway in validating time based claims to account for clock skew
.setRequireSubject() // the JWT must have a subject claim
.setExpectedIssuer(ISSUER) // whom the JWT needs to have been issued by
.setExpectedAudience(AUDIENCE) // to whom the JWT is intended for
.setVerificationKey(key) // verify the signature with the public key
.build(); // create the JwtConsumer instance
JwtClaims jwtClaims;
try {
// Validate the JWT and process it to the Claims
jwtClaims = jwtConsumer.processToClaims(token);
} catch (InvalidJwtException e) {
throw new UnauthorizedException(e.getMessage());
}
String userUid;
try {
userUid = jwtClaims.getSubject();
} catch(MalformedClaimException e) {
throw new UnauthorizedException(e.getMessage());
}
return userUid;
}
/**
* Grab the certificate corresponding to the keyid specified in the JWT
*
* @param kid key id corresponding to one of the public keys listed at public keys listed at
* https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com
* @return the certificate
* @throws IOException if the process fails
*/
private static String getPublicKey(String kid) throws IOException {
URL url = new URL(KEYS_URL);
HttpURLConnection request = (HttpURLConnection) url.openConnection();
request.connect();
JsonParser jp = new JsonParser(); //from gson
JsonElement root = jp.parse(new InputStreamReader((InputStream) request.getContent()));
JsonObject rootobj = root.getAsJsonObject();
String publicKey = rootobj.get(kid).getAsString();
return publicKey;
}
}
@Dan7620,提出了另一种方法,但没有解决问题。这是一个简单的解决方案,它使用 Firebase Admin SDK,在 Cloud Endpoints 模块中正确配置和初始化。我将在这里总结一下步骤:
- 将您的 serviceAccountKey.json 放入应用程序的 WEB-INF 文件夹中。
将此插入您的 appengineweb.xml:
<resource-files> <include path="/**.json" /> </resource-files>
在某处定义一个 class,如下所示。强制有一个单独的 init() 方法:
public class FirebaseService { public static void init() { try { FileInputStream serviceAccount = new FileInputStream(new File("WEB-INF/serviceAccountKey.json")); FirebaseOptions options = new FirebaseOptions.Builder() .setCredential(FirebaseCredentials.fromCertificate(serviceAccount)) .setDatabaseUrl("https://[YOUR_APP_NAME].firebaseio.com/") .build(); FirebaseApp.initializeApp(options); System.out.print("In Firebase Init module...!!"); } catch(FileNotFoundException ignored) {} }
在您定义的任何端点的任何 static{} 代码中调用此方法。例如:
static { ObjectifyService.register(FCMTokenMap.class); FirebaseService.init(); }
- 您可以从任何地方调用涉及数据库、FCM 等的其他 Firebase 方法..!