Tính năng Đăng nhập bằng Google cho Trợ lý mang lại trải nghiệm người dùng đơn giản và dễ dàng nhất cho người dùng và nhà phát triển cho cả việc liên kết tài khoản và tạo tài khoản. Hành động của bạn có thể yêu cầu quyền truy cập vào hồ sơ trên Google của người dùng trong cuộc trò chuyện, bao gồm tên, địa chỉ email và ảnh hồ sơ của người dùng.
Thông tin hồ sơ có thể được dùng để tạo trải nghiệm người dùng được cá nhân hoá trong Hành động của bạn. Nếu bạn có ứng dụng trên các nền tảng khác và chúng sử dụng tính năng Đăng nhập bằng Google, bạn cũng có thể tìm và liên kết với tài khoản của người dùng hiện có, tạo tài khoản mới, và thiết lập một kênh liên lạc trực tiếp với người dùng.
Để liên kết tài khoản bằng tính năng Đăng nhập bằng Google, bạn phải yêu cầu người dùng đồng ý để truy cập hồ sơ trên Google của họ. Sau đó, bạn sử dụng thông tin này trong hồ sơ của trẻ, để ví dụ địa chỉ email của họ để xác định người dùng trong hệ thống của bạn.
Triển khai việc liên kết tài khoản Đăng nhập bằng Google
Làm theo các bước trong các phần sau để thêm tài khoản Đăng nhập bằng Google liên kết với Hành động.
Định cấu hình dự án
Để định cấu hình dự án nhằm sử dụng tính năng liên kết tài khoản Đăng nhập bằng Google, hãy làm theo các bước sau:
- Mở Actions Console rồi chọn một dự án.
- Nhấp vào thẻ Phát triển rồi chọn Liên kết tài khoản.
- Bật nút chuyển bên cạnh Liên kết tài khoản.
- Trong phần Tạo tài khoản, hãy chọn Có.
Trong mục Loại liên kết, hãy chọn Đăng nhập bằng Google.
Mở Thông tin khách hàng và ghi lại giá trị Mã khách hàng do Google cấp cho Hành động của bạn.
Nhấp vào Lưu.
Bắt đầu quy trình xác thực
Sử dụng ý định trợ giúp Đăng nhập vào tài khoản để bắt đầu quy trình xác thực.
Sau khi người dùng cho phép bạn truy cập vào hồ sơ trên Google của họ, bạn sẽ nhận được mã thông báo ID Google chứa thông tin hồ sơ trên Google của người dùng trong mỗi đối với hành động của bạn.
Để truy cập thông tin hồ sơ của người dùng, trước tiên bạn cần xác thực và giải mã mã thông báo này bằng cách làm như sau:
- Sử dụng thư viện giải mã JWT cho ngôn ngữ của bạn để giải mã mã thông báo và sử dụng khoá công khai của Google (có trong JWK hoặc định dạng PEM) để xác minh chữ ký của mã thông báo.
- Xác minh rằng nhà phát hành mã thông báo (trường
iss
trong mã thông báo đã giải mã) là https://accounts.google.com và đối tượng (trườngaud
trong mã thông báo đã giải mã) là giá trị của Mã ứng dụng khách do Google cấp cho Actions của bạn, được chỉ định cho dự án của bạn trong bảng điều khiển Actions on Google.
Sau đây là ví dụ về mã thông báo đã giải mã:
{ "sub": 1234567890, // The unique ID of the user's Google Account "iss": "https://accounts.google.com", // The token's issuer "aud": "123-abc.apps.googleusercontent.com", // Client ID assigned to your Actions project "iat": 233366400, // Unix timestamp of the token's creation time "exp": 233370000, // Unix timestamp of the token's expiration time "name": "Jan Jansen", "given_name": "Jan", "family_name": "Jansen", "email": "jan@gmail.com", // If present, the user's email address "locale": "en_US" }
Nếu bạn sử dụng thư viện ứng dụng Actions on Google cho Node.js hoặc thư viện ứng dụng Java, nó sẽ đảm nhận việc xác thực và giải mã mã thông báo và cấp cho bạn quyền truy cập vào nội dung hồ sơ, như được thể hiện trong các đoạn mã sau. Xin lưu ý rằng JSON bên dưới mô tả lần lượt yêu cầu webhook cho Dialogflow và SDK Hành động.
Các đoạn mã sau đây dùng Dialogflow để đăng nhập:
const {dialogflow, SignIn} = require('actions-on-google'); const app = dialogflow({ // REPLACE THE PLACEHOLDER WITH THE CLIENT_ID OF YOUR ACTIONS PROJECT clientId: CLIENT_ID, }); // Intent that starts the account linking flow. app.intent('Start Signin', (conv) => { conv.ask(new SignIn('To get your account details')); }); // Create a Dialogflow intent with the `actions_intent_SIGN_IN` event. app.intent('Get Signin', (conv, params, signin) => { if (signin.status === 'OK') { const payload = conv.user.profile.payload; conv.ask(`I got your account details, ${payload.name}. What do you want to do next?`); } else { conv.ask(`I won't be able to save your data, but what do you want to do next?`); } });
private String clientId = "<your_client_id>"; @ForIntent("Start Signin") public ActionResponse text(ActionRequest request) { ResponseBuilder rb = getResponseBuilder(request); return rb.add(new SignIn().setContext("To get your account details")).build(); } @ForIntent("actions.intent.SIGN_IN") public ActionResponse getSignInStatus(ActionRequest request) { ResponseBuilder responseBuilder = getResponseBuilder(request); if (request.isSignInGranted()) { GoogleIdToken.Payload profile = getUserProfile(request.getUser().getIdToken()); responseBuilder.add( "I got your account details, " + profile.get("given_name") + ". What do you want to do next?"); } else { responseBuilder.add("I won't be able to save your data, but what do you want to do next?"); } return responseBuilder.build(); } private GoogleIdToken.Payload getUserProfile(String idToken) { GoogleIdToken.Payload profile = null; try { profile = decodeIdToken(idToken); } catch (Exception e) { LOGGER.error("error decoding idtoken"); LOGGER.error(e.toString()); } return profile; } private GoogleIdToken.Payload decodeIdToken(String idTokenString) throws GeneralSecurityException, IOException { HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport(); JacksonFactory jsonFactory = JacksonFactory.getDefaultInstance(); GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory) // Specify the CLIENT_ID of the app that accesses the backend: .setAudience(Collections.singletonList(clientId)) .build(); GoogleIdToken idToken = verifier.verify(idTokenString); return idToken.getPayload(); }
{ "responseId": "", "queryResult": { "queryText": "", "action": "", "parameters": {}, "allRequiredParamsPresent": true, "fulfillmentText": "", "fulfillmentMessages": [], "outputContexts": [], "intent": { "name": "Get Signin", "displayName": "Get Signin" }, "intentDetectionConfidence": 1, "diagnosticInfo": {}, "languageCode": "" }, "originalDetectIntentRequest": { "source": "google", "version": "2", "payload": { "isInSandbox": true, "surface": { "capabilities": [ { "name": "actions.capability.SCREEN_OUTPUT" }, { "name": "actions.capability.AUDIO_OUTPUT" }, { "name": "actions.capability.MEDIA_RESPONSE_AUDIO" }, { "name": "actions.capability.WEB_BROWSER" } ] }, "inputs": [ { "rawInputs": [], "intent": "", "arguments": [ { "name": "SIGN_IN", "extension": { "@type": "type.googleapis.com/google.actions.v2.SignInValue", "status": "OK" } } ] } ], "user": { "idToken": "peJaCGci..." }, "conversation": {}, "availableSurfaces": [ { "capabilities": [ { "name": "actions.capability.SCREEN_OUTPUT" }, { "name": "actions.capability.AUDIO_OUTPUT" }, { "name": "actions.capability.MEDIA_RESPONSE_AUDIO" }, { "name": "actions.capability.WEB_BROWSER" } ] } ] } }, "session": "" }
Các đoạn mã sau đây sử dụng SDK Hành động để đăng nhập:
const {actionssdk, SignIn} = require('actions-on-google'); const app = actionssdk({ // REPLACE THE PLACEHOLDER WITH THE CLIENT_ID OF YOUR ACTIONS PROJECT clientId: CLIENT_ID, }); // Intent that starts the account linking flow. app.intent('actions.intent.TEXT', (conv) => { conv.ask(new SignIn('To get your account details')); }); // Create an Actions SDK intent with the `actions_intent_SIGN_IN` event. app.intent('actions.intent.SIGN_IN', (conv, params, signin) => { if (signin.status === 'OK') { const payload = conv.user.profile.payload; conv.ask(`I got your account details, ${payload.name}. What do you want to do next?`); } else { conv.ask(`I won't be able to save your data, but what do you want to do next?`); } });
private String clientId = "<your_client_id>"; @ForIntent("actions.intent.TEXT") public ActionResponse text(ActionRequest request) { ResponseBuilder rb = getResponseBuilder(request); return rb.add(new SignIn().setContext("To get your account details")).build(); } @ForIntent("actions.intent.SIGN_IN") public ActionResponse getSignInStatus(ActionRequest request) { ResponseBuilder responseBuilder = getResponseBuilder(request); if (request.isSignInGranted()) { GoogleIdToken.Payload profile = getUserProfile(request.getUser().getIdToken()); responseBuilder.add( "I got your account details, " + profile.get("given_name") + ". What do you want to do next?"); } else { responseBuilder.add("I won't be able to save your data, but what do you want to do next?"); } return responseBuilder.build(); } private GoogleIdToken.Payload getUserProfile(String idToken) { GoogleIdToken.Payload profile = null; try { profile = decodeIdToken(idToken); } catch (Exception e) { LOGGER.error("error decoding idtoken"); LOGGER.error(e.toString()); } return profile; } private GoogleIdToken.Payload decodeIdToken(String idTokenString) throws GeneralSecurityException, IOException { HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport(); JacksonFactory jsonFactory = JacksonFactory.getDefaultInstance(); GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory) // Specify the CLIENT_ID of the app that accesses the backend: .setAudience(Collections.singletonList(this.clientId)) .build(); GoogleIdToken idToken = verifier.verify(idTokenString); return idToken.getPayload(); }
{ "user": { "idToken": "peJaCGci..." }, "device": {}, "surface": { "capabilities": [ { "name": "actions.capability.SCREEN_OUTPUT" }, { "name": "actions.capability.AUDIO_OUTPUT" }, { "name": "actions.capability.MEDIA_RESPONSE_AUDIO" }, { "name": "actions.capability.WEB_BROWSER" } ] }, "conversation": {}, "inputs": [ { "rawInputs": [], "intent": "actions.intent.SIGN_IN", "arguments": [ { "name": "SIGN_IN", "extension": { "@type": "type.googleapis.com/google.actions.v2.SignInValue", "status": "OK" } } ] } ], "availableSurfaces": [ { "capabilities": [ { "name": "actions.capability.SCREEN_OUTPUT" }, { "name": "actions.capability.AUDIO_OUTPUT" }, { "name": "actions.capability.MEDIA_RESPONSE_AUDIO" }, { "name": "actions.capability.WEB_BROWSER" } ] } ] }
Xử lý các yêu cầu về quyền truy cập dữ liệu
Để xử lý yêu cầu truy cập dữ liệu, chỉ cần xác minh rằng người dùng đã xác nhận bằng mã Google đã có trong cơ sở dữ liệu của bạn. Đoạn mã sau đây cho thấy ví dụ về cách kiểm tra xem tài khoản người dùng đã tồn tại trong cơ sở dữ liệu Firestore hay chưa.
const admin = require('firebase-admin'); const functions = require('firebase-functions'); admin.initializeApp(); const auth = admin.auth(); const db = admin.firestore(); // Save the user in the Firestore DB after successful signin app.intent('Get Sign In', async (conv, params, signin) => { if (signin.status !== 'OK') { return conv.close(`Let's try again next time.`); } const color = conv.data[Fields.COLOR]; const {email} = conv.user; if (!conv.data.uid && email) { try { conv.data.uid = (await auth.getUserByEmail(email)).uid; } catch (e) { if (e.code !== 'auth/user-not-found') { throw e; } // If the user is not found, create a new Firebase auth user // using the email obtained from the Google Assistant conv.data.uid = (await auth.createUser({email})).uid; } } if (conv.data.uid) { conv.user.ref = db.collection('users').doc(conv.data.uid); } conv.close(`I saved ${color} as your favorite color for next time.`); }); // Retrieve the user's favorite color if an account exists, ask if it doesn't. app.intent('Default Welcome Intent', async (conv) => { const {payload} = conv.user.profile; const name = payload ? ` ${payload.given_name}` : ''; conv.ask(`Hi${name}!`); // conv.user.ref contains the id of the record for the user in a Firestore DB if (conv.user.ref) { const doc = await conv.user.ref.get(); if (doc.exists) { const color = doc.data()[Fields.COLOR]; return conv.ask(`Your favorite color was ${color}. ` + 'Tell me a color to update it.'); } } conv.ask(`What's your favorite color?`); });
private class FirestoreManager { private final Firestore db; private final DocumentReference userDocRef; private final String uid; public FirestoreManager(String databaseUrl, String email) throws IOException, FirebaseAuthException { if (FirebaseApp.getApps().isEmpty()) { // Use the application default credentials (works on GCP based hosting). FirebaseOptions options = new FirebaseOptions.Builder() .setCredentials(GoogleCredentials.getApplicationDefault()) .setDatabaseUrl(databaseUrl) .build(); FirebaseApp.initializeApp(options); } this.db = FirestoreClient.getFirestore(); UserRecord userRecord; try { userRecord = FirebaseAuth.getInstance().getUserByEmail(email); } catch (FirebaseAuthException e) { if (e.getErrorCode() == FIREBASE_USER_NOT_FOUND_ERROR) { UserRecord.CreateRequest createRequest = new UserRecord.CreateRequest().setEmail(email); userRecord = FirebaseAuth.getInstance().createUser(createRequest); } else { throw e; } } uid = userRecord.getUid(); userDocRef = db.collection(FIRESTORE_USERS_PATH).document(uid); } public String readUserColor() throws ExecutionException, InterruptedException { ApiFuture<DocumentSnapshot> future = userDocRef.get(); // future.get() blocks on response DocumentSnapshot document = future.get(); if (document.exists()) { return document.get(COLOR_KEY).toString(); } else { return ""; } } public Timestamp writeUserColor(String color) throws ExecutionException, InterruptedException { Map<String, Object> docData = new HashMap<>(); docData.put(COLOR_KEY, color); ApiFuture<WriteResult> future = userDocRef.set(docData); // future.get() blocks on response return future.get().getUpdateTime(); } } @ForIntent("Get Sign In") public ActionResponse getSignIn(ActionRequest request) { LOGGER.info("Get sign in intent start."); ResponseBuilder responseBuilder = getResponseBuilder(request); if (request.isSignInGranted()) { String color = request.getConversationData().get(COLOR_KEY).toString(); GoogleIdToken.Payload profile = getUserProfile(request.getUser().getIdToken()); try { FirestoreManager firestoreManager = new FirestoreManager(DATABASE_URL, profile.getEmail()); saveColor(firestoreManager, color); } catch (Exception e) { LOGGER.error(e.toString()); } responseBuilder .add("I saved " + color + " as your favorite color for next time.") .endConversation(); } else { responseBuilder.add("Let's try again next time"); } LOGGER.info("Get sign in intent end."); return responseBuilder.build(); } private void saveColor(FirestoreManager firestoreManager, String color) { try { Timestamp updateTime = firestoreManager.writeUserColor(color); LOGGER.info(String.format("Update time: %s", updateTime.toString())); } catch (Exception e) { LOGGER.error(e.toString()); } }